mirror of
https://github.com/modrinth/code.git
synced 2026-09-02 04:56:52 +00:00
fix: screenshots page lagging with lots of images (#7336)
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useDroppable } from '@dnd-kit/vue'
|
import { useDroppable } from '@dnd-kit/vue'
|
||||||
import { defineMessages, useVIntl } from '@modrinth/ui'
|
import { defineMessages, useVIntl } from '@modrinth/ui'
|
||||||
import { computed, ref } from 'vue'
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
|
|
||||||
import type { InstanceScreenshot } from '@/helpers/instance'
|
import type { InstanceScreenshot } from '@/helpers/instance'
|
||||||
|
|
||||||
@@ -12,6 +12,9 @@ const props = defineProps<{
|
|||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
screenshots: InstanceScreenshot[]
|
screenshots: InstanceScreenshot[]
|
||||||
|
renderedScreenshots?: InstanceScreenshot[]
|
||||||
|
virtualGridHeight?: number
|
||||||
|
virtualGridTop?: number
|
||||||
selectedKeys: ReadonlySet<string>
|
selectedKeys: ReadonlySet<string>
|
||||||
selectionActive: boolean
|
selectionActive: boolean
|
||||||
activeDraggedKeys: ReadonlySet<string>
|
activeDraggedKeys: ReadonlySet<string>
|
||||||
@@ -43,6 +46,53 @@ const messages = defineMessages({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const shouldShowGrid = computed(() =>
|
||||||
|
Boolean(props.hideHeader || props.forceOpen || !collapsed.value),
|
||||||
|
)
|
||||||
|
const visibleScreenshots = ref<InstanceScreenshot[]>(props.renderedScreenshots ?? props.screenshots)
|
||||||
|
const renderGrid = ref(shouldShowGrid.value)
|
||||||
|
const virtualGridStyle = computed(() =>
|
||||||
|
props.virtualGridHeight === undefined ? undefined : { height: `${props.virtualGridHeight}px` },
|
||||||
|
)
|
||||||
|
const visibleGridStyle = computed(() =>
|
||||||
|
props.virtualGridTop === undefined
|
||||||
|
? undefined
|
||||||
|
: { transform: `translateY(${props.virtualGridTop}px)` },
|
||||||
|
)
|
||||||
|
let unmountGridTimeout: ReturnType<typeof setTimeout> | undefined
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.renderedScreenshots ?? props.screenshots,
|
||||||
|
(screenshots) => {
|
||||||
|
if (shouldShowGrid.value) visibleScreenshots.value = screenshots
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
shouldShowGrid,
|
||||||
|
(showGrid, previouslyShown) => {
|
||||||
|
if (unmountGridTimeout) clearTimeout(unmountGridTimeout)
|
||||||
|
if (showGrid) {
|
||||||
|
visibleScreenshots.value = props.renderedScreenshots ?? props.screenshots
|
||||||
|
renderGrid.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!previouslyShown) {
|
||||||
|
renderGrid.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
unmountGridTimeout = setTimeout(() => {
|
||||||
|
renderGrid.value = false
|
||||||
|
unmountGridTimeout = undefined
|
||||||
|
}, 300)
|
||||||
|
},
|
||||||
|
{ flush: 'post' },
|
||||||
|
)
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (unmountGridTimeout) clearTimeout(unmountGridTimeout)
|
||||||
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'activate', screenshot: InstanceScreenshot, event: MouseEvent | KeyboardEvent): void
|
(e: 'activate', screenshot: InstanceScreenshot, event: MouseEvent | KeyboardEvent): void
|
||||||
(e: 'toggle-selection' | 'copy' | 'edit', screenshot: InstanceScreenshot): void
|
(e: 'toggle-selection' | 'copy' | 'edit', screenshot: InstanceScreenshot): void
|
||||||
@@ -98,44 +148,48 @@ function getSelectionKey(screenshot: InstanceScreenshot) {
|
|||||||
<template #actions="{ startEditing }">
|
<template #actions="{ startEditing }">
|
||||||
<slot name="actions" :start-editing="startEditing" />
|
<slot name="actions" :start-editing="startEditing" />
|
||||||
</template>
|
</template>
|
||||||
<TransitionGroup
|
<div v-if="renderGrid" class="relative min-h-[45px] w-full" :style="virtualGridStyle">
|
||||||
tag="div"
|
<TransitionGroup
|
||||||
class="grid min-h-[45px] w-full grid-cols-1 gap-3 sm:grid-cols-2 2xl:grid-cols-4"
|
tag="div"
|
||||||
move-class="transition-transform duration-200 ease-out motion-reduce:transition-none"
|
class="grid min-h-[45px] w-full grid-cols-1 gap-3 sm:grid-cols-2 2xl:grid-cols-4"
|
||||||
:enter-active-class="
|
:class="{ 'absolute inset-x-0 top-0': virtualGridHeight !== undefined }"
|
||||||
animateEntry
|
:style="visibleGridStyle"
|
||||||
? 'transition-[opacity,transform] duration-[150ms] ease-out motion-reduce:transition-none'
|
move-class="transition-transform duration-200 ease-out motion-reduce:transition-none"
|
||||||
: ''
|
:enter-active-class="
|
||||||
"
|
animateEntry
|
||||||
:enter-from-class="animateEntry ? 'opacity-0' : ''"
|
? 'transition-[opacity,transform] duration-[150ms] ease-out motion-reduce:transition-none'
|
||||||
enter-to-class="opacity-100 scale-100"
|
: ''
|
||||||
>
|
"
|
||||||
<ScreenshotCard
|
:enter-from-class="animateEntry ? 'opacity-0' : ''"
|
||||||
v-for="screenshot in screenshots"
|
enter-to-class="opacity-100 scale-100"
|
||||||
:key="getSelectionKey(screenshot)"
|
|
||||||
:screenshot="screenshot"
|
|
||||||
:selection-key="getSelectionKey(screenshot)"
|
|
||||||
:selected="selectedKeys.has(getSelectionKey(screenshot))"
|
|
||||||
:selection-active="selectionActive"
|
|
||||||
:active-dragged="activeDraggedKeys.has(getSelectionKey(screenshot))"
|
|
||||||
:can-drag="canDrag"
|
|
||||||
:show-instance-name="showInstanceName"
|
|
||||||
:highlighted="highlightedScreenshotId === screenshot.id"
|
|
||||||
:copied="copiedScreenshotIds.has(screenshot.id)"
|
|
||||||
@activate="(event) => emit('activate', screenshot, event)"
|
|
||||||
@toggle-selection="emit('toggle-selection', screenshot)"
|
|
||||||
@copy="emit('copy', screenshot)"
|
|
||||||
@edit="emit('edit', screenshot)"
|
|
||||||
@more="(event) => emit('more', screenshot, event)"
|
|
||||||
/>
|
|
||||||
<p
|
|
||||||
v-if="screenshots.length === 0"
|
|
||||||
key="empty-group"
|
|
||||||
class="col-span-full m-0 pl-0.5 pt-1 text-base font-base text-secondary opacity-80"
|
|
||||||
>
|
>
|
||||||
{{ formatMessage(messages.emptyGroup) }}
|
<ScreenshotCard
|
||||||
</p>
|
v-for="screenshot in visibleScreenshots"
|
||||||
</TransitionGroup>
|
:key="getSelectionKey(screenshot)"
|
||||||
|
:screenshot="screenshot"
|
||||||
|
:selection-key="getSelectionKey(screenshot)"
|
||||||
|
:selected="selectedKeys.has(getSelectionKey(screenshot))"
|
||||||
|
:selection-active="selectionActive"
|
||||||
|
:active-dragged="activeDraggedKeys.has(getSelectionKey(screenshot))"
|
||||||
|
:can-drag="canDrag"
|
||||||
|
:show-instance-name="showInstanceName"
|
||||||
|
:highlighted="highlightedScreenshotId === screenshot.id"
|
||||||
|
:copied="copiedScreenshotIds.has(screenshot.id)"
|
||||||
|
@activate="(event) => emit('activate', screenshot, event)"
|
||||||
|
@toggle-selection="emit('toggle-selection', screenshot)"
|
||||||
|
@copy="emit('copy', screenshot)"
|
||||||
|
@edit="emit('edit', screenshot)"
|
||||||
|
@more="(event) => emit('more', screenshot, event)"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
v-if="screenshots.length === 0"
|
||||||
|
key="empty-group"
|
||||||
|
class="col-span-full m-0 pl-0.5 pt-1 text-base font-base text-secondary opacity-80"
|
||||||
|
>
|
||||||
|
{{ formatMessage(messages.emptyGroup) }}
|
||||||
|
</p>
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
</ScreenshotSection>
|
</ScreenshotSection>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -36,12 +36,13 @@ import {
|
|||||||
ReadyTransition,
|
ReadyTransition,
|
||||||
useFormatDateTime,
|
useFormatDateTime,
|
||||||
useReadyState,
|
useReadyState,
|
||||||
|
useScrollViewport,
|
||||||
useVIntl,
|
useVIntl,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||||
import { save } from '@tauri-apps/plugin-dialog'
|
import { save } from '@tauri-apps/plugin-dialog'
|
||||||
import { readFile } from '@tauri-apps/plugin-fs'
|
import { readFile } from '@tauri-apps/plugin-fs'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useElementSize, useStorage, useWindowSize } from '@vueuse/core'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
@@ -107,6 +108,28 @@ type ScreenshotDropData = {
|
|||||||
customGroupId?: string | null
|
customGroupId?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ScreenshotGroupLayout = {
|
||||||
|
group: ScreenshotGroupData
|
||||||
|
top: number
|
||||||
|
height: number
|
||||||
|
isOpen: boolean
|
||||||
|
gridHeight: number
|
||||||
|
gridTop: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type VisibleScreenshotGroupLayout = ScreenshotGroupLayout & {
|
||||||
|
renderedScreenshots: InstanceScreenshot[]
|
||||||
|
virtualGridTop: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const SCREENSHOT_GRID_GAP = 12
|
||||||
|
const SCREENSHOT_GROUP_SPACING = 12
|
||||||
|
const SCREENSHOT_GROUP_HEADER_HEIGHT = 40
|
||||||
|
const SCREENSHOT_GROUP_CONTENT_SPACING = 10
|
||||||
|
const SCREENSHOT_GROUP_OVERSCAN = 900
|
||||||
|
const SCREENSHOT_GRID_MIN_HEIGHT = 45
|
||||||
|
const FALLBACK_SCREENSHOT_CARD_WIDTH = 320
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
instanceId?: string
|
instanceId?: string
|
||||||
@@ -151,6 +174,7 @@ const copiedScreenshotIds = ref(new Set<string>())
|
|||||||
const copiedResetTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
const copiedResetTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||||
const screenshotsPage = ref<HTMLElement>()
|
const screenshotsPage = ref<HTMLElement>()
|
||||||
const regrouping = ref(false)
|
const regrouping = ref(false)
|
||||||
|
const screenshotsScrolling = ref(false)
|
||||||
const screenshotToDelete = ref<InstanceScreenshot | null>(null)
|
const screenshotToDelete = ref<InstanceScreenshot | null>(null)
|
||||||
const deleteFromPreview = ref(false)
|
const deleteFromPreview = ref(false)
|
||||||
const activeDrag = ref<ActiveScreenshotDrag | null>(null)
|
const activeDrag = ref<ActiveScreenshotDrag | null>(null)
|
||||||
@@ -173,6 +197,25 @@ const route = useRoute()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const { addNotification, handleError } = injectNotificationManager()
|
const { addNotification, handleError } = injectNotificationManager()
|
||||||
|
let screenshotsScrollIdleTimeout: ReturnType<typeof setTimeout> | undefined
|
||||||
|
const {
|
||||||
|
listContainer: screenshotListContainer,
|
||||||
|
containerOffset: screenshotListOffset,
|
||||||
|
relativeScrollTop: screenshotListScrollTop,
|
||||||
|
scrollContainer: screenshotScrollContainer,
|
||||||
|
viewportHeight: screenshotViewportHeight,
|
||||||
|
} = useScrollViewport({
|
||||||
|
onScroll: () => {
|
||||||
|
screenshotsScrolling.value = true
|
||||||
|
if (screenshotsScrollIdleTimeout) clearTimeout(screenshotsScrollIdleTimeout)
|
||||||
|
screenshotsScrollIdleTimeout = setTimeout(() => {
|
||||||
|
screenshotsScrolling.value = false
|
||||||
|
screenshotsScrollIdleTimeout = undefined
|
||||||
|
}, 120)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { width: screenshotListWidth } = useElementSize(screenshotListContainer)
|
||||||
|
const { width: windowWidth } = useWindowSize()
|
||||||
const formatDateTime = useFormatDateTime({ dateStyle: 'long', timeStyle: 'short' })
|
const formatDateTime = useFormatDateTime({ dateStyle: 'long', timeStyle: 'short' })
|
||||||
const formatMonth = useFormatDateTime({ month: 'long', year: 'numeric' })
|
const formatMonth = useFormatDateTime({ month: 'long', year: 'numeric' })
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
@@ -469,6 +512,93 @@ const groupedScreenshots = computed((): ScreenshotGroupData[] => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const screenshotColumnCount = computed(() => {
|
||||||
|
if (windowWidth.value >= 1536) return 4
|
||||||
|
if (windowWidth.value >= 640) return 2
|
||||||
|
return 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const screenshotCardWidth = computed(() => {
|
||||||
|
if (screenshotListWidth.value <= 0) return FALLBACK_SCREENSHOT_CARD_WIDTH
|
||||||
|
const gapsWidth = (screenshotColumnCount.value - 1) * SCREENSHOT_GRID_GAP
|
||||||
|
return Math.max(0, (screenshotListWidth.value - gapsWidth) / screenshotColumnCount.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const screenshotCardHeight = computed(() => (screenshotCardWidth.value * 9) / 16)
|
||||||
|
const screenshotRowHeight = computed(() => screenshotCardHeight.value + SCREENSHOT_GRID_GAP)
|
||||||
|
|
||||||
|
const screenshotGroupLayouts = computed<ScreenshotGroupLayout[]>(() => {
|
||||||
|
const layouts: ScreenshotGroupLayout[] = []
|
||||||
|
let top = 0
|
||||||
|
|
||||||
|
for (const group of groupedScreenshots.value) {
|
||||||
|
const isHeaderHidden = groupBy.value === 'none'
|
||||||
|
const isOpen = isHeaderHidden || search.value.length > 0 || !collapsedGroups.value[group.id]
|
||||||
|
const rowCount = Math.ceil(group.screenshots.length / screenshotColumnCount.value)
|
||||||
|
const gridHeight =
|
||||||
|
rowCount === 0
|
||||||
|
? SCREENSHOT_GRID_MIN_HEIGHT
|
||||||
|
: rowCount * screenshotCardHeight.value + Math.max(0, rowCount - 1) * SCREENSHOT_GRID_GAP
|
||||||
|
const headerHeight = isHeaderHidden ? 0 : SCREENSHOT_GROUP_HEADER_HEIGHT
|
||||||
|
const gridTop = top + headerHeight + SCREENSHOT_GROUP_CONTENT_SPACING
|
||||||
|
const height =
|
||||||
|
headerHeight +
|
||||||
|
(isOpen ? SCREENSHOT_GROUP_CONTENT_SPACING + gridHeight : 0) +
|
||||||
|
SCREENSHOT_GROUP_SPACING
|
||||||
|
|
||||||
|
layouts.push({ group, top, height, isOpen, gridHeight, gridTop })
|
||||||
|
top += height
|
||||||
|
}
|
||||||
|
|
||||||
|
return layouts
|
||||||
|
})
|
||||||
|
|
||||||
|
const screenshotListHeight = computed(() => {
|
||||||
|
const lastGroup = screenshotGroupLayouts.value[screenshotGroupLayouts.value.length - 1]
|
||||||
|
return lastGroup ? lastGroup.top + lastGroup.height : 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const visibleScreenshotGroups = computed<VisibleScreenshotGroupLayout[]>(() => {
|
||||||
|
const hasViewport = Boolean(screenshotListContainer.value && screenshotScrollContainer.value)
|
||||||
|
const viewportStart = hasViewport
|
||||||
|
? Math.max(0, screenshotListScrollTop.value - SCREENSHOT_GROUP_OVERSCAN)
|
||||||
|
: 0
|
||||||
|
const viewportEnd = hasViewport
|
||||||
|
? screenshotListScrollTop.value + screenshotViewportHeight.value + SCREENSHOT_GROUP_OVERSCAN
|
||||||
|
: SCREENSHOT_GROUP_OVERSCAN
|
||||||
|
|
||||||
|
return screenshotGroupLayouts.value
|
||||||
|
.filter((layout) => layout.top + layout.height >= viewportStart && layout.top <= viewportEnd)
|
||||||
|
.map((layout) => {
|
||||||
|
if (!layout.isOpen || layout.group.screenshots.length === 0) {
|
||||||
|
return { ...layout, renderedScreenshots: [], virtualGridTop: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowCount = Math.ceil(layout.group.screenshots.length / screenshotColumnCount.value)
|
||||||
|
const firstRow = Math.min(
|
||||||
|
rowCount,
|
||||||
|
Math.max(0, Math.floor((viewportStart - layout.gridTop) / screenshotRowHeight.value)),
|
||||||
|
)
|
||||||
|
const lastRow = Math.min(
|
||||||
|
rowCount,
|
||||||
|
Math.max(
|
||||||
|
firstRow,
|
||||||
|
Math.ceil(
|
||||||
|
(viewportEnd - layout.gridTop + SCREENSHOT_GRID_GAP) / screenshotRowHeight.value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
const firstScreenshot = firstRow * screenshotColumnCount.value
|
||||||
|
const lastScreenshot = lastRow * screenshotColumnCount.value
|
||||||
|
|
||||||
|
return {
|
||||||
|
...layout,
|
||||||
|
renderedScreenshots: layout.group.screenshots.slice(firstScreenshot, lastScreenshot),
|
||||||
|
virtualGridTop: firstRow * screenshotRowHeight.value,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const previewItems = computed(() =>
|
const previewItems = computed(() =>
|
||||||
filteredScreenshots.value.map((screenshot) => ({
|
filteredScreenshots.value.map((screenshot) => ({
|
||||||
id: getSelectionKey(screenshot),
|
id: getSelectionKey(screenshot),
|
||||||
@@ -959,10 +1089,27 @@ async function revealScreenshot(id: string) {
|
|||||||
if (group) setGroupCollapsed(group.id, false)
|
if (group) setGroupCollapsed(group.id, false)
|
||||||
|
|
||||||
await nextTick()
|
await nextTick()
|
||||||
const card = document.querySelector<HTMLElement>(`[data-screenshot-id="${CSS.escape(id)}"]`)
|
await waitForScreenshotViewport()
|
||||||
card?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
||||||
card?.focus()
|
|
||||||
highlightedScreenshotId.value = id
|
highlightedScreenshotId.value = id
|
||||||
|
|
||||||
|
const layout = screenshotGroupLayouts.value.find((candidate) => candidate.group.id === group?.id)
|
||||||
|
const screenshotIndex = layout?.group.screenshots.findIndex((screenshot) => screenshot.id === id)
|
||||||
|
const scrollTarget = screenshotScrollContainer.value
|
||||||
|
if (layout && screenshotIndex !== undefined && screenshotIndex >= 0 && scrollTarget) {
|
||||||
|
const row = Math.floor(screenshotIndex / screenshotColumnCount.value)
|
||||||
|
const top = Math.max(
|
||||||
|
0,
|
||||||
|
screenshotListOffset.value +
|
||||||
|
layout.gridTop +
|
||||||
|
row * screenshotRowHeight.value +
|
||||||
|
screenshotCardHeight.value / 2 -
|
||||||
|
screenshotViewportHeight.value / 2,
|
||||||
|
)
|
||||||
|
scrollTarget.scrollTo({ top, behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const card = await waitForScreenshotCard(id)
|
||||||
|
card?.focus({ preventScroll: true })
|
||||||
revealTimeout = setTimeout(() => {
|
revealTimeout = setTimeout(() => {
|
||||||
if (highlightedScreenshotId.value === id) highlightedScreenshotId.value = undefined
|
if (highlightedScreenshotId.value === id) highlightedScreenshotId.value = undefined
|
||||||
if (revealedScreenshotId.value === id) revealedScreenshotId.value = undefined
|
if (revealedScreenshotId.value === id) revealedScreenshotId.value = undefined
|
||||||
@@ -970,6 +1117,23 @@ async function revealScreenshot(id: string) {
|
|||||||
}, 2400)
|
}, 2400)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function waitForScreenshotViewport() {
|
||||||
|
for (let frame = 0; frame < 10; frame++) {
|
||||||
|
if (screenshotListContainer.value && screenshotScrollContainer.value) return
|
||||||
|
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForScreenshotCard(id: string) {
|
||||||
|
const selector = `[data-screenshot-id="${CSS.escape(id)}"]`
|
||||||
|
for (let frame = 0; frame < 60; frame++) {
|
||||||
|
const card = screenshotListContainer.value?.querySelector<HTMLElement>(selector)
|
||||||
|
if (card) return card
|
||||||
|
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
async function confirmDelete() {
|
async function confirmDelete() {
|
||||||
const screenshot = screenshotToDelete.value
|
const screenshot = screenshotToDelete.value
|
||||||
if (!screenshot) return
|
if (!screenshot) return
|
||||||
@@ -1171,6 +1335,7 @@ watch(activeDropGroupId, (groupId) => {
|
|||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
clearGroupHoverOpenTimeout()
|
clearGroupHoverOpenTimeout()
|
||||||
if (revealTimeout) clearTimeout(revealTimeout)
|
if (revealTimeout) clearTimeout(revealTimeout)
|
||||||
|
if (screenshotsScrollIdleTimeout) clearTimeout(screenshotsScrollIdleTimeout)
|
||||||
for (const timeout of copiedResetTimeouts.values()) clearTimeout(timeout)
|
for (const timeout of copiedResetTimeouts.values()) clearTimeout(timeout)
|
||||||
copiedResetTimeouts.clear()
|
copiedResetTimeouts.clear()
|
||||||
})
|
})
|
||||||
@@ -1312,65 +1477,85 @@ onBeforeUnmount(() => {
|
|||||||
@drag-over="handleDragOver"
|
@drag-over="handleDragOver"
|
||||||
@drag-end="handleDragEnd"
|
@drag-end="handleDragEnd"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col">
|
<div
|
||||||
<ScreenshotGroupSection
|
ref="screenshotListContainer"
|
||||||
v-for="group in groupedScreenshots"
|
class="relative w-full"
|
||||||
:id="group.id"
|
:style="{ height: `${screenshotListHeight}px`, overflowAnchor: 'none' }"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="{
|
||||||
|
group,
|
||||||
|
top,
|
||||||
|
gridHeight,
|
||||||
|
renderedScreenshots,
|
||||||
|
virtualGridTop,
|
||||||
|
} in visibleScreenshotGroups"
|
||||||
:key="group.id"
|
:key="group.id"
|
||||||
:title="group.title"
|
class="absolute inset-x-0 transition-transform duration-300 ease-in-out will-change-transform motion-reduce:transition-none"
|
||||||
:screenshots="group.screenshots"
|
:style="{ transform: `translateY(${top}px)` }"
|
||||||
:selected-keys="selectedKeys"
|
|
||||||
:selection-active="selectionActive"
|
|
||||||
:active-dragged-keys="activeDraggedKeys"
|
|
||||||
:show-drop-outline="activeDropGroupId === group.id && canDropScreenshotsOnGroup(group)"
|
|
||||||
:can-drag="groupBy === 'custom' || (isGlobal && groupBy === 'instance')"
|
|
||||||
:drop-instance-id="groupBy === 'instance' ? group.dropInstanceId : undefined"
|
|
||||||
:drop-custom-group="groupBy === 'custom'"
|
|
||||||
:drop-custom-group-id="group.customGroupId ?? undefined"
|
|
||||||
:show-instance-name="isGlobal && groupBy !== 'instance'"
|
|
||||||
:highlighted-screenshot-id="highlightedScreenshotId"
|
|
||||||
:copied-screenshot-ids="copiedScreenshotIds"
|
|
||||||
:animate-entry="!regrouping"
|
|
||||||
:force-open="search.length > 0"
|
|
||||||
:hide-header="groupBy === 'none'"
|
|
||||||
:editable-title="Boolean(group.customGroupId)"
|
|
||||||
:start-editing-title="groupIdPendingNameEdit === group.customGroupId"
|
|
||||||
:max-title-length="MAX_INSTANCE_GROUP_NAME_LENGTH"
|
|
||||||
:validate-title="validateCustomGroupName"
|
|
||||||
:on-title-change="(name: string) => renameCustomGroup(group.customGroupId, name)"
|
|
||||||
:collapsed="Boolean(collapsedGroups[group.id])"
|
|
||||||
@update:collapsed="(value) => setGroupCollapsed(group.id, value)"
|
|
||||||
@activate="activateScreenshot"
|
|
||||||
@toggle-selection="toggleScreenshotSelection"
|
|
||||||
@copy="copyScreenshot"
|
|
||||||
@edit="editScreenshot"
|
|
||||||
@more="showScreenshotOptions"
|
|
||||||
>
|
>
|
||||||
<template v-if="group.customGroupId" #actions="{ startEditing }">
|
<ScreenshotGroupSection
|
||||||
<div
|
:id="group.id"
|
||||||
class="flex shrink-0 items-center opacity-0 transition-opacity duration-250 group-hover/header:opacity-100 focus-within:opacity-100"
|
:title="group.title"
|
||||||
>
|
:screenshots="group.screenshots"
|
||||||
<IconButton
|
:rendered-screenshots="renderedScreenshots"
|
||||||
v-tooltip="formatMessage(messages.editGroup)"
|
:virtual-grid-height="gridHeight"
|
||||||
:label="formatMessage(messages.editGroup)"
|
:virtual-grid-top="virtualGridTop"
|
||||||
type="quiet"
|
:selected-keys="selectedKeys"
|
||||||
size="sm"
|
:selection-active="selectionActive"
|
||||||
@click.stop="startEditing"
|
:active-dragged-keys="activeDraggedKeys"
|
||||||
|
:show-drop-outline="
|
||||||
|
activeDropGroupId === group.id && canDropScreenshotsOnGroup(group)
|
||||||
|
"
|
||||||
|
:can-drag="groupBy === 'custom' || (isGlobal && groupBy === 'instance')"
|
||||||
|
:drop-instance-id="groupBy === 'instance' ? group.dropInstanceId : undefined"
|
||||||
|
:drop-custom-group="groupBy === 'custom'"
|
||||||
|
:drop-custom-group-id="group.customGroupId ?? undefined"
|
||||||
|
:show-instance-name="isGlobal && groupBy !== 'instance'"
|
||||||
|
:highlighted-screenshot-id="highlightedScreenshotId"
|
||||||
|
:copied-screenshot-ids="copiedScreenshotIds"
|
||||||
|
:animate-entry="!regrouping && !screenshotsScrolling"
|
||||||
|
:force-open="search.length > 0"
|
||||||
|
:hide-header="groupBy === 'none'"
|
||||||
|
:editable-title="Boolean(group.customGroupId)"
|
||||||
|
:start-editing-title="groupIdPendingNameEdit === group.customGroupId"
|
||||||
|
:max-title-length="MAX_INSTANCE_GROUP_NAME_LENGTH"
|
||||||
|
:validate-title="validateCustomGroupName"
|
||||||
|
:on-title-change="(name: string) => renameCustomGroup(group.customGroupId, name)"
|
||||||
|
:collapsed="Boolean(collapsedGroups[group.id])"
|
||||||
|
@update:collapsed="(value) => setGroupCollapsed(group.id, value)"
|
||||||
|
@activate="activateScreenshot"
|
||||||
|
@toggle-selection="toggleScreenshotSelection"
|
||||||
|
@copy="copyScreenshot"
|
||||||
|
@edit="editScreenshot"
|
||||||
|
@more="showScreenshotOptions"
|
||||||
|
>
|
||||||
|
<template v-if="group.customGroupId" #actions="{ startEditing }">
|
||||||
|
<div
|
||||||
|
class="flex shrink-0 items-center opacity-0 transition-opacity duration-250 group-hover/header:opacity-100 focus-within:opacity-100"
|
||||||
>
|
>
|
||||||
<EditIcon />
|
<IconButton
|
||||||
</IconButton>
|
v-tooltip="formatMessage(messages.editGroup)"
|
||||||
<IconButton
|
:label="formatMessage(messages.editGroup)"
|
||||||
v-tooltip="formatMessage(messages.deleteGroup)"
|
type="quiet"
|
||||||
:label="formatMessage(messages.deleteGroup)"
|
size="sm"
|
||||||
type="quiet"
|
@click.stop="startEditing"
|
||||||
size="sm"
|
>
|
||||||
@click.stop="requestCustomGroupDeletion(group.customGroupId)"
|
<EditIcon />
|
||||||
>
|
</IconButton>
|
||||||
<TrashIcon />
|
<IconButton
|
||||||
</IconButton>
|
v-tooltip="formatMessage(messages.deleteGroup)"
|
||||||
</div>
|
:label="formatMessage(messages.deleteGroup)"
|
||||||
</template>
|
type="quiet"
|
||||||
</ScreenshotGroupSection>
|
size="sm"
|
||||||
|
@click.stop="requestCustomGroupDeletion(group.customGroupId)"
|
||||||
|
>
|
||||||
|
<TrashIcon />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ScreenshotGroupSection>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<div class="pointer-events-none fixed inset-0 z-[9999]">
|
<div class="pointer-events-none fixed inset-0 z-[9999]">
|
||||||
|
|||||||
Reference in New Issue
Block a user