mirror of
https://github.com/modrinth/code.git
synced 2026-08-31 12:05:53 +00:00
fix: gallery issues
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
+3
-3
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<HTMLCanvasElement>('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 })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -456,20 +399,31 @@ defineExpose({ getTextContrast, markSaved })
|
||||
@wheel="handleWheel"
|
||||
>
|
||||
<img
|
||||
v-if="mode === 'view' && loadingEditorData"
|
||||
v-if="mode === 'view' && loadingEditorData && !nativeImageView"
|
||||
:src="item.src"
|
||||
:alt="item.alt"
|
||||
class="pointer-events-none relative z-[2] m-auto block max-h-full max-w-full shrink-0 object-contain"
|
||||
class="pointer-events-none relative z-[2] m-auto block h-full w-full shrink-0 object-contain"
|
||||
draggable="false"
|
||||
/>
|
||||
<div
|
||||
v-show="!loadingEditorData"
|
||||
v-show="nativeImageView || !loadingEditorData"
|
||||
class="editor-canvas relative z-[2] m-auto shrink-0"
|
||||
:class="{
|
||||
'h-full w-full': nativeImageView && loadingEditorData,
|
||||
'is-native-image-view': nativeImageView,
|
||||
}"
|
||||
@pointerenter="updateBrushPointer"
|
||||
@pointermove="updateBrushPointer"
|
||||
@pointerleave="brushPointer.visible = false"
|
||||
>
|
||||
<canvas ref="canvasElement" :aria-label="item.alt" role="img" />
|
||||
<img
|
||||
v-if="nativeImageView"
|
||||
:src="item.src"
|
||||
:alt="item.alt"
|
||||
class="pointer-events-none absolute inset-0 z-[2] h-full w-full object-contain"
|
||||
draggable="false"
|
||||
/>
|
||||
<div
|
||||
v-if="hasBrushPointer && brushPointer.visible"
|
||||
class="pointer-events-none absolute z-10 -translate-x-1/2 -translate-y-1/2 rounded-full border border-solid shadow-[0_0_0_1px_rgb(0_0_0_/_80%),inset_0_0_0_1px_rgb(255_255_255_/_35%)]"
|
||||
@@ -582,6 +536,22 @@ defineExpose({ getTextContrast, markSaved })
|
||||
box-shadow: 0 1.5rem 4rem rgb(0 0 0 / 24%);
|
||||
}
|
||||
|
||||
.editor-canvas.is-native-image-view :deep(.canvas-container) {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.editor-viewport.is-view .editor-canvas.is-native-image-view {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
.editor-viewport.is-view-zoomed .editor-canvas.is-native-image-view {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.editor-viewport.is-panning .editor-canvas.is-native-image-view {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.editor-viewport.is-view :deep(.upper-canvas) {
|
||||
cursor: zoom-in !important;
|
||||
}
|
||||
|
||||
@@ -34,16 +34,11 @@ const emit = defineEmits<{
|
||||
|
||||
const activeId = ref<string | null>(null)
|
||||
const mode = ref<ImageViewerEditorMode>('view')
|
||||
const titleContrast = ref<'dark' | 'light'>('light')
|
||||
const descriptionContrast = ref<'dark' | 'light'>('light')
|
||||
const closeAfterEditing = ref(false)
|
||||
const editorComponent = ref<InstanceType<typeof Editor>>()
|
||||
const titleElement = ref<HTMLElement>()
|
||||
const descriptionElement = ref<HTMLElement>()
|
||||
const context = injectImageViewerEditor(null)
|
||||
const itemDataCache = new Map<string, Promise<ImageViewerEditorData>>()
|
||||
const itemImageCache = new Map<string, HTMLImageElement>()
|
||||
let headingContrastTimer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
const activeIndex = computed(() => props.items.findIndex((item) => item.id === activeId.value))
|
||||
const activeItem = computed(() => props.items[activeIndex.value] ?? null)
|
||||
@@ -129,11 +124,8 @@ function preloadItemsAround(index: number) {
|
||||
function show(index: number) {
|
||||
const item = props.items[index]
|
||||
if (!item) return
|
||||
cancelHeadingContrastUpdate()
|
||||
preloadItemsAround(index)
|
||||
if (activeId.value === null) {
|
||||
titleContrast.value = 'light'
|
||||
descriptionContrast.value = 'light'
|
||||
context?.onShow?.()
|
||||
}
|
||||
activeId.value = item.id
|
||||
@@ -165,7 +157,6 @@ function finishEditing() {
|
||||
|
||||
function hide() {
|
||||
if (activeId.value === null || props.saving) return
|
||||
cancelHeadingContrastUpdate()
|
||||
activeId.value = null
|
||||
mode.value = 'view'
|
||||
closeAfterEditing.value = false
|
||||
@@ -177,33 +168,12 @@ function hide() {
|
||||
|
||||
function navigate(offset: number, direction: 'next' | 'previous') {
|
||||
if (mode.value !== 'view' || props.items.length < 2) return
|
||||
cancelHeadingContrastUpdate()
|
||||
const index = (activeIndex.value + offset + props.items.length) % props.items.length
|
||||
preloadItemsAround(index)
|
||||
activeId.value = props.items[index].id
|
||||
emit('navigate', props.items[index], index, direction)
|
||||
}
|
||||
|
||||
function updateHeadingContrast() {
|
||||
cancelHeadingContrastUpdate()
|
||||
headingContrastTimer = setTimeout(() => {
|
||||
headingContrastTimer = undefined
|
||||
if (titleElement.value) {
|
||||
titleContrast.value = editorComponent.value?.getTextContrast(titleElement.value) ?? 'light'
|
||||
}
|
||||
if (descriptionElement.value) {
|
||||
descriptionContrast.value =
|
||||
editorComponent.value?.getTextContrast(descriptionElement.value) ?? 'light'
|
||||
}
|
||||
}, 120)
|
||||
}
|
||||
|
||||
function cancelHeadingContrastUpdate() {
|
||||
if (headingContrastTimer === undefined) return
|
||||
clearTimeout(headingContrastTimer)
|
||||
headingContrastTimer = undefined
|
||||
}
|
||||
|
||||
function next() {
|
||||
navigate(1, 'next')
|
||||
}
|
||||
@@ -237,7 +207,6 @@ function handleKeydown(event: KeyboardEvent) {
|
||||
onMounted(() => document.addEventListener('keydown', handleKeydown))
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('keydown', handleKeydown)
|
||||
cancelHeadingContrastUpdate()
|
||||
itemDataCache.clear()
|
||||
itemImageCache.clear()
|
||||
if (activeId.value !== null) context?.onHide?.()
|
||||
@@ -261,20 +230,16 @@ defineExpose({ show, edit, hide, next, previous, markSavedAndView })
|
||||
class="absolute inset-x-6 top-[calc(var(--top-bar-height,3rem)_+_1.5rem)] z-10 min-w-0"
|
||||
@click.stop
|
||||
>
|
||||
<div class="w-fit min-w-0 max-w-full">
|
||||
<div class="viewer-heading-text w-fit min-w-0 max-w-full">
|
||||
<h2
|
||||
v-if="activeItem.title"
|
||||
ref="titleElement"
|
||||
class="m-0 max-w-[min(42rem,70vw)] truncate text-base font-semibold leading-snug transition-colors duration-200 ease-out"
|
||||
:class="titleContrast === 'dark' ? 'text-gray-950' : 'text-white'"
|
||||
class="m-0 max-w-[min(42rem,70vw)] truncate text-base font-semibold leading-snug text-contrast"
|
||||
>
|
||||
{{ activeItem.title }}
|
||||
</h2>
|
||||
<p
|
||||
v-if="activeItem.description"
|
||||
ref="descriptionElement"
|
||||
class="mb-0 mt-1 max-w-[min(42rem,70vw)] truncate text-xs leading-snug opacity-70 transition-colors duration-200 ease-out"
|
||||
:class="descriptionContrast === 'dark' ? 'text-gray-950' : 'text-white'"
|
||||
class="mb-0 mt-1 max-w-[min(42rem,70vw)] truncate text-xs leading-snug text-white"
|
||||
>
|
||||
{{ activeItem.description }}
|
||||
</p>
|
||||
@@ -296,7 +261,6 @@ defineExpose({ show, edit, hide, next, previous, markSavedAndView })
|
||||
@previous="previous"
|
||||
@cancel="finishEditing"
|
||||
@save="emit('save', $event)"
|
||||
@image-ready="updateHeadingContrast"
|
||||
>
|
||||
<template #actions>
|
||||
<slot name="actions" :item="activeItem" :index="activeIndex" :hide="hide" />
|
||||
@@ -305,3 +269,9 @@ defineExpose({ show, edit, hide, next, previous, markSavedAndView })
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.viewer-heading-text {
|
||||
filter: drop-shadow(0 2px 3px rgb(0 0 0 / 90%));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1353,6 +1353,16 @@ export function useImageEditor() {
|
||||
applyDisplayScale()
|
||||
}
|
||||
|
||||
function waitForRender() {
|
||||
const editorCanvas = canvas.value
|
||||
if (!editorCanvas) return Promise.resolve()
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
editorCanvas.once('after:render', () => resolve())
|
||||
editorCanvas.requestRenderAll()
|
||||
})
|
||||
}
|
||||
|
||||
function applyDisplayScale() {
|
||||
const editorCanvas = canvas.value
|
||||
if (!editorCanvas) return
|
||||
@@ -1621,6 +1631,7 @@ export function useImageEditor() {
|
||||
fitToViewport,
|
||||
setZoom,
|
||||
setFit,
|
||||
waitForRender,
|
||||
exportPng,
|
||||
handleKeyboardShortcut,
|
||||
isTextEditing,
|
||||
|
||||
Reference in New Issue
Block a user