diff --git a/src/components/SelectionManager.vue b/src/components/SelectionManager.vue index 5bd481a6e..555da773d 100644 --- a/src/components/SelectionManager.vue +++ b/src/components/SelectionManager.vue @@ -44,6 +44,7 @@ :updateLoading="updateLoading" /> + @@ -73,6 +74,7 @@ import EditDate from "./modal/EditDate.vue"; import EditExif from "./modal/EditExif.vue"; import FaceMoveModal from "./modal/FaceMoveModal.vue"; import AddToAlbumModal from "./modal/AddToAlbumModal.vue"; +import MoveToFolderModal from "./modal/MoveToFolderModal.vue"; import StarIcon from "vue-material-design-icons/Star.vue"; import DownloadIcon from "vue-material-design-icons/Download.vue"; @@ -86,6 +88,7 @@ import CloseIcon from "vue-material-design-icons/Close.vue"; import MoveIcon from "vue-material-design-icons/ImageMove.vue"; 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"; type Selection = Map; @@ -98,6 +101,7 @@ export default defineComponent({ EditExif, FaceMoveModal, AddToAlbumModal, + MoveToFolderModal, CloseIcon, }, @@ -185,6 +189,12 @@ export default defineComponent({ callback: this.viewInFolder.bind(this), if: () => this.selection.size === 1 && !this.routeIsAlbum(), }, + { + name: t("memories", "Move to folder"), + icon: FolderMoveIcon, + callback: this.moveToFolder.bind(this), + if: () => !this.routeIsAlbum() && !this.routeIsArchive(), + }, { name: t("memories", "Add to album"), icon: AlbumsIcon, @@ -800,6 +810,13 @@ export default defineComponent({ (this.$refs.addToAlbumModal).open(Array.from(selection.values())); }, + /** + * Move selected photos to folder + */ + async moveToFolder(selection: Selection) { + (this.$refs.moveToFolderModal).open(Array.from(selection.values())); + }, + /** * Move selected photos to another person */ diff --git a/src/components/modal/MoveToFolderModal.vue b/src/components/modal/MoveToFolderModal.vue new file mode 100644 index 000000000..0773e684c --- /dev/null +++ b/src/components/modal/MoveToFolderModal.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/src/services/dav/base.ts b/src/services/dav/base.ts index 41d06bec5..aedbf0410 100644 --- a/src/services/dav/base.ts +++ b/src/services/dav/base.ts @@ -190,19 +190,12 @@ export async function* runInParallel( } /** - * Delete all files in a given list of Ids + * Extend given list of Ids with extra files for live photos. * - * @param photos list of photos to delete - * @returns list of file ids that were deleted + * @param photos list of photos to search for live photos + * @returns list of file ids that contains extra file Ids for live photos if any */ -export async function* deletePhotos(photos: IPhoto[]) { - if (photos.length === 0) { - return; - } - - const fileIdsSet = new Set(photos.map((p) => p.fileid)); - - // Get live photo data +async function extendWithLivePhotos(photos: IPhoto[]) { const livePhotos = ( await Promise.all( photos @@ -212,7 +205,6 @@ export async function* deletePhotos(photos: IPhoto[]) { try { const response = await axios.get(url); const data = response.data; - fileIdsSet.add(data.fileid); return { fileid: data.fileid, } as IPhoto; @@ -224,12 +216,29 @@ export async function* deletePhotos(photos: IPhoto[]) { ) ).filter((p) => p !== null) as IPhoto[]; + return photos.concat(livePhotos); +} + +/** + * Delete all files in a given list of Ids + * + * @param photos list of photos to delete + * @returns list of file ids that were deleted + */ +export async function* deletePhotos(photos: IPhoto[]) { + if (photos.length === 0) { + return; + } + + const photosWithLive = await extendWithLivePhotos(photos); + const fileIdsSet = new Set(photosWithLive.map((p) => p.fileid)); + // Get files data let fileInfos: IFileInfo[] = []; try { - fileInfos = await getFiles(photos.concat(livePhotos)); + fileInfos = await getFiles(photosWithLive); } catch (e) { - console.error("Failed to get file info for files to delete", photos, e); + console.error("Failed to get file info for files to delete", photosWithLive, e); showError(t("memories", "Failed to delete files.")); return; } @@ -253,3 +262,70 @@ export async function* deletePhotos(photos: IPhoto[]) { yield* runInParallel(calls, 10); } + +/** + * Move all files in a given list of Ids to given destination + * + * @param photos list of photos to move + * @param destination to move photos into + * @param overwrite behaviour if the target exists. `true` overwrites, `false` fails. + * @returns list of file ids that were moved + */ +export async function* movePhotos(photos: IPhoto[], destination: string, overwrite: boolean) { + if (photos.length === 0) { + return; + } + + // Set absolute target path + const prefixPath = `files/${getCurrentUser()?.uid}`; + let targetPath = prefixPath + destination; + if (!targetPath.endsWith('/')) { + targetPath += '/'; + } + + const photosWithLive = await extendWithLivePhotos(photos); + const fileIdsSet = new Set(photosWithLive.map((p) => p.fileid)); + + // Get files data + let fileInfos: IFileInfo[] = []; + try { + fileInfos = await getFiles(photosWithLive); + } catch (e) { + console.error("Failed to get file info for files to move", photosWithLive, e); + showError(t("memories", "Failed to move files.")); + return; + } + + // Move each file + fileInfos = fileInfos.filter((f) => fileIdsSet.has(f.fileid)); + const calls = fileInfos.map((fileInfo) => async () => { + try { + await client.moveFile( + fileInfo.originalFilename, + targetPath + fileInfo.basename, + // @ts-ignore - https://github.com/perry-mitchell/webdav-client/issues/329 + { headers: { 'Overwrite' : overwrite ? 'T' : 'F' }}); + return fileInfo.fileid; + } catch (error) { + console.error("Failed to move", fileInfo, error); + if (error.response?.status === 412) { + // Precondition failed (only if `overwrite` flag set to false) + showError( + t("memories", "Could not move {fileName}, target exists.", { + fileName: fileInfo.filename, + }) + ); + return 0; + } + + showError( + t("memories", "Failed to move {fileName}.", { + fileName: fileInfo.filename, + }) + ); + return 0; + } + }); + + yield* runInParallel(calls, 10); +}