diff --git a/src/components/SelectionManager.vue b/src/components/SelectionManager.vue index 2444a406b..8625eb702 100644 --- a/src/components/SelectionManager.vue +++ b/src/components/SelectionManager.vue @@ -194,6 +194,10 @@ export default defineComponent({ }), mounted() { + _m.selectionManager = { + selectPhoto: this.selectPhoto.bind(this) as typeof this.selectPhoto, + }; + // Make default actions this.defaultActions = [ { diff --git a/src/components/viewer/Viewer.vue b/src/components/viewer/Viewer.vue index 0b975c178..6f9cba5fd 100644 --- a/src/components/viewer/Viewer.vue +++ b/src/components/viewer/Viewer.vue @@ -89,6 +89,8 @@ import EditFileIcon from 'vue-material-design-icons/FileEdit.vue'; import AlbumRemoveIcon from 'vue-material-design-icons/BookRemove.vue'; import AlbumIcon from 'vue-material-design-icons/ImageAlbum.vue'; import RotateLeftIcon from 'vue-material-design-icons/RotateLeft.vue'; +import CheckCircleIcon from 'vue-material-design-icons/CheckCircle.vue'; +import CheckboxBlankCircleOutline from 'vue-material-design-icons/CheckboxBlankCircleOutline.vue'; type IViewerAction = { /** Identifier (optional) */ @@ -231,6 +233,13 @@ export default defineComponent({ /** Get all actions to show */ actions(): IViewerAction[] { return [ + { + id: 'select', + name: this.t('memories', 'Select'), + icon: (this.currentPhoto?.flag ?? 0) & this.c.FLAG_SELECTED ? CheckCircleIcon : CheckboxBlankCircleOutline, + callback: this.toggleSelectCurrent, + if: true, + }, { id: 'share', name: this.t('memories', 'Share'), @@ -698,6 +707,22 @@ export default defineComponent({ // Remove fragment if closed if (!this.isOpen) { + const fragments = utils.fragment.list; + const hasSelection = fragments.some((f) => f.type === utils.fragment.types.selection); + + // We selected some photos while using the viewer. + if (hasSelection) { + // Keep the selection by replacing the route. + const newFragments = fragments.filter((f) => f.type !== utils.fragment.types.viewer); + const hash = utils.fragment.encode(newFragments); + + return _m.router.replace({ + path: _m.route.path, + query: _m.route.query, + hash: hash, + }); + } + return utils.fragment.pop(utils.fragment.types.viewer); } }, @@ -1034,6 +1059,10 @@ export default defineComponent({ keydown(e: KeyboardEvent) { if (e.defaultPrevented) return; + if (e.key == ' ') { + this.toggleSelectCurrent(); + } + if (e.key === 'Delete') { this.deleteCurrent(); } @@ -1043,6 +1072,13 @@ export default defineComponent({ } }, + /** Select the current photo */ + toggleSelectCurrent() { + if (this.currentPhoto) { + _m.selectionManager.selectPhoto(this.currentPhoto); + } + }, + /** Delete this photo and refresh */ async deleteCurrent() { if (this.routeIsPublic) return; diff --git a/src/globals.d.ts b/src/globals.d.ts index f0d52c8f1..a53047623 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -50,6 +50,10 @@ declare global { search: () => void; }; + selectionManager: { + selectPhoto: (photo: IPhoto, val?: boolean, noUpdate?: boolean) => void; + }; + sidebar: { open: (photo: IPhoto | number, filename?: string, forceNative?: boolean) => void; close: () => void; diff --git a/src/main.ts b/src/main.ts index ed41f11a0..fc7f11938 100644 --- a/src/main.ts +++ b/src/main.ts @@ -23,6 +23,7 @@ globalThis._m = { routes: routes, modals: {} as any, + selectionManager: {} as any, sidebar: {} as any, viewer: {} as any, video: {} as any, diff --git a/src/services/utils/fragment.ts b/src/services/utils/fragment.ts index 68dfc7cbc..220f83856 100644 --- a/src/services/utils/fragment.ts +++ b/src/services/utils/fragment.ts @@ -193,6 +193,8 @@ export const fragment = { get viewer() { return this.get(FragmentType.viewer); }, + + encode: encodeFragment, }; onDOMLoaded(() => {