Merge branch 'move_to_folder' of https://github.com/ahaltindis/memories into ahaltindis-move_to_folder
commit
ee3f9d2670
|
@ -44,6 +44,7 @@
|
|||
:updateLoading="updateLoading"
|
||||
/>
|
||||
<AddToAlbumModal ref="addToAlbumModal" @added="clearSelection" />
|
||||
<MoveToFolderModal ref="moveToFolderModal" @moved="refresh" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -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<number, IPhoto>;
|
||||
|
||||
|
@ -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({
|
|||
(<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
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
<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
|
||||
* @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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue