diff --git a/apps/app-frontend/src/components/ui/settings/instances/InstancesSyncedSettings.vue b/apps/app-frontend/src/components/ui/settings/instances/InstancesSyncedSettings.vue index f96b836b53..548abba7af 100644 --- a/apps/app-frontend/src/components/ui/settings/instances/InstancesSyncedSettings.vue +++ b/apps/app-frontend/src/components/ui/settings/instances/InstancesSyncedSettings.vue @@ -67,27 +67,27 @@ const messages = defineMessages({ // }, multiplayerServers: { id: 'app.settings.synced-options.multiplayer-servers', - defaultMessage: 'Multiplayer servers', + defaultMessage: 'Sync multiplayer servers', }, multiplayerServersDescription: { id: 'app.settings.synced-options.multiplayer-servers.description', - defaultMessage: 'Sync multiplayer servers across your instances.', + defaultMessage: 'Use the same multiplayer servers across your instances.', }, commandHistory: { id: 'app.settings.synced-options.command-history', - defaultMessage: 'Command history', + defaultMessage: 'Sync command history', }, commandHistoryDescription: { id: 'app.settings.synced-options.command-history.description', - defaultMessage: 'Sync command history across your instances.', + defaultMessage: 'Use the same command history across your instances.', }, creativeHotbars: { id: 'app.settings.synced-options.creative-hotbars', - defaultMessage: 'Saved creative hotbars', + defaultMessage: 'Sync saved creative hotbars', }, creativeHotbarsDescription: { id: 'app.settings.synced-options.creative-hotbars.description', - defaultMessage: 'Sync saved creative hotbars across your instances.', + defaultMessage: 'Use the same saved creative hotbars across your instances.', }, chooseSyncSourceTitle: { id: 'app.settings.synced-options.choose-sync-source.title', diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json index 4dc1854696..cb74a71b57 100644 --- a/apps/app-frontend/src/locales/en-US/index.json +++ b/apps/app-frontend/src/locales/en-US/index.json @@ -1719,28 +1719,28 @@ "message": "Choose a sync source" }, "app.settings.synced-options.command-history": { - "message": "Command history" + "message": "Sync command history" }, "app.settings.synced-options.command-history.description": { - "message": "Sync command history across your instances." + "message": "Use the same command history across your instances." }, "app.settings.synced-options.command-history.editor-title": { "message": "Edit command history" }, "app.settings.synced-options.creative-hotbars": { - "message": "Saved creative hotbars" + "message": "Sync saved creative hotbars" }, "app.settings.synced-options.creative-hotbars.description": { - "message": "Sync saved creative hotbars across your instances." + "message": "Use the same saved creative hotbars across your instances." }, "app.settings.synced-options.multiplayer-servers": { - "message": "Multiplayer servers" + "message": "Sync multiplayer servers" }, "app.settings.synced-options.multiplayer-servers.address": { "message": "Server address" }, "app.settings.synced-options.multiplayer-servers.description": { - "message": "Sync multiplayer servers across your instances." + "message": "Use the same multiplayer servers across your instances." }, "app.settings.synced-options.multiplayer-servers.editor-title": { "message": "Edit synced servers" @@ -2589,7 +2589,7 @@ "message": "Select update channel" }, "instance.settings.tabs.hooks.description": { - "message": "Hooks allow advanced users to run certain system commands before and after launching the game." + "message": "Run instance-specific system commands before and after launching the game." }, "instance.settings.tabs.hooks.post-exit": { "message": "Post-exit" @@ -2610,7 +2610,7 @@ "message": "Enter pre-launch command..." }, "instance.settings.tabs.hooks.title": { - "message": "Game launch hooks" + "message": "Custom game launch hooks" }, "instance.settings.tabs.hooks.variables.description": { "message": "Hooks run in the working directory of the instance, with the following variables:" @@ -2652,16 +2652,16 @@ "message": "Installation settings are unavailable while this instance is locked." }, "instance.settings.tabs.java.custom-environment-variables": { - "message": "Use custom environment variables for this instance." + "message": "Set environment variables separately for this instance." }, "instance.settings.tabs.java.custom-java-arguments": { - "message": "Use custom Java arguments for this instance." + "message": "Set Java arguments separately for this instance." }, "instance.settings.tabs.java.custom-java-installation": { - "message": "Use a custom Java installation for this instance." + "message": "Choose a different Java installation for this instance." }, "instance.settings.tabs.java.custom-memory-allocation": { - "message": "Use a custom memory allocation for this instance." + "message": "Set the memory allocation separately for this instance." }, "instance.settings.tabs.java.enter-environment-variables": { "message": "Enter environmental variables..." @@ -2670,19 +2670,19 @@ "message": "Enter Java arguments..." }, "instance.settings.tabs.java.environment-variables": { - "message": "Environment variables" + "message": "Custom environment variables" }, "instance.settings.tabs.java.hooks": { "message": "Hooks" }, "instance.settings.tabs.java.java-arguments": { - "message": "Java arguments" + "message": "Custom Java arguments" }, "instance.settings.tabs.java.java-installation": { - "message": "Java installation" + "message": "Custom Java installation" }, "instance.settings.tabs.java.java-memory": { - "message": "Memory allocated" + "message": "Custom memory allocation" }, "instance.settings.tabs.java.java-path-placeholder": { "message": "/path/to/java" @@ -2694,7 +2694,7 @@ "message": "Sharing" }, "instance.settings.tabs.synced-options.command-history": { - "message": "Command history" + "message": "Unsync command history" }, "instance.settings.tabs.synced-options.command-history.disabled-in-app": { "message": "Command history syncing is turned off in app settings." @@ -2703,7 +2703,7 @@ "message": "Keep this instance's command history separate from synced command history." }, "instance.settings.tabs.synced-options.creative-hotbars": { - "message": "Saved creative hotbars" + "message": "Unsync saved creative hotbars" }, "instance.settings.tabs.synced-options.creative-hotbars.disabled-in-app": { "message": "Saved creative hotbar syncing is turned off in app settings." @@ -2727,7 +2727,7 @@ "message": "Use synced" }, "instance.settings.tabs.synced-options.multiplayer-servers": { - "message": "Multiplayer servers" + "message": "Unsync multiplayer servers" }, "instance.settings.tabs.synced-options.multiplayer-servers.disabled-in-app": { "message": "Multiplayer server syncing is turned off in app settings." @@ -2742,10 +2742,10 @@ "message": "Enable an override to keep a synced setting separate for this instance." }, "instance.settings.tabs.window": { - "message": "Window" + "message": "Custom window settings" }, "instance.settings.tabs.window.custom-window-settings": { - "message": "Use custom window settings for this instance." + "message": "Configure fullscreen and launch resolution separately for this instance." }, "instance.settings.tabs.window.fullscreen": { "message": "Fullscreen" diff --git a/apps/app-frontend/src/pages/instance/components/settings-modal/hooks-settings.vue b/apps/app-frontend/src/pages/instance/components/settings-modal/hooks-settings.vue index 8e214fdf31..3dfc0ca208 100644 --- a/apps/app-frontend/src/pages/instance/components/settings-modal/hooks-settings.vue +++ b/apps/app-frontend/src/pages/instance/components/settings-modal/hooks-settings.vue @@ -61,12 +61,11 @@ watch( const messages = defineMessages({ hooks: { id: 'instance.settings.tabs.hooks.title', - defaultMessage: 'Game launch hooks', + defaultMessage: 'Custom game launch hooks', }, hooksDescription: { id: 'instance.settings.tabs.hooks.description', - defaultMessage: - 'Hooks allow advanced users to run certain system commands before and after launching the game.', + defaultMessage: 'Run instance-specific system commands before and after launching the game.', }, hookVariablesDescription: { id: 'instance.settings.tabs.hooks.variables.description', diff --git a/apps/app-frontend/src/pages/instance/components/settings-modal/java-settings.vue b/apps/app-frontend/src/pages/instance/components/settings-modal/java-settings.vue index 64402d3030..b3dadad3c1 100644 --- a/apps/app-frontend/src/pages/instance/components/settings-modal/java-settings.vue +++ b/apps/app-frontend/src/pages/instance/components/settings-modal/java-settings.vue @@ -155,11 +155,11 @@ watch( const messages = defineMessages({ javaInstallation: { id: 'instance.settings.tabs.java.java-installation', - defaultMessage: 'Java installation', + defaultMessage: 'Custom Java installation', }, customJavaInstallation: { id: 'instance.settings.tabs.java.custom-java-installation', - defaultMessage: 'Use a custom Java installation for this instance.', + defaultMessage: 'Choose a different Java installation for this instance.', }, javaPathPlaceholder: { id: 'instance.settings.tabs.java.java-path-placeholder', @@ -167,19 +167,19 @@ const messages = defineMessages({ }, javaMemory: { id: 'instance.settings.tabs.java.java-memory', - defaultMessage: 'Memory allocated', + defaultMessage: 'Custom memory allocation', }, customMemoryAllocation: { id: 'instance.settings.tabs.java.custom-memory-allocation', - defaultMessage: 'Use a custom memory allocation for this instance.', + defaultMessage: 'Set the memory allocation separately for this instance.', }, javaArguments: { id: 'instance.settings.tabs.java.java-arguments', - defaultMessage: 'Java arguments', + defaultMessage: 'Custom Java arguments', }, customJavaArguments: { id: 'instance.settings.tabs.java.custom-java-arguments', - defaultMessage: 'Use custom Java arguments for this instance.', + defaultMessage: 'Set Java arguments separately for this instance.', }, enterJavaArguments: { id: 'instance.settings.tabs.java.enter-java-arguments', @@ -187,11 +187,11 @@ const messages = defineMessages({ }, javaEnvironmentVariables: { id: 'instance.settings.tabs.java.environment-variables', - defaultMessage: 'Environment variables', + defaultMessage: 'Custom environment variables', }, customEnvironmentVariables: { id: 'instance.settings.tabs.java.custom-environment-variables', - defaultMessage: 'Use custom environment variables for this instance.', + defaultMessage: 'Set environment variables separately for this instance.', }, enterEnvironmentVariables: { id: 'instance.settings.tabs.java.enter-environment-variables', diff --git a/apps/app-frontend/src/pages/instance/components/settings-modal/synced-options-settings.vue b/apps/app-frontend/src/pages/instance/components/settings-modal/synced-options-settings.vue index 2c61cc712a..3410a58c7b 100644 --- a/apps/app-frontend/src/pages/instance/components/settings-modal/synced-options-settings.vue +++ b/apps/app-frontend/src/pages/instance/components/settings-modal/synced-options-settings.vue @@ -45,7 +45,7 @@ const messages = defineMessages({ }, multiplayerServers: { id: 'instance.settings.tabs.synced-options.multiplayer-servers', - defaultMessage: 'Multiplayer servers', + defaultMessage: 'Unsync multiplayer servers', }, multiplayerServersDescription: { id: 'instance.settings.tabs.synced-options.multiplayer-servers.override-description', @@ -57,7 +57,7 @@ const messages = defineMessages({ }, commandHistory: { id: 'instance.settings.tabs.synced-options.command-history', - defaultMessage: 'Command history', + defaultMessage: 'Unsync command history', }, commandHistoryDescription: { id: 'instance.settings.tabs.synced-options.command-history.override-description', @@ -69,7 +69,7 @@ const messages = defineMessages({ }, creativeHotbars: { id: 'instance.settings.tabs.synced-options.creative-hotbars', - defaultMessage: 'Saved creative hotbars', + defaultMessage: 'Unsync saved creative hotbars', }, creativeHotbarsDescription: { id: 'instance.settings.tabs.synced-options.creative-hotbars.override-description', diff --git a/apps/app-frontend/src/pages/instance/components/settings-modal/window-settings.vue b/apps/app-frontend/src/pages/instance/components/settings-modal/window-settings.vue index 70dc3b0c3b..7c88c49e85 100644 --- a/apps/app-frontend/src/pages/instance/components/settings-modal/window-settings.vue +++ b/apps/app-frontend/src/pages/instance/components/settings-modal/window-settings.vue @@ -56,11 +56,11 @@ watch( const messages = defineMessages({ window: { id: 'instance.settings.tabs.window', - defaultMessage: 'Window', + defaultMessage: 'Custom window settings', }, customWindowSettings: { id: 'instance.settings.tabs.window.custom-window-settings', - defaultMessage: 'Use custom window settings for this instance.', + defaultMessage: 'Configure fullscreen and launch resolution separately for this instance.', }, fullscreen: { id: 'instance.settings.tabs.window.fullscreen', diff --git a/packages/ui/src/components/image-viewer-editor/editor.vue b/packages/ui/src/components/image-viewer-editor/editor.vue index 19e793b016..0d1fed9f37 100644 --- a/packages/ui/src/components/image-viewer-editor/editor.vue +++ b/packages/ui/src/components/image-viewer-editor/editor.vue @@ -43,7 +43,6 @@ const emit = defineEmits<{ cancel: [] close: [] edit: [] - imageReady: [] next: [] previous: [] save: [payload: ImageViewerEditorSavePayload] @@ -85,6 +84,7 @@ const { fitToViewport, setZoom, setFit, + waitForRender, exportPng, handleKeyboardShortcut, isTextEditing, @@ -108,11 +108,31 @@ const viewZoom = computed(() => zoom.value / Math.max(fitScale.value, Number.EPS const viewZoomPercent = computed(() => Math.round(viewZoom.value * 100)) const viewZoomed = computed(() => viewZoom.value > 1.001) const canViewZoomIn = computed(() => canZoomIn.value && viewZoom.value < MAX_VIEW_ZOOM) +const nativeImageView = computed( + () => + props.mode === 'view' && + [props.item.src, props.item.editorSource?.path].some(isGifOrWebpSource), +) let resizeObserver: ResizeObserver | undefined let initializationGeneration = 0 let initializationChain = Promise.resolve() -let imageReadyFrame: number | undefined + +function isGifOrWebpSource(source?: string) { + if (!source) return false + let pathname = source + try { + pathname = new URL(source, 'https://modrinth.invalid').pathname + } catch { + // ... ignore + } + try { + pathname = decodeURIComponent(pathname) + } catch { + // ... ignore + } + return /\.(?:gif|webp)$/i.test(pathname) +} function queueInitialization() { const generation = ++initializationGeneration @@ -120,7 +140,6 @@ function queueInitialization() { const editorDataPromise = props.loadData(props.item) initializationChain = initializationChain.then(async () => { if (generation !== initializationGeneration) return - let initialized = false try { const editorData = await editorDataPromise if (generation !== initializationGeneration || !canvasElement.value) return @@ -128,7 +147,8 @@ function queueInitialization() { if (generation !== initializationGeneration) return setInteractionEnabled(props.mode === 'edit') observeViewport() - initialized = true + await waitForRender() + if (generation !== initializationGeneration) return } catch (error) { if (generation !== initializationGeneration) return handleError(error) @@ -136,20 +156,11 @@ function queueInitialization() { } finally { if (generation === initializationGeneration) { loadingEditorData.value = false - if (initialized) notifyImageReady() } } }) } -function notifyImageReady() { - if (imageReadyFrame !== undefined) cancelAnimationFrame(imageReadyFrame) - imageReadyFrame = requestAnimationFrame(() => { - imageReadyFrame = undefined - emit('imageReady') - }) -} - function observeViewport() { resizeObserver?.disconnect() if (!viewport.value || !fitBounds.value) return @@ -164,7 +175,6 @@ function fitEditorToViewport() { if (!viewportSize) return fitToViewport(viewportSize.width, viewportSize.height) if (isFit.value) centerViewport() - else notifyImageReady() } function getViewportSize() { @@ -186,72 +196,9 @@ function centerViewport() { 0, (viewport.value.scrollHeight - viewport.value.clientHeight) / 2, ) - notifyImageReady() }) } -function getTextContrast(target: HTMLElement): 'dark' | 'light' { - const renderedCanvas = viewport.value?.querySelector('canvas.lower-canvas') - const context = renderedCanvas?.getContext('2d', { willReadFrequently: true }) - if (!renderedCanvas || !context) return 'light' - - const targetBounds = target.getBoundingClientRect() - const canvasBounds = renderedCanvas.getBoundingClientRect() - const intersection = { - left: Math.max(targetBounds.left, canvasBounds.left), - top: Math.max(targetBounds.top, canvasBounds.top), - right: Math.min(targetBounds.right, canvasBounds.right), - bottom: Math.min(targetBounds.bottom, canvasBounds.bottom), - } - if (intersection.right <= intersection.left || intersection.bottom <= intersection.top) - return 'light' - - const scaleX = renderedCanvas.width / canvasBounds.width - const scaleY = renderedCanvas.height / canvasBounds.height - const sourceX = Math.max(0, Math.floor((intersection.left - canvasBounds.left) * scaleX)) - const sourceY = Math.max(0, Math.floor((intersection.top - canvasBounds.top) * scaleY)) - const sourceWidth = Math.min( - renderedCanvas.width - sourceX, - Math.max(1, Math.ceil((intersection.right - intersection.left) * scaleX)), - ) - const sourceHeight = Math.min( - renderedCanvas.height - sourceY, - Math.max(1, Math.ceil((intersection.bottom - intersection.top) * scaleY)), - ) - - try { - const pixels = context.getImageData(sourceX, sourceY, sourceWidth, sourceHeight).data - const sampleStride = Math.max(1, Math.floor(Math.sqrt((sourceWidth * sourceHeight) / 4096))) - let luminanceTotal = 0 - let sampleCount = 0 - for (let y = 0; y < sourceHeight; y += sampleStride) { - for (let x = 0; x < sourceWidth; x += sampleStride) { - const offset = (y * sourceWidth + x) * 4 - const red = srgbToLinear(pixels[offset] / 255) - const green = srgbToLinear(pixels[offset + 1] / 255) - const blue = srgbToLinear(pixels[offset + 2] / 255) - const alpha = pixels[offset + 3] / 255 - luminanceTotal += (0.2126 * red + 0.7152 * green + 0.0722 * blue) * alpha - sampleCount++ - } - } - - const targetArea = targetBounds.width * targetBounds.height - const intersectionArea = - (intersection.right - intersection.left) * (intersection.bottom - intersection.top) - const coverage = targetArea > 0 ? intersectionArea / targetArea : 0 - if (coverage < 0.9) return 'light' - const averageLuminance = sampleCount > 0 ? luminanceTotal / sampleCount : 0 - return averageLuminance > 0.179 ? 'dark' : 'light' - } catch { - return 'light' - } -} - -function srgbToLinear(value: number) { - return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4 -} - function resetView() { setFit() centerViewport() @@ -360,10 +307,7 @@ function stopPan() { const pan = panning.value panning.value = undefined if (!pan || props.mode !== 'view') return - if (pan.moved) { - notifyImageReady() - return - } + if (pan.moved) return if (viewZoomed.value) resetView() else setViewZoom(CLICK_ZOOM) } @@ -427,7 +371,6 @@ onMounted(async () => { onBeforeUnmount(() => { initializationGeneration++ - if (imageReadyFrame !== undefined) cancelAnimationFrame(imageReadyFrame) document.removeEventListener('keydown', handleKeydown) document.removeEventListener('keyup', handleKeyup) document.removeEventListener('edit-menu:undo', handleEditMenuUndo) @@ -436,7 +379,7 @@ onBeforeUnmount(() => { void dispose() }) -defineExpose({ getTextContrast, markSaved }) +defineExpose({ markSaved })