mirror of
https://github.com/modrinth/code.git
synced 2026-08-30 19:46:33 +00:00
fix: screenshots page lagging with lots of images (#7336)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useDroppable } from '@dnd-kit/vue'
|
||||
import { defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
|
||||
import type { InstanceScreenshot } from '@/helpers/instance'
|
||||
|
||||
@@ -12,6 +12,9 @@ const props = defineProps<{
|
||||
id: string
|
||||
title: string
|
||||
screenshots: InstanceScreenshot[]
|
||||
renderedScreenshots?: InstanceScreenshot[]
|
||||
virtualGridHeight?: number
|
||||
virtualGridTop?: number
|
||||
selectedKeys: ReadonlySet<string>
|
||||
selectionActive: boolean
|
||||
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<{
|
||||
(e: 'activate', screenshot: InstanceScreenshot, event: MouseEvent | KeyboardEvent): void
|
||||
(e: 'toggle-selection' | 'copy' | 'edit', screenshot: InstanceScreenshot): void
|
||||
@@ -98,44 +148,48 @@ function getSelectionKey(screenshot: InstanceScreenshot) {
|
||||
<template #actions="{ startEditing }">
|
||||
<slot name="actions" :start-editing="startEditing" />
|
||||
</template>
|
||||
<TransitionGroup
|
||||
tag="div"
|
||||
class="grid min-h-[45px] w-full grid-cols-1 gap-3 sm:grid-cols-2 2xl:grid-cols-4"
|
||||
move-class="transition-transform duration-200 ease-out motion-reduce:transition-none"
|
||||
:enter-active-class="
|
||||
animateEntry
|
||||
? 'transition-[opacity,transform] duration-[150ms] ease-out motion-reduce:transition-none'
|
||||
: ''
|
||||
"
|
||||
:enter-from-class="animateEntry ? 'opacity-0' : ''"
|
||||
enter-to-class="opacity-100 scale-100"
|
||||
>
|
||||
<ScreenshotCard
|
||||
v-for="screenshot in screenshots"
|
||||
: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"
|
||||
<div v-if="renderGrid" class="relative min-h-[45px] w-full" :style="virtualGridStyle">
|
||||
<TransitionGroup
|
||||
tag="div"
|
||||
class="grid min-h-[45px] w-full grid-cols-1 gap-3 sm:grid-cols-2 2xl:grid-cols-4"
|
||||
:class="{ 'absolute inset-x-0 top-0': virtualGridHeight !== undefined }"
|
||||
:style="visibleGridStyle"
|
||||
move-class="transition-transform duration-200 ease-out motion-reduce:transition-none"
|
||||
:enter-active-class="
|
||||
animateEntry
|
||||
? 'transition-[opacity,transform] duration-[150ms] ease-out motion-reduce:transition-none'
|
||||
: ''
|
||||
"
|
||||
:enter-from-class="animateEntry ? 'opacity-0' : ''"
|
||||
enter-to-class="opacity-100 scale-100"
|
||||
>
|
||||
{{ formatMessage(messages.emptyGroup) }}
|
||||
</p>
|
||||
</TransitionGroup>
|
||||
<ScreenshotCard
|
||||
v-for="screenshot in visibleScreenshots"
|
||||
: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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -36,12 +36,13 @@ import {
|
||||
ReadyTransition,
|
||||
useFormatDateTime,
|
||||
useReadyState,
|
||||
useScrollViewport,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { save } from '@tauri-apps/plugin-dialog'
|
||||
import { readFile } from '@tauri-apps/plugin-fs'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { useElementSize, useStorage, useWindowSize } from '@vueuse/core'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
@@ -107,6 +108,28 @@ type ScreenshotDropData = {
|
||||
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(
|
||||
defineProps<{
|
||||
instanceId?: string
|
||||
@@ -151,6 +174,7 @@ const copiedScreenshotIds = ref(new Set<string>())
|
||||
const copiedResetTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
const screenshotsPage = ref<HTMLElement>()
|
||||
const regrouping = ref(false)
|
||||
const screenshotsScrolling = ref(false)
|
||||
const screenshotToDelete = ref<InstanceScreenshot | null>(null)
|
||||
const deleteFromPreview = ref(false)
|
||||
const activeDrag = ref<ActiveScreenshotDrag | null>(null)
|
||||
@@ -173,6 +197,25 @@ const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { formatMessage } = useVIntl()
|
||||
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 formatMonth = useFormatDateTime({ month: 'long', year: 'numeric' })
|
||||
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(() =>
|
||||
filteredScreenshots.value.map((screenshot) => ({
|
||||
id: getSelectionKey(screenshot),
|
||||
@@ -959,10 +1089,27 @@ async function revealScreenshot(id: string) {
|
||||
if (group) setGroupCollapsed(group.id, false)
|
||||
|
||||
await nextTick()
|
||||
const card = document.querySelector<HTMLElement>(`[data-screenshot-id="${CSS.escape(id)}"]`)
|
||||
card?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||
card?.focus()
|
||||
await waitForScreenshotViewport()
|
||||
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(() => {
|
||||
if (highlightedScreenshotId.value === id) highlightedScreenshotId.value = undefined
|
||||
if (revealedScreenshotId.value === id) revealedScreenshotId.value = undefined
|
||||
@@ -970,6 +1117,23 @@ async function revealScreenshot(id: string) {
|
||||
}, 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() {
|
||||
const screenshot = screenshotToDelete.value
|
||||
if (!screenshot) return
|
||||
@@ -1171,6 +1335,7 @@ watch(activeDropGroupId, (groupId) => {
|
||||
onBeforeUnmount(() => {
|
||||
clearGroupHoverOpenTimeout()
|
||||
if (revealTimeout) clearTimeout(revealTimeout)
|
||||
if (screenshotsScrollIdleTimeout) clearTimeout(screenshotsScrollIdleTimeout)
|
||||
for (const timeout of copiedResetTimeouts.values()) clearTimeout(timeout)
|
||||
copiedResetTimeouts.clear()
|
||||
})
|
||||
@@ -1312,65 +1477,85 @@ onBeforeUnmount(() => {
|
||||
@drag-over="handleDragOver"
|
||||
@drag-end="handleDragEnd"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<ScreenshotGroupSection
|
||||
v-for="group in groupedScreenshots"
|
||||
:id="group.id"
|
||||
<div
|
||||
ref="screenshotListContainer"
|
||||
class="relative w-full"
|
||||
:style="{ height: `${screenshotListHeight}px`, overflowAnchor: 'none' }"
|
||||
>
|
||||
<div
|
||||
v-for="{
|
||||
group,
|
||||
top,
|
||||
gridHeight,
|
||||
renderedScreenshots,
|
||||
virtualGridTop,
|
||||
} in visibleScreenshotGroups"
|
||||
:key="group.id"
|
||||
:title="group.title"
|
||||
:screenshots="group.screenshots"
|
||||
: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"
|
||||
class="absolute inset-x-0 transition-transform duration-300 ease-in-out will-change-transform motion-reduce:transition-none"
|
||||
:style="{ transform: `translateY(${top}px)` }"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<IconButton
|
||||
v-tooltip="formatMessage(messages.editGroup)"
|
||||
:label="formatMessage(messages.editGroup)"
|
||||
type="quiet"
|
||||
size="sm"
|
||||
@click.stop="startEditing"
|
||||
<ScreenshotGroupSection
|
||||
:id="group.id"
|
||||
:title="group.title"
|
||||
:screenshots="group.screenshots"
|
||||
:rendered-screenshots="renderedScreenshots"
|
||||
:virtual-grid-height="gridHeight"
|
||||
:virtual-grid-top="virtualGridTop"
|
||||
: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 && !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.deleteGroup)"
|
||||
:label="formatMessage(messages.deleteGroup)"
|
||||
type="quiet"
|
||||
size="sm"
|
||||
@click.stop="requestCustomGroupDeletion(group.customGroupId)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
</template>
|
||||
</ScreenshotGroupSection>
|
||||
<IconButton
|
||||
v-tooltip="formatMessage(messages.editGroup)"
|
||||
:label="formatMessage(messages.editGroup)"
|
||||
type="quiet"
|
||||
size="sm"
|
||||
@click.stop="startEditing"
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
v-tooltip="formatMessage(messages.deleteGroup)"
|
||||
:label="formatMessage(messages.deleteGroup)"
|
||||
type="quiet"
|
||||
size="sm"
|
||||
@click.stop="requestCustomGroupDeletion(group.customGroupId)"
|
||||
>
|
||||
<TrashIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
</template>
|
||||
</ScreenshotGroupSection>
|
||||
</div>
|
||||
</div>
|
||||
<Teleport to="body">
|
||||
<div class="pointer-events-none fixed inset-0 z-[9999]">
|
||||
|
||||
Reference in New Issue
Block a user