From a75ca45cf8f4a996dc839fde6e99f4d94cd350f4 Mon Sep 17 00:00:00 2001 From: Darrell van Swinderen Date: Wed, 23 Sep 2026 10:21:05 +0200 Subject: [PATCH] feat(storage): rename a clip on its tile, and refresh when you say so A clip could not be renamed from Storage Saver, which is the moment somebody finds the clip worth keeping and wants to say what it is. The tile's name is now `ClipNameInput`, the field a library card uses: it saves on Enter or on leaving it, never per keystroke. Picking is the corner box's job alone, so a click on the name edits it rather than ticking the clip. **The list does not refresh on save.** A named clip is one somebody wants, so it no longer belongs here, but taking it away the moment Enter is pressed would pull the tile from under somebody fixing a typo. The row is patched in place so the new name shows, and a toast says why the clip will leave, with a "Refresh list" button that takes it away when the reader is done. Clearing a name back to nothing says nothing. Driven: renaming a tile keeps all 36 in place with the new name showing, the toast carries the button, and pressing it leaves 35. `screens.spec.ts` 9 passed; `npm run check` 849. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/components/Storage/BurstCluster.vue | 2 ++ .../src/components/Storage/BurstSection.vue | 2 ++ .../components/Storage/GraveyardSection.vue | 2 ++ .../components/Storage/StorageClipTile.vue | 31 ++++++++++++------- .../composables/library/useStorageSaver.ts | 31 +++++++++++++++++++ src/renderer/src/views/StorageSaverPage.vue | 23 ++++++++++++++ 6 files changed, 79 insertions(+), 12 deletions(-) diff --git a/src/renderer/src/components/Storage/BurstCluster.vue b/src/renderer/src/components/Storage/BurstCluster.vue index 84a4eaf..06033a1 100644 --- a/src/renderer/src/components/Storage/BurstCluster.vue +++ b/src/renderer/src/components/Storage/BurstCluster.vue @@ -31,6 +31,7 @@ const props = defineProps(); const emit = defineEmits<{ (e: 'keep', clipId: number): void; (e: 'delete', clips: Clip[]): void; + (e: 'renamed', clip: Clip): void; }>(); const { formatBytes } = useFormat(); @@ -70,6 +71,7 @@ const publishedDoomed = computed(() => doomed.value.filter((clip) => clip.publis mode="keep" :keeper="clip.id === keeperId" @toggle="emit('keep', clip.id)" + @renamed="emit('renamed', $event)" /> diff --git a/src/renderer/src/components/Storage/BurstSection.vue b/src/renderer/src/components/Storage/BurstSection.vue index 63d96e7..9feb6a4 100644 --- a/src/renderer/src/components/Storage/BurstSection.vue +++ b/src/renderer/src/components/Storage/BurstSection.vue @@ -27,6 +27,7 @@ defineProps(); const emit = defineEmits<{ (e: 'keep', clusterIndex: number, clipId: number): void; (e: 'delete', clips: Clip[]): void; + (e: 'renamed', clip: Clip): void; }>(); @@ -73,6 +74,7 @@ const emit = defineEmits<{ :deleting="deleting" @keep="emit('keep', index, $event)" @delete="emit('delete', $event)" + @renamed="emit('renamed', $event)" /> diff --git a/src/renderer/src/components/Storage/GraveyardSection.vue b/src/renderer/src/components/Storage/GraveyardSection.vue index 265172d..c4b2406 100644 --- a/src/renderer/src/components/Storage/GraveyardSection.vue +++ b/src/renderer/src/components/Storage/GraveyardSection.vue @@ -35,6 +35,7 @@ const emit = defineEmits<{ (e: 'toggle', clipId: number): void; (e: 'toggle-group', game: string): void; (e: 'delete'): void; + (e: 'renamed', clip: Clip): void; }>(); const { formatBytes } = useFormat(); @@ -110,6 +111,7 @@ function groupSelected(clips: Clip[]): boolean { :clip="clip" :selected="selected.has(clip.id)" @toggle="emit('toggle', clip.id)" + @renamed="emit('renamed', $event)" /> diff --git a/src/renderer/src/components/Storage/StorageClipTile.vue b/src/renderer/src/components/Storage/StorageClipTile.vue index ad4bd45..0a194f0 100644 --- a/src/renderer/src/components/Storage/StorageClipTile.vue +++ b/src/renderer/src/components/Storage/StorageClipTile.vue @@ -12,6 +12,7 @@ import ClipGoodBitPips from '@renderer/components/Library/ClipGoodBitPips.vue'; import { clipGoodBitRanges } from '@renderer/utils/goodBits'; import { useClipDetail } from '@renderer/composables/clips/useClipDetail'; import { formatRelativeTime } from '@renderer/helpers/dateFormat'; +import ClipNameInput from '@renderer/components/Library/ClipNameInput.vue'; import type { Clip } from '@renderer/types/clip'; /** @@ -33,8 +34,8 @@ import type { Clip } from '@renderer/types/clip'; * library card, from the same `useHoverScrub` and the same setting, and * pressing it opens the clip itself, full size, where it plays and can be * trimmed or deleted. - * - **The box and the caption are the choice.** The checkbox in the corner and - * the name and size underneath tick it, or in a burst, keep it. + * - **The corner box is the choice.** It ticks the clip, or in a burst, keeps + * it. The name under the picture is a field: it renames the clip in place. * * The size is the loudest number on it, deliberately: it is the reason * somebody is looking at this list. @@ -49,7 +50,11 @@ interface Props { } const props = withDefaults(defineProps(), { mode: 'select' }); -const emit = defineEmits<{ (e: 'toggle'): void }>(); +const emit = defineEmits<{ + (e: 'toggle'): void; + /** Renamed in place. The row is handed up so the tile shows the new name at once. */ + (e: 'renamed', clip: Clip): void; +}>(); const { formatBytes } = useFormat(); const config = useConfiguration(); @@ -235,19 +240,21 @@ const frame = computed(() => [ - - + diff --git a/src/renderer/src/composables/library/useStorageSaver.ts b/src/renderer/src/composables/library/useStorageSaver.ts index d387e55..4225d7a 100644 --- a/src/renderer/src/composables/library/useStorageSaver.ts +++ b/src/renderer/src/composables/library/useStorageSaver.ts @@ -230,9 +230,40 @@ export function useStorageSaver() { } } + /** + * Put a changed row in place of the old one, and nothing else. + * + * For a rename typed on a tile: the tile has to show the new name, and the + * list must not move under somebody who may be about to fix a typo in it. + * A named clip does not belong on this screen, but taking it away is the + * reader's call, made from the toast, not something that happens mid-edit. + */ + function patch(changed: Clip): void { + const swap = (clips: Clip[]): Clip[] => clips.map((clip) => (clip.id === changed.id ? { ...clip, ...changed } : clip)); + if (unreviewed.value) { + unreviewed.value = { + ...unreviewed.value, + groups: unreviewed.value.groups.map((group) => ({ ...group, clips: swap(group.clips) })), + }; + } + if (bursts.value) { + bursts.value = { + ...bursts.value, + clusters: bursts.value.clusters.map((cluster) => ({ ...cluster, clips: swap(cluster.clips) })), + }; + } + } + + /** Take every clip that has since been named or noted off the screen. */ + async function refreshAll(): Promise { + await Promise.all([loadUnreviewed(), loadBursts()]); + } + return { unreviewed, refresh, + patch, + refreshAll, bursts, loadingUnreviewed, loadingBursts, diff --git a/src/renderer/src/views/StorageSaverPage.vue b/src/renderer/src/views/StorageSaverPage.vue index d28033d..e5aa186 100644 --- a/src/renderer/src/views/StorageSaverPage.vue +++ b/src/renderer/src/views/StorageSaverPage.vue @@ -1,6 +1,7 @@