From e3e20f9731695ff7dbe21b0a16fb2a2d9be30f49 Mon Sep 17 00:00:00 2001 From: g3n35i5 Date: Wed, 16 Aug 2023 20:58:22 +0200 Subject: [PATCH 1/2] fix: Fix a bug that caused some unassigned face photos to be unselectable This resolves #721 --- src/components/SelectionManager.vue | 87 ++++++++++++++++------------- src/types.ts | 57 ++++++++++++++++++- 2 files changed, 103 insertions(+), 41 deletions(-) diff --git a/src/components/SelectionManager.vue b/src/components/SelectionManager.vue index f84644d5..4545ee53 100644 --- a/src/components/SelectionManager.vue +++ b/src/components/SelectionManager.vue @@ -71,9 +71,7 @@ import AlbumsIcon from 'vue-material-design-icons/ImageAlbum.vue'; import AlbumRemoveIcon from 'vue-material-design-icons/BookRemove.vue'; import FolderMoveIcon from 'vue-material-design-icons/FolderMove.vue'; -import { IDay, IHeadRow, IPhoto, IRow, IRowType, ISelectionAction } from '../types'; - -type Selection = Map; +import { IDay, IHeadRow, IPhoto, IRow, IRowType, ISelectionAction, SelectionProxy } from '../types'; /** * The distance for which the touch selection is clamped. @@ -124,14 +122,14 @@ export default defineComponent({ data: () => ({ show: false, size: 0, - selection: new Map(), + selection: new SelectionProxy(), defaultActions: null! as ISelectionAction[], touchAnchor: null as IPhoto | null, prevTouch: null as Touch | null, touchTimer: 0, touchMoved: false, - touchPrevSel: null as Selection | null, + touchPrevSel: null as SelectionProxy | null, prevOver: null as IPhoto | null, touchScrollInterval: 0, touchScrollDelta: 0, @@ -246,8 +244,17 @@ export default defineComponent({ this.$emit('delete', photos); }, - deleteSelectedPhotosById(delIds: number[], selection: Selection) { - return this.deletePhotos(delIds.map((id) => selection.get(id)).filter((p): p is IPhoto => p !== undefined)); + deleteSelectedPhotosById(delIds: number[], selection: SelectionProxy) { + let photosToDelete: IPhoto[] = [] + for (const photo of selection.values()) { + if (photo === undefined) { + continue + } + if (delIds.indexOf(photo.fileid) > -1) { + photosToDelete.push(photo) + } + } + return this.deletePhotos(photosToDelete); }, updateLoading(delta: number) { @@ -286,11 +293,11 @@ export default defineComponent({ }, /** Is this fileid (or anything if not specified) selected */ - has(fileid?: number) { - if (fileid === undefined) { + has(photo?: IPhoto) { + if (photo === undefined) { return this.selection.size > 0; } - return this.selection.has(fileid); + return this.selection.has(photo); }, /** Get the actions list */ @@ -342,7 +349,7 @@ export default defineComponent({ this.touchAnchor = photo; this.prevOver = photo; this.prevTouch = event.touches[0]; - this.touchPrevSel = new Map(this.selection); + this.touchPrevSel = this.selection.copy(); this.touchMoved = false; this.touchTimer = window.setTimeout(() => { if (this.touchAnchor === photo) { @@ -499,7 +506,7 @@ export default defineComponent({ reverse = overPhoto.dayid > this.touchAnchor.dayid != this.isreverse; } - const newSelection = new Map(this.touchPrevSel); + const newSelection = this.touchPrevSel!.copy(); const updatedDays = new Set(); // Walk over rows @@ -524,7 +531,7 @@ export default defineComponent({ if (!p) break; // shouldn't happen, ever // This is there now - newSelection.set(p.fileid, p); + newSelection.add(p); // Perf: only update heads if not selected if (!(p.flag & this.c.FLAG_SELECTED)) { @@ -541,10 +548,10 @@ export default defineComponent({ } // Remove unselected - for (const [fileid, p] of this.selection) { - if (!newSelection.has(fileid)) { - this.selectPhoto(p, false, true); - updatedDays.add(p.dayid); + for (const photo of this.selection.values()) { + if (!newSelection.has(photo)) { + this.selectPhoto(photo, false, true); + updatedDays.add(photo.dayid); } } @@ -563,10 +570,10 @@ export default defineComponent({ return; // ignore placeholders } - const nval = val ?? !this.selection.has(photo.fileid); + const nval = val ?? !this.selection.has(photo); if (nval) { photo.flag |= this.c.FLAG_SELECTED; - this.selection.set(photo.fileid, photo); + this.selection.add(photo); this.selectionChanged(); } else { photo.flag &= ~this.c.FLAG_SELECTED; @@ -574,8 +581,8 @@ export default defineComponent({ // Only do this if the photo in the selection set is this one. // The problem arises when there are duplicates (e.g. face rect) // in the list, which creates an inconsistent state if we do this. - if (this.selection.get(photo.fileid) === photo) { - this.selection.delete(photo.fileid); + if (this.selection.get(photo) === photo) { + this.selection.delete(photo); this.selectionChanged(); } } @@ -679,7 +686,7 @@ export default defineComponent({ Array.from(toClear).forEach((photo: IPhoto) => { photo.flag &= ~this.c.FLAG_SELECTED; heads.add(this.heads[photo.dayid]); - this.selection.delete(photo.fileid); + this.selection.delete(photo); this.selectionChanged(); }); heads.forEach(this.updateHeadSelected); @@ -693,26 +700,26 @@ export default defineComponent({ } // FileID => Photo for new day - const dayMap = new Map(); + const dayMap = new Map(); day.detail?.forEach((photo) => { - dayMap.set(photo.fileid, photo); + dayMap.set(photo.key!, photo); }); - this.selection.forEach((photo, fileid) => { + this.selection.forEach((photo, key) => { // Process this day only if (photo.dayid !== day.dayid) { return; } // Remove all selections that are not in the new day - const newPhoto = dayMap.get(fileid); + const newPhoto = dayMap.get(photo.key!); if (!newPhoto) { - this.selection.delete(fileid); + this.selection.delete(photo); return; } // Update the photo object - this.selection.set(fileid, newPhoto); + this.selection.add(newPhoto); newPhoto.flag |= this.c.FLAG_SELECTED; }); @@ -722,7 +729,7 @@ export default defineComponent({ /** * Download the currently selected files */ - async downloadSelection(selection: Selection) { + async downloadSelection(selection: SelectionProxy) { if (selection.size >= 100) { if (!confirm(this.t('memories', 'You are about to download a large number of files. Are you sure?'))) { return; @@ -734,14 +741,14 @@ export default defineComponent({ /** * Check if all files selected currently are favorites */ - allSelectedFavorites(selection: Selection) { + allSelectedFavorites(selection: SelectionProxy) { return Array.from(selection.values()).every((p) => p.flag & this.c.FLAG_IS_FAVORITE); }, /** * Favorite the currently selected photos */ - async favoriteSelection(selection: Selection) { + async favoriteSelection(selection: SelectionProxy) { const val = !this.allSelectedFavorites(selection); for await (const favIds of dav.favoritePhotos(Array.from(selection.values()), val)) { } @@ -751,7 +758,7 @@ export default defineComponent({ /** * Delete the currently selected photos */ - async deleteSelection(selection: Selection) { + async deleteSelection(selection: SelectionProxy) { if (selection.size >= 100) { if (!confirm(this.t('memories', 'You are about to delete a large number of files. Are you sure?'))) { return; @@ -771,7 +778,7 @@ export default defineComponent({ /** * Open the edit date dialog */ - async editMetadataSelection(selection: Selection, sections?: number[]) { + async editMetadataSelection(selection: SelectionProxy, sections?: number[]) { globalThis.editMetadata(Array.from(selection.values()), sections); }, @@ -779,7 +786,7 @@ export default defineComponent({ * Open the files app with the selected file (one) * Opens a new window. */ - async viewInFolder(selection: Selection) { + async viewInFolder(selection: SelectionProxy) { if (selection.size !== 1) return; dav.viewInFolder(selection.values().next().value); }, @@ -787,14 +794,14 @@ export default defineComponent({ /** * Archive the currently selected photos */ - async archiveSelection(selection: Selection) { + async archiveSelection(selection: SelectionProxy) { if (selection.size >= 100) { if (!confirm(this.t('memories', 'You are about to touch a large number of files. Are you sure?'))) { return; } } - for await (let delIds of dav.archiveFilesByIds(Array.from(selection.keys()), !this.routeIsArchive)) { + for await (let delIds of dav.archiveFilesByIds(Array.from(selection.fileIDs()), !this.routeIsArchive)) { this.deleteSelectedPhotosById(delIds, selection); } }, @@ -802,21 +809,21 @@ export default defineComponent({ /** * Move selected photos to album */ - async addToAlbum(selection: Selection) { + async addToAlbum(selection: SelectionProxy) { globalThis.updateAlbums(Array.from(selection.values())); }, /** * Move selected photos to folder */ - async moveToFolder(selection: Selection) { + async moveToFolder(selection: SelectionProxy) { (this.$refs.moveToFolderModal).open(Array.from(selection.values())); }, /** * Move selected photos to another person */ - async moveSelectionToPerson(selection: Selection) { + async moveSelectionToPerson(selection: SelectionProxy) { if (!this.config.show_face_rect && !this.routeIsRecognizeUnassigned) { showError(this.t('memories', 'You must enable "Mark person in preview" to use this feature')); return; @@ -827,7 +834,7 @@ export default defineComponent({ /** * Remove currently selected photos from person */ - async removeSelectionFromPerson(selection: Selection) { + async removeSelectionFromPerson(selection: SelectionProxy) { // Make sure route is valid const { user, name } = this.$route.params; if (this.$route.name !== 'recognize' || !user || !name) { diff --git a/src/types.ts b/src/types.ts index 432f453a..37156cc7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -220,6 +220,61 @@ export type ITick = { key?: number; }; +export class SelectionProxy { + private map: Map = new Map(); + + add(photo: IPhoto): this { + this.map.set(photo.key!, photo); + return this; + } + + get(photo: IPhoto): IPhoto | undefined { + return this.map.get(photo.key!); + } + + clear() { + this.map.clear(); + } + + delete(photo: IPhoto): boolean { + return this.map.delete(photo.key!); + } + + has(photo: IPhoto): boolean { + return this.map.has(photo.key!); + } + + values(): IterableIterator { + return this.map.values(); + } + + get size() { + return this.map.size; + } + + fileIDs(): number[] { + let fileIDs: number[] = []; + this.forEach((photo, _) => { + fileIDs.push(photo.fileid); + }); + return fileIDs; + } + + forEach(callbackfn: (value: IPhoto, key: string, map: Map) => void, thisArg?: any): void { + this.map.forEach((value, key) => { + callbackfn.call(thisArg, value, key, this); + }); + } + + copy(): SelectionProxy { + let newProxy = new SelectionProxy(); + this.forEach((photo, _) => { + newProxy.add(photo); + }); + return newProxy; + } +} + export type ISelectionAction = { /** Identifier (optional) */ id?: string; @@ -228,7 +283,7 @@ export type ISelectionAction = { /** Icon component */ icon: any; /** Action to perform */ - callback: (selection: Map) => Promise; + callback: (selection: any) => Promise; /** Condition to check for including */ if?: (self?: any) => boolean; /** Allow for public routes (default false) */ From 693fa029d7d9216339c172db9f26fd12013dc4d7 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Sat, 19 Aug 2023 09:07:49 -0700 Subject: [PATCH 2/2] Refactor SelectionManager --- src/components/SelectionManager.vue | 182 +++++++++++++++++----------- src/services/dav/favorites.ts | 19 ++- src/types.ts | 70 ----------- 3 files changed, 123 insertions(+), 148 deletions(-) diff --git a/src/components/SelectionManager.vue b/src/components/SelectionManager.vue index 4545ee53..0fed13e6 100644 --- a/src/components/SelectionManager.vue +++ b/src/components/SelectionManager.vue @@ -71,7 +71,7 @@ import AlbumsIcon from 'vue-material-design-icons/ImageAlbum.vue'; import AlbumRemoveIcon from 'vue-material-design-icons/BookRemove.vue'; import FolderMoveIcon from 'vue-material-design-icons/FolderMove.vue'; -import { IDay, IHeadRow, IPhoto, IRow, IRowType, ISelectionAction, SelectionProxy } from '../types'; +import { IDay, IHeadRow, IPhoto, IRow, IRowType } from '../types'; /** * The distance for which the touch selection is clamped. @@ -84,6 +84,63 @@ const TOUCH_SELECT_CLAMP = { bufferPx: 5, // number of pixels to clamp inside recycler area }; +class Selection extends Map { + addBy(photo: IPhoto): this { + console.assert(photo?.key, 'SelectionManager::addBy encountered a photo without a key'); + this.set(photo.key!, photo); + return this; + } + + getBy({ key }: { key?: string }): IPhoto | undefined { + console.assert(key, 'SelectionManager::getBy encountered a photo without a key'); + return this.get(key!); + } + + deleteBy({ key }: { key?: string }): boolean { + console.assert(key, 'SelectionManager::deleteBy encountered a photo without a key'); + return this.delete(key!); + } + + hasBy({ key }: { key?: string }): boolean { + console.assert(key, 'SelectionManager::hasBy encountered a photo without a key'); + return this.has(key!); + } + + fileids(): Set { + return new Set(Array.from(this.values()).map((p) => p.fileid)); + } + + photosNoDupFileId(): IPhoto[] { + const fileids = this.fileids(); + return Array.from(this.values()).filter((p) => fileids.delete(p.fileid)); + } + + photosFromFileIds(fileIds: number[] | Set): IPhoto[] { + const idSet = new Set(fileIds); + const photos = Array.from(this.values()); + return photos.filter((p) => idSet.has(p?.fileid)); + } + + clone(): Selection { + return new Selection(this); + } +} + +type ISelectionAction = { + /** Identifier (optional) */ + id?: string; + /** Display text */ + name: string; + /** Icon component */ + icon: any; + /** Action to perform */ + callback: (selection: Selection) => Promise; + /** Condition to check for including */ + if?: (self?: any) => boolean; + /** Allow for public routes (default false) */ + allowPublic?: boolean; +}; + export default defineComponent({ name: 'SelectionManager', components: { @@ -122,14 +179,14 @@ export default defineComponent({ data: () => ({ show: false, size: 0, - selection: new SelectionProxy(), + selection: new Selection(), defaultActions: null! as ISelectionAction[], touchAnchor: null as IPhoto | null, prevTouch: null as Touch | null, touchTimer: 0, touchMoved: false, - touchPrevSel: null as SelectionProxy | null, + touchPrevSel: null as Selection | null, prevOver: null as IPhoto | null, touchScrollInterval: 0, touchScrollDelta: 0, @@ -244,17 +301,8 @@ export default defineComponent({ this.$emit('delete', photos); }, - deleteSelectedPhotosById(delIds: number[], selection: SelectionProxy) { - let photosToDelete: IPhoto[] = [] - for (const photo of selection.values()) { - if (photo === undefined) { - continue - } - if (delIds.indexOf(photo.fileid) > -1) { - photosToDelete.push(photo) - } - } - return this.deletePhotos(photosToDelete); + deleteSelectedPhotosById(delIds: number[], selection: Selection) { + return this.deletePhotos(selection.photosFromFileIds(delIds)); }, updateLoading(delta: number) { @@ -292,12 +340,9 @@ export default defineComponent({ document.body.classList.toggle('has-top-bar', has); }, - /** Is this fileid (or anything if not specified) selected */ - has(photo?: IPhoto) { - if (photo === undefined) { - return this.selection.size > 0; - } - return this.selection.has(photo); + /** Is the selection empty */ + empty(): boolean { + return !this.selection.size; }, /** Get the actions list */ @@ -323,7 +368,7 @@ export default defineComponent({ if (event.pointerType === 'touch') return; // let touch events handle this if (event.pointerType === 'mouse' && event.button !== 0) return; // only left click for mouse - if (this.has() || event.ctrlKey || event.shiftKey) { + if (!this.empty() || event.ctrlKey || event.shiftKey) { this.clickSelectionIcon(photo, event, rowIdx); } else { this.openViewer(photo); @@ -332,7 +377,7 @@ export default defineComponent({ /** Clicking on checkmark icon */ clickSelectionIcon(photo: IPhoto, event: PointerEvent, rowIdx: number) { - if (this.has() && event.shiftKey) { + if (!this.empty() && event.shiftKey) { this.selectMulti(photo, this.rows, rowIdx); } else { this.selectPhoto(photo); @@ -349,7 +394,7 @@ export default defineComponent({ this.touchAnchor = photo; this.prevOver = photo; this.prevTouch = event.touches[0]; - this.touchPrevSel = this.selection.copy(); + this.touchPrevSel = this.selection.clone(); this.touchMoved = false; this.touchTimer = window.setTimeout(() => { if (this.touchAnchor === photo) { @@ -506,7 +551,7 @@ export default defineComponent({ reverse = overPhoto.dayid > this.touchAnchor.dayid != this.isreverse; } - const newSelection = this.touchPrevSel!.copy(); + const newSelection = this.touchPrevSel!.clone(); const updatedDays = new Set(); // Walk over rows @@ -527,29 +572,29 @@ export default defineComponent({ continue; } - let p = this.rows[i]?.photos?.[j]; - if (!p) break; // shouldn't happen, ever + const photo = this.rows[i]?.photos?.[j]; + if (!photo) break; // shouldn't happen, ever // This is there now - newSelection.add(p); + newSelection.addBy(photo); // Perf: only update heads if not selected - if (!(p.flag & this.c.FLAG_SELECTED)) { - this.selectPhoto(p, true, true); - updatedDays.add(p.dayid); + if (!(photo.flag & this.c.FLAG_SELECTED)) { + this.selectPhoto(photo, true, true); + updatedDays.add(photo.dayid); } // We're trying to update too much -- something went wrong if (newSelection.size - this.selection.size > 50) break; // Check goal - if (p === overPhoto) break; + if (photo === overPhoto) break; j += reverse ? -1 : 1; } // Remove unselected - for (const photo of this.selection.values()) { - if (!newSelection.has(photo)) { + for (const [_, photo] of this.selection) { + if (!newSelection.hasBy(photo)) { this.selectPhoto(photo, false, true); updatedDays.add(photo.dayid); } @@ -570,21 +615,15 @@ export default defineComponent({ return; // ignore placeholders } - const nval = val ?? !this.selection.has(photo); + const nval = val ?? !this.selection.hasBy(photo); if (nval) { photo.flag |= this.c.FLAG_SELECTED; - this.selection.add(photo); + this.selection.addBy(photo); this.selectionChanged(); } else { photo.flag &= ~this.c.FLAG_SELECTED; - - // Only do this if the photo in the selection set is this one. - // The problem arises when there are duplicates (e.g. face rect) - // in the list, which creates an inconsistent state if we do this. - if (this.selection.get(photo) === photo) { - this.selection.delete(photo); - this.selectionChanged(); - } + this.selection.deleteBy(photo); + this.selectionChanged(); } if (!noUpdate) { @@ -686,7 +725,7 @@ export default defineComponent({ Array.from(toClear).forEach((photo: IPhoto) => { photo.flag &= ~this.c.FLAG_SELECTED; heads.add(this.heads[photo.dayid]); - this.selection.delete(photo); + this.selection.deleteBy(photo); this.selectionChanged(); }); heads.forEach(this.updateHeadSelected); @@ -695,15 +734,11 @@ export default defineComponent({ /** Restore selections from new day object */ restoreDay(day: IDay) { - if (!this.has()) { - return; - } + if (this.empty()) return; // FileID => Photo for new day - const dayMap = new Map(); - day.detail?.forEach((photo) => { - dayMap.set(photo.key!, photo); - }); + const dayMap = new Selection(); + day.detail?.forEach((photo) => dayMap.addBy(photo)); this.selection.forEach((photo, key) => { // Process this day only @@ -712,14 +747,14 @@ export default defineComponent({ } // Remove all selections that are not in the new day - const newPhoto = dayMap.get(photo.key!); + const newPhoto = dayMap.get(key); if (!newPhoto) { - this.selection.delete(photo); + this.selection.delete(key); return; } // Update the photo object - this.selection.add(newPhoto); + this.selection.addBy(newPhoto); newPhoto.flag |= this.c.FLAG_SELECTED; }); @@ -729,28 +764,29 @@ export default defineComponent({ /** * Download the currently selected files */ - async downloadSelection(selection: SelectionProxy) { + async downloadSelection(selection: Selection) { if (selection.size >= 100) { if (!confirm(this.t('memories', 'You are about to download a large number of files. Are you sure?'))) { return; } } - await dav.downloadFilesByPhotos(Array.from(selection.values())); + await dav.downloadFilesByPhotos(selection.photosNoDupFileId()); }, /** * Check if all files selected currently are favorites */ - allSelectedFavorites(selection: SelectionProxy) { + allSelectedFavorites(selection: Selection) { return Array.from(selection.values()).every((p) => p.flag & this.c.FLAG_IS_FAVORITE); }, /** * Favorite the currently selected photos */ - async favoriteSelection(selection: SelectionProxy) { + async favoriteSelection(selection: Selection) { const val = !this.allSelectedFavorites(selection); - for await (const favIds of dav.favoritePhotos(Array.from(selection.values()), val)) { + for await (const ids of dav.favoritePhotos(selection.photosNoDupFileId(), val)) { + selection.photosFromFileIds(ids).forEach((photo) => dav.favoriteSetFlag(photo, val)); } this.clearSelection(); }, @@ -758,7 +794,7 @@ export default defineComponent({ /** * Delete the currently selected photos */ - async deleteSelection(selection: SelectionProxy) { + async deleteSelection(selection: Selection) { if (selection.size >= 100) { if (!confirm(this.t('memories', 'You are about to delete a large number of files. Are you sure?'))) { return; @@ -766,7 +802,7 @@ export default defineComponent({ } try { - for await (const delIds of dav.deletePhotos(Array.from(selection.values()))) { + for await (const delIds of dav.deletePhotos(selection.photosNoDupFileId())) { this.deleteSelectedPhotosById(delIds, selection); } } catch (e) { @@ -778,15 +814,15 @@ export default defineComponent({ /** * Open the edit date dialog */ - async editMetadataSelection(selection: SelectionProxy, sections?: number[]) { - globalThis.editMetadata(Array.from(selection.values()), sections); + async editMetadataSelection(selection: Selection, sections?: number[]) { + globalThis.editMetadata(selection.photosNoDupFileId(), sections); }, /** * Open the files app with the selected file (one) * Opens a new window. */ - async viewInFolder(selection: SelectionProxy) { + async viewInFolder(selection: Selection) { if (selection.size !== 1) return; dav.viewInFolder(selection.values().next().value); }, @@ -794,14 +830,14 @@ export default defineComponent({ /** * Archive the currently selected photos */ - async archiveSelection(selection: SelectionProxy) { + async archiveSelection(selection: Selection) { if (selection.size >= 100) { - if (!confirm(this.t('memories', 'You are about to touch a large number of files. Are you sure?'))) { + if (!confirm(this.t('memories', 'You are about to move a large number of files. Are you sure?'))) { return; } } - for await (let delIds of dav.archiveFilesByIds(Array.from(selection.fileIDs()), !this.routeIsArchive)) { + for await (let delIds of dav.archiveFilesByIds(Array.from(selection.fileids()), !this.routeIsArchive)) { this.deleteSelectedPhotosById(delIds, selection); } }, @@ -809,21 +845,21 @@ export default defineComponent({ /** * Move selected photos to album */ - async addToAlbum(selection: SelectionProxy) { - globalThis.updateAlbums(Array.from(selection.values())); + async addToAlbum(selection: Selection) { + globalThis.updateAlbums(selection.photosNoDupFileId()); }, /** * Move selected photos to folder */ - async moveToFolder(selection: SelectionProxy) { - (this.$refs.moveToFolderModal).open(Array.from(selection.values())); + async moveToFolder(selection: Selection) { + (this.$refs.moveToFolderModal).open(selection.photosNoDupFileId()); }, /** * Move selected photos to another person */ - async moveSelectionToPerson(selection: SelectionProxy) { + async moveSelectionToPerson(selection: Selection) { if (!this.config.show_face_rect && !this.routeIsRecognizeUnassigned) { showError(this.t('memories', 'You must enable "Mark person in preview" to use this feature')); return; @@ -834,7 +870,7 @@ export default defineComponent({ /** * Remove currently selected photos from person */ - async removeSelectionFromPerson(selection: SelectionProxy) { + async removeSelectionFromPerson(selection: Selection) { // Make sure route is valid const { user, name } = this.$route.params; if (this.$route.name !== 'recognize' || !user || !name) { diff --git a/src/services/dav/favorites.ts b/src/services/dav/favorites.ts index b952cf0b..c6cd700a 100644 --- a/src/services/dav/favorites.ts +++ b/src/services/dav/favorites.ts @@ -60,11 +60,7 @@ export async function* favoritePhotos(photos: IPhoto[], favoriteState: boolean) try { await favoriteFile(fileInfo.originalFilename, favoriteState); const photo = photos.find((p) => p.fileid === fileInfo.fileid)!; - if (favoriteState) { - photo.flag |= utils.constants.c.FLAG_IS_FAVORITE; - } else { - photo.flag &= ~utils.constants.c.FLAG_IS_FAVORITE; - } + favoriteSetFlag(photo, favoriteState); return fileInfo.fileid as number; } catch (error) { console.error('Failed to favorite', fileInfo, error); @@ -79,3 +75,16 @@ export async function* favoritePhotos(photos: IPhoto[], favoriteState: boolean) yield* base.runInParallel(calls, 10); } + +/** + * Set the favorite flag on a photo + * @param photo Photo to set the flag on + * @param val New value of the flag + */ +export function favoriteSetFlag(photo: IPhoto, val: boolean) { + if (val) { + photo.flag |= utils.constants.c.FLAG_IS_FAVORITE; + } else { + photo.flag &= ~utils.constants.c.FLAG_IS_FAVORITE; + } +} diff --git a/src/types.ts b/src/types.ts index 9793e436..dc3cb513 100644 --- a/src/types.ts +++ b/src/types.ts @@ -221,76 +221,6 @@ export type ITick = { key?: number; }; -export class SelectionProxy { - private map: Map = new Map(); - - add(photo: IPhoto): this { - this.map.set(photo.key!, photo); - return this; - } - - get(photo: IPhoto): IPhoto | undefined { - return this.map.get(photo.key!); - } - - clear() { - this.map.clear(); - } - - delete(photo: IPhoto): boolean { - return this.map.delete(photo.key!); - } - - has(photo: IPhoto): boolean { - return this.map.has(photo.key!); - } - - values(): IterableIterator { - return this.map.values(); - } - - get size() { - return this.map.size; - } - - fileIDs(): number[] { - let fileIDs: number[] = []; - this.forEach((photo, _) => { - fileIDs.push(photo.fileid); - }); - return fileIDs; - } - - forEach(callbackfn: (value: IPhoto, key: string, map: Map) => void, thisArg?: any): void { - this.map.forEach((value, key) => { - callbackfn.call(thisArg, value, key, this); - }); - } - - copy(): SelectionProxy { - let newProxy = new SelectionProxy(); - this.forEach((photo, _) => { - newProxy.add(photo); - }); - return newProxy; - } -} - -export type ISelectionAction = { - /** Identifier (optional) */ - id?: string; - /** Display text */ - name: string; - /** Icon component */ - icon: any; - /** Action to perform */ - callback: (selection: any) => Promise; - /** Condition to check for including */ - if?: (self?: any) => boolean; - /** Allow for public routes (default false) */ - allowPublic?: boolean; -}; - export type IConfig = { version: string; vod_disable: boolean;