Merge branch 'ahaltindis-move_to_folder'
commit
43102ce847
|
@ -44,6 +44,7 @@
|
||||||
:updateLoading="updateLoading"
|
:updateLoading="updateLoading"
|
||||||
/>
|
/>
|
||||||
<AddToAlbumModal ref="addToAlbumModal" @added="clearSelection" />
|
<AddToAlbumModal ref="addToAlbumModal" @added="clearSelection" />
|
||||||
|
<MoveToFolderModal ref="moveToFolderModal" @moved="refresh" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -73,6 +74,7 @@ import EditDate from "./modal/EditDate.vue";
|
||||||
import EditExif from "./modal/EditExif.vue";
|
import EditExif from "./modal/EditExif.vue";
|
||||||
import FaceMoveModal from "./modal/FaceMoveModal.vue";
|
import FaceMoveModal from "./modal/FaceMoveModal.vue";
|
||||||
import AddToAlbumModal from "./modal/AddToAlbumModal.vue";
|
import AddToAlbumModal from "./modal/AddToAlbumModal.vue";
|
||||||
|
import MoveToFolderModal from "./modal/MoveToFolderModal.vue";
|
||||||
|
|
||||||
import StarIcon from "vue-material-design-icons/Star.vue";
|
import StarIcon from "vue-material-design-icons/Star.vue";
|
||||||
import DownloadIcon from "vue-material-design-icons/Download.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 MoveIcon from "vue-material-design-icons/ImageMove.vue";
|
||||||
import AlbumsIcon from "vue-material-design-icons/ImageAlbum.vue";
|
import AlbumsIcon from "vue-material-design-icons/ImageAlbum.vue";
|
||||||
import AlbumRemoveIcon from "vue-material-design-icons/BookRemove.vue";
|
import AlbumRemoveIcon from "vue-material-design-icons/BookRemove.vue";
|
||||||
|
import FolderMoveIcon from "vue-material-design-icons/FolderMove.vue";
|
||||||
|
|
||||||
type Selection = Map<number, IPhoto>;
|
type Selection = Map<number, IPhoto>;
|
||||||
|
|
||||||
|
@ -98,6 +101,7 @@ export default defineComponent({
|
||||||
EditExif,
|
EditExif,
|
||||||
FaceMoveModal,
|
FaceMoveModal,
|
||||||
AddToAlbumModal,
|
AddToAlbumModal,
|
||||||
|
MoveToFolderModal,
|
||||||
|
|
||||||
CloseIcon,
|
CloseIcon,
|
||||||
},
|
},
|
||||||
|
@ -185,6 +189,12 @@ export default defineComponent({
|
||||||
callback: this.viewInFolder.bind(this),
|
callback: this.viewInFolder.bind(this),
|
||||||
if: () => this.selection.size === 1 && !this.routeIsAlbum(),
|
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"),
|
name: t("memories", "Add to album"),
|
||||||
icon: AlbumsIcon,
|
icon: AlbumsIcon,
|
||||||
|
@ -800,6 +810,13 @@ export default defineComponent({
|
||||||
(<any>this.$refs.addToAlbumModal).open(Array.from(selection.values()));
|
(<any>this.$refs.addToAlbumModal).open(Array.from(selection.values()));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move selected photos to folder
|
||||||
|
*/
|
||||||
|
async moveToFolder(selection: Selection) {
|
||||||
|
(<any>this.$refs.moveToFolderModal).open(Array.from(selection.values()));
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move selected photos to another person
|
* Move selected photos to another person
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
<template>
|
||||||
|
<Modal @close="close" size="normal" v-if="processing">
|
||||||
|
<template #title>
|
||||||
|
{{ t("memories", "Move to folder") }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="outer">
|
||||||
|
{{
|
||||||
|
t("memories", "Processing … {n}/{m}", {
|
||||||
|
n: photosDone,
|
||||||
|
m: photos.length,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "vue";
|
||||||
|
|
||||||
|
import * as dav from "../../services/DavRequests";
|
||||||
|
import { getFilePickerBuilder, FilePickerType } from "@nextcloud/dialogs";
|
||||||
|
import { showInfo } from "@nextcloud/dialogs";
|
||||||
|
import { IPhoto } from "../../types";
|
||||||
|
|
||||||
|
import Modal from "./Modal.vue";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: "MoveToFolderModal",
|
||||||
|
components: {
|
||||||
|
Modal,
|
||||||
|
},
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
photos: [] as IPhoto[],
|
||||||
|
photosDone: 0,
|
||||||
|
processing: false,
|
||||||
|
}),
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
open(photos: IPhoto[]) {
|
||||||
|
this.photosDone = 0;
|
||||||
|
this.processing = false;
|
||||||
|
this.photos = photos;
|
||||||
|
|
||||||
|
this.chooseFolderPath();
|
||||||
|
},
|
||||||
|
|
||||||
|
moved(photos: IPhoto[]) {
|
||||||
|
this.$emit("moved", photos);
|
||||||
|
},
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.photos = [];
|
||||||
|
this.processing = false;
|
||||||
|
this.$emit("close");
|
||||||
|
},
|
||||||
|
|
||||||
|
async chooseFolderModal(title: string, initial: string) {
|
||||||
|
const picker = getFilePickerBuilder(title)
|
||||||
|
.setMultiSelect(false)
|
||||||
|
.setModal(false)
|
||||||
|
.setType(FilePickerType.Move)
|
||||||
|
.addMimeTypeFilter("httpd/unix-directory")
|
||||||
|
.allowDirectories()
|
||||||
|
.startAt(initial)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return await picker.pick();
|
||||||
|
},
|
||||||
|
|
||||||
|
async chooseFolderPath() {
|
||||||
|
let destination = await this.chooseFolderModal(
|
||||||
|
this.t("memories", "Choose a folder"),
|
||||||
|
this.config_foldersPath
|
||||||
|
);
|
||||||
|
// Fails if the target exists, same behavior with Nextcloud files implementation.
|
||||||
|
const gen = dav.movePhotos(this.photos, destination, false);
|
||||||
|
this.processing = true;
|
||||||
|
|
||||||
|
for await (const fids of gen) {
|
||||||
|
this.photosDone += fids.filter((f) => f).length;
|
||||||
|
this.moved(this.photos.filter((p) => fids.includes(p.fileid)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const n = this.photosDone;
|
||||||
|
showInfo(
|
||||||
|
this.n(
|
||||||
|
"memories",
|
||||||
|
"{n} item moved to folder",
|
||||||
|
"{n} items moved to folder",
|
||||||
|
n,
|
||||||
|
{ n }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
this.close();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.outer {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -190,19 +190,12 @@ export async function* runInParallel<T>(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
* @param photos list of photos to search for live photos
|
||||||
* @returns list of file ids that were deleted
|
* @returns list of file ids that contains extra file Ids for live photos if any
|
||||||
*/
|
*/
|
||||||
export async function* deletePhotos(photos: IPhoto[]) {
|
async function extendWithLivePhotos(photos: IPhoto[]) {
|
||||||
if (photos.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileIdsSet = new Set(photos.map((p) => p.fileid));
|
|
||||||
|
|
||||||
// Get live photo data
|
|
||||||
const livePhotos = (
|
const livePhotos = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
photos
|
photos
|
||||||
|
@ -212,7 +205,6 @@ export async function* deletePhotos(photos: IPhoto[]) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(url);
|
const response = await axios.get(url);
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
fileIdsSet.add(data.fileid);
|
|
||||||
return {
|
return {
|
||||||
fileid: data.fileid,
|
fileid: data.fileid,
|
||||||
} as IPhoto;
|
} as IPhoto;
|
||||||
|
@ -224,12 +216,29 @@ export async function* deletePhotos(photos: IPhoto[]) {
|
||||||
)
|
)
|
||||||
).filter((p) => p !== null) as 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
|
// Get files data
|
||||||
let fileInfos: IFileInfo[] = [];
|
let fileInfos: IFileInfo[] = [];
|
||||||
try {
|
try {
|
||||||
fileInfos = await getFiles(photos.concat(livePhotos));
|
fileInfos = await getFiles(photosWithLive);
|
||||||
} catch (e) {
|
} 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."));
|
showError(t("memories", "Failed to delete files."));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -253,3 +262,70 @@ export async function* deletePhotos(photos: IPhoto[]) {
|
||||||
|
|
||||||
yield* runInParallel(calls, 10);
|
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);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue