- use existing loading overlay, move to common
- change deletion to use single shared flow
This commit is contained in:
pythongosssss
2026-01-12 11:22:05 +00:00
parent 6a4874bf90
commit 1a91cb3634
6 changed files with 80 additions and 143 deletions

View File

@@ -5,8 +5,13 @@
class="absolute inset-0 z-50 flex items-center justify-center bg-backdrop/50"
>
<div class="flex flex-col items-center">
<div class="spinner" />
<div class="mt-4 text-lg text-base-foreground">
<div class="relative flex items-center justify-center">
<div class="animate-spin spinner" />
<div class="absolute">
<slot />
</div>
</div>
<div v-if="loadingMessage" class="mt-4 text-lg text-base-foreground">
{{ loadingMessage }}
</div>
</div>
@@ -17,7 +22,7 @@
<script setup lang="ts">
defineProps<{
loading: boolean
loadingMessage: string
loadingMessage?: string
}>()
</script>
@@ -25,18 +30,8 @@ defineProps<{
.spinner {
width: 50px;
height: 50px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border: 4px solid var(--muted-foreground);
border-top-color: var(--base-foreground);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@@ -31,7 +31,7 @@
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue'
import LoadingOverlay from '@/components/load3d/LoadingOverlay.vue'
import LoadingOverlay from '@/components/common/LoadingOverlay.vue'
import { useLoad3dDrag } from '@/composables/useLoad3dDrag'
const props = defineProps<{

View File

@@ -120,11 +120,9 @@
@click="handleAssetSelect(item)"
@zoom="handleZoomClick(item)"
@output-count-click="enterFolderView(item)"
@asset-deleted="refreshAssets"
@asset-deleting="handleAssetDeleting(item.id, $event)"
@context-menu-opened="openContextMenuId = item.id"
@bulk-download="handleBulkDownload"
@bulk-delete="handleBulkDelete"
@delete-assets="handleDeleteAssets"
/>
</template>
</VirtualGrid>
@@ -544,26 +542,15 @@ const setAssetsDeletingState = (assetIds: string[], isDeleting: boolean) => {
)
}
const handleDeleteSelected = async () => {
const selectedAssets = getSelectedAssets(displayAssets.value)
const assetIds = selectedAssets.map((a) => a.id)
await deleteMultipleAssets(selectedAssets, (isDeleting) =>
setAssetsDeletingState(assetIds, isDeleting)
)
clearSelection()
}
const handleBulkDownload = (assets: AssetItem[]) => {
downloadMultipleAssets(assets)
clearSelection()
}
const handleAssetDeleting = (assetId: string, isDeleting: boolean) => {
setAssetsDeletingState([assetId], isDeleting)
}
const handleDeleteSelected = () =>
handleDeleteAssets(getSelectedAssets(displayAssets.value))
const handleBulkDelete = async (assets: AssetItem[]) => {
const handleDeleteAssets = async (assets: AssetItem[]) => {
const assetIds = assets.map((a) => a.id)
await deleteMultipleAssets(assets, (isDeleting) =>

View File

@@ -46,17 +46,9 @@
@image-loaded="handleImageLoaded"
/>
<div v-if="isDeleting" class="absolute inset-0 bg-black/50 z-10">
<div class="flex items-center justify-center h-full relative">
<div
class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-white absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
></div>
<i
class="icon-[lucide--trash-2] size-6 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
>
</i>
</div>
</div>
<LoadingOverlay :loading="isDeleting">
<i class="icon-[lucide--trash-2] size-5" />
</LoadingOverlay>
<!-- Action buttons overlay (top-left) -->
<div
@@ -142,10 +134,8 @@
:selected-assets="selectedAssets"
:is-bulk-mode="hasSelection && (selectedAssets?.length ?? 0) > 1"
@zoom="handleZoomClick"
@asset-deleted="emit('asset-deleted')"
@asset-deleting="emit('asset-deleting', $event)"
@bulk-download="emit('bulk-download', $event)"
@bulk-delete="emit('bulk-delete', $event)"
@delete-assets="emit('delete-assets', $event)"
/>
</template>
@@ -154,6 +144,7 @@ import { useElementHover, whenever } from '@vueuse/core'
import { computed, defineAsyncComponent, provide, ref, toRef } from 'vue'
import IconGroup from '@/components/button/IconGroup.vue'
import LoadingOverlay from '@/components/common/LoadingOverlay.vue'
import Button from '@/components/ui/button/Button.vue'
import {
formatDuration,
@@ -212,11 +203,9 @@ const emit = defineEmits<{
click: []
zoom: [asset: AssetItem]
'output-count-click': []
'asset-deleted': []
'asset-deleting': [isDeleting: boolean]
'context-menu-opened': []
'bulk-download': [assets: AssetItem[]]
'bulk-delete': [assets: AssetItem[]]
'delete-assets': [assets: AssetItem[]]
}>()
const cardContainerRef = ref<HTMLElement>()

View File

@@ -62,10 +62,8 @@ const {
const emit = defineEmits<{
zoom: []
'asset-deleted': []
'asset-deleting': [boolean]
'bulk-download': [assets: AssetItem[]]
'bulk-delete': [assets: AssetItem[]]
'delete-assets': [assets: AssetItem[]]
}>()
const contextMenu = ref<InstanceType<typeof ContextMenu>>()
@@ -153,7 +151,7 @@ const contextMenuItems = computed<MenuItem[]>(() => {
items.push({
label: t('mediaAsset.selection.deleteSelectedAll'),
icon: 'icon-[lucide--trash-2]',
command: () => emit('bulk-delete', selectedAssets)
command: () => emit('delete-assets', selectedAssets)
})
}
@@ -220,16 +218,7 @@ const contextMenuItems = computed<MenuItem[]>(() => {
items.push({
label: t('mediaAsset.actions.delete'),
icon: 'icon-[lucide--trash-2]',
command: async () => {
if (asset) {
const success = await actions.confirmDelete(asset, (deleting) => {
emit('asset-deleting', deleting)
})
if (success) {
emit('asset-deleted')
}
}
}
command: () => emit('delete-assets', [asset])
})
}

View File

@@ -137,74 +137,27 @@ export function useMediaAssetActions() {
/**
* Show confirmation dialog and delete asset if confirmed
* @param asset The asset to delete
* @param onDeleting Optional callback called with true when deletion starts and false when complete
* @returns true if the asset was deleted, false otherwise
*/
const confirmDelete = async (
asset: AssetItem,
onDeleting?: (deleting: boolean) => void
): Promise<boolean> => {
const assetType = getAssetType(asset)
return new Promise((resolve) => {
dialogStore.showDialog({
key: 'delete-asset-confirmation',
title: t('mediaAsset.deleteAssetTitle'),
component: ConfirmationDialogContent,
props: {
message: t('mediaAsset.deleteAssetDescription'),
type: 'delete',
itemList: [asset.name],
onConfirm: async () => {
onDeleting?.(true)
try {
const success = await deleteAsset(asset, assetType)
resolve(success)
} finally {
onDeleting?.(false)
}
},
onCancel: () => {
resolve(false)
}
}
})
})
return deleteMultipleAssets([asset], onDeleting)
}
const deleteAsset = async (asset: AssetItem, assetType: string) => {
const assetsStore = useAssetsStore()
try {
// Perform the deletion
await deleteAssetApi(asset, assetType)
// Update the appropriate store based on asset type
if (assetType === 'output') {
await assetsStore.updateHistory()
} else {
await assetsStore.updateInputs()
}
toast.add({
severity: 'success',
summary: t('g.success'),
detail: t('mediaAsset.assetDeletedSuccessfully'),
life: 2000
})
return true
} catch (error) {
console.error('Failed to delete asset:', error)
const errorMessage = error instanceof Error ? error.message : ''
const isCloudWarning = errorMessage.includes('Cloud')
toast.add({
severity: isCloudWarning ? 'warn' : 'error',
summary: isCloudWarning ? t('g.warning') : t('g.error'),
detail: errorMessage || t('mediaAsset.failedToDeleteAsset'),
life: 3000
})
return false
}
/**
* Delete a single asset with confirmation
* @param asset The asset to delete
* @param onDeleting Optional callback called with true when deletion starts and false when complete
* @returns true if the asset was deleted, false otherwise
*/
const deleteAsset = async (
asset: AssetItem,
onDeleting?: (deleting: boolean) => void
): Promise<boolean> => {
return deleteMultipleAssets([asset], onDeleting)
}
const copyJobId = async (asset?: AssetItem) => {
@@ -374,27 +327,33 @@ export function useMediaAssetActions() {
}
/**
* Delete multiple assets with confirmation dialog
* Delete one or more assets with confirmation dialog
* @param assets Array of assets to delete
* @param onDeleting Optional callback called with true when deletion starts (after confirmation) and false when complete
* @returns true if all assets were deleted successfully, false otherwise
*/
const deleteMultipleAssets = async (
assets: AssetItem[],
onDeleting?: (deleting: boolean) => void
) => {
if (!assets || assets.length === 0) return
): Promise<boolean> => {
if (!assets || assets.length === 0) return false
const assetsStore = useAssetsStore()
const isSingleAsset = assets.length === 1
return new Promise<void>((resolve) => {
return new Promise<boolean>((resolve) => {
dialogStore.showDialog({
key: 'delete-multiple-assets-confirmation',
title: t('mediaAsset.deleteSelectedTitle'),
key: 'delete-assets-confirmation',
title: isSingleAsset
? t('mediaAsset.deleteAssetTitle')
: t('mediaAsset.deleteSelectedTitle'),
component: ConfirmationDialogContent,
props: {
message: t('mediaAsset.deleteSelectedDescription', {
count: assets.length
}),
message: isSingleAsset
? t('mediaAsset.deleteAssetDescription')
: t('mediaAsset.deleteSelectedDescription', {
count: assets.length
}),
type: 'delete',
itemList: assets.map((asset) => asset.name),
onConfirm: async () => {
@@ -407,6 +366,8 @@ export function useMediaAssetActions() {
)
)
await new Promise((resolve) => setTimeout(resolve, 2000))
// Count successes and failures
const succeeded = results.filter(
(r) => r.status === 'fulfilled'
@@ -442,17 +403,29 @@ export function useMediaAssetActions() {
toast.add({
severity: 'success',
summary: t('g.success'),
detail: t('mediaAsset.selection.assetsDeletedSuccessfully', {
count: succeeded
}),
detail: isSingleAsset
? t('mediaAsset.assetDeletedSuccessfully')
: t('mediaAsset.selection.assetsDeletedSuccessfully', {
count: succeeded
}),
life: 2000
})
} else if (succeeded === 0) {
// All failed
const errorMessage =
failed[0].status === 'rejected'
? (failed[0].reason as Error)?.message
: ''
const isCloudWarning = errorMessage?.includes('Cloud')
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('mediaAsset.selection.failedToDeleteAssets'),
severity: isCloudWarning ? 'warn' : 'error',
summary: isCloudWarning ? t('g.warning') : t('g.error'),
detail:
errorMessage ||
(isSingleAsset
? t('mediaAsset.failedToDeleteAsset')
: t('mediaAsset.selection.failedToDeleteAssets')),
life: 3000
})
} else {
@@ -467,22 +440,26 @@ export function useMediaAssetActions() {
life: 3000
})
}
resolve(failed.length === 0)
} catch (error) {
console.error('Failed to delete assets:', error)
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('mediaAsset.selection.failedToDeleteAssets'),
detail: isSingleAsset
? t('mediaAsset.failedToDeleteAsset')
: t('mediaAsset.selection.failedToDeleteAssets'),
life: 3000
})
resolve(false)
} finally {
onDeleting?.(false)
}
resolve()
},
onCancel: () => {
resolve()
resolve(false)
}
}
})