Skip to content

Commit

Permalink
Merge branch 'move_to_folder' of https://github.com/ahaltindis/memories
Browse files Browse the repository at this point in the history
… into ahaltindis-move_to_folder
  • Loading branch information
pulsejet committed Jan 18, 2023
2 parents b18a098 + cdcf489 commit ee3f9d2
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 14 deletions.
17 changes: 17 additions & 0 deletions src/components/SelectionManager.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
:updateLoading="updateLoading"
/>
<AddToAlbumModal ref="addToAlbumModal" @added="clearSelection" />
<MoveToFolderModal ref="moveToFolderModal" @moved="refresh" />
</div>
</template>

Expand Down Expand Up @@ -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";
Expand All @@ -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>;
Expand All @@ -98,6 +101,7 @@ export default defineComponent({
EditExif,
FaceMoveModal,
AddToAlbumModal,
MoveToFolderModal,
CloseIcon,
},
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
*/
Expand Down
107 changes: 107 additions & 0 deletions src/components/modal/MoveToFolderModal.vue
Original file line number Diff line number Diff line change
@@ -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>
104 changes: 90 additions & 14 deletions src/services/dav/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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;
}
Expand All @@ -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);
}

0 comments on commit ee3f9d2

Please sign in to comment.