From d5a9350a4ddf8847d048128833a8a100ec2d3eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=B1=CE=B5=D1=8F=D1=8F=CE=B5?= <47398145+AiroPi@users.noreply.github.com> Date: Thu, 11 Apr 2024 18:51:33 -0700 Subject: [PATCH 1/8] change chooseNcFolder "type" argument to "buttonFactory" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "type" is deprecated, and using "buttonFactory" allow to add more than one button the the modal. Signed-off-by: Pıεяяε <47398145+AiroPi@users.noreply.github.com> --- src/components/modal/MoveToFolderModal.vue | 15 ++++++++++++- src/services/utils/dialog.ts | 26 ++++++++++++++++++---- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/components/modal/MoveToFolderModal.vue b/src/components/modal/MoveToFolderModal.vue index 7c5698b0c..989db385a 100644 --- a/src/components/modal/MoveToFolderModal.vue +++ b/src/components/modal/MoveToFolderModal.vue @@ -26,6 +26,7 @@ import ModalMixin from './ModalMixin'; import * as dav from '@services/dav'; import * as utils from '@services/utils'; +import type { Node } from '@nextcloud/files'; import type { IPhoto } from '@typings'; export default defineComponent({ @@ -61,11 +62,23 @@ export default defineComponent({ }, async chooseFolderPath() { + let moveByDate = false; let destination = await utils.chooseNcFolder( this.t('memories', 'Choose a folder'), this.config.folders_path, - FilePickerType.Move, + (selectedNodes: Node[], currentPath: string, currentView: string) => [ + { + label: 'Move', + type: 'primary', + callback: (_) => null, + }, + { + label: 'Move and organize', + callback: (_) => (moveByDate = true), + }, + ], ); + // Fails if the target exists, same behavior with Nextcloud files implementation. const gen = dav.movePhotos(this.photos, destination, false); this.show = true; diff --git a/src/services/utils/dialog.ts b/src/services/utils/dialog.ts index 111dfb8b0..7c1f6cf67 100644 --- a/src/services/utils/dialog.ts +++ b/src/services/utils/dialog.ts @@ -1,5 +1,7 @@ -import { FilePickerType, getFilePickerBuilder } from '@nextcloud/dialogs'; +import { getFilePickerBuilder } from '@nextcloud/dialogs'; import { showError } from '@nextcloud/dialogs'; +import type { IFilePickerButton } from '@nextcloud/dialogs'; +import type { Node } from '@nextcloud/files'; import { translate as t, translatePlural as n } from '@services/l10n'; import { bus } from './event-bus'; @@ -8,6 +10,12 @@ import { fragment } from './fragment'; // https://github.com/nextcloud/server/blob/4b7ec0a0c18d4e2007565dc28ee214814940161e/core/src/OC/dialogs.js const oc_dialogs = (OC).dialogs; +type IFilePickerButtonFactory = ( + selectedNodes: Node[], + currentPath: string, + currentView: string, +) => IFilePickerButton[]; + type ConfirmOptions = { /** Title of dialog */ title?: string; @@ -154,18 +162,28 @@ export async function prompt(opts: PromptOptions): Promise { * * @param title Title of the file picker * @param initial Initial path - * @param type Type of the file picker + * @param buttonFactory Buttons factory * * @returns The path of the chosen folder */ export async function chooseNcFolder( title: string, initial: string = '/', - type: FilePickerType = FilePickerType.Choose, + buttonFactory: IFilePickerButtonFactory = (i, _) => { + const r = i?.[0]?.attributes?.displayName || i?.[0]?.basename; + let a = i.length === 1 ? t('memories', 'Choose {file}', { file: r }) : t('memories', 'Choose'); + return [ + { + callback: () => {}, + type: 'primary', + label: a, + }, + ]; + }, ): Promise { const picker = getFilePickerBuilder(title) .setMultiSelect(false) - .setType(type) + .setButtonFactory(buttonFactory) .addMimeTypeFilter('httpd/unix-directory') .allowDirectories() .startAt(initial) From 8a9f204c894f7cbb534574a4921db30bb2fa953a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=B1=CE=B5=D1=8F=D1=8F=CE=B5?= <47398145+AiroPi@users.noreply.github.com> Date: Fri, 12 Apr 2024 08:04:34 -0700 Subject: [PATCH 2/8] allow multiple path move MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Pıεяяε <47398145+AiroPi@users.noreply.github.com> --- src/components/modal/MoveToFolderModal.vue | 38 +++++++++---- src/services/dav/base.ts | 63 ++++++++++++++++------ src/services/utils/dialog.ts | 24 +++++---- 3 files changed, 88 insertions(+), 37 deletions(-) diff --git a/src/components/modal/MoveToFolderModal.vue b/src/components/modal/MoveToFolderModal.vue index 989db385a..de4e21937 100644 --- a/src/components/modal/MoveToFolderModal.vue +++ b/src/components/modal/MoveToFolderModal.vue @@ -66,26 +66,44 @@ export default defineComponent({ let destination = await utils.chooseNcFolder( this.t('memories', 'Choose a folder'), this.config.folders_path, - (selectedNodes: Node[], currentPath: string, currentView: string) => [ + () => [ { label: 'Move', type: 'primary', - callback: (_) => null, + callback: () => null, }, { label: 'Move and organize', - callback: (_) => (moveByDate = true), + callback: () => (moveByDate = true), }, ], ); - // Fails if the target exists, same behavior with Nextcloud files implementation. - const gen = dav.movePhotos(this.photos, destination, false); - this.show = true; - - for await (const fids of gen) { - this.photosDone += fids.filter(Boolean).length; - utils.bus.emit('memories:timeline:soft-refresh', null); + if (moveByDate) { + const grouped: Map = new Map(); + for (const photo of this.photos) { + const date = utils.dayIdToDate(photo.dayid); + const datePath = `/${date.getFullYear()}/${date.getMonth() + 1}`; + if (grouped.has(datePath)) { + grouped.get(datePath)?.push(photo); + } else { + grouped.set(datePath, [photo]); + } + } + + for (const group of grouped) { + + console.log(`Move ${group[1].length} photos to ${destination}${group[0]}`); + } + } else { + // Fails if the target exists, same behavior with Nextcloud files implementation. + const gen = dav.movePhotos(this.photos, destination, false); + this.show = true; + + for await (const fids of gen) { + this.photosDone += fids.filter(Boolean).length; + utils.bus.emit('memories:timeline:soft-refresh', null); + } } const n = this.photosDone; diff --git a/src/services/dav/base.ts b/src/services/dav/base.ts index fb632e766..9cf136a09 100644 --- a/src/services/dav/base.ts +++ b/src/services/dav/base.ts @@ -1,13 +1,13 @@ -import { showError } from '@nextcloud/dialogs'; import axios from '@nextcloud/axios'; +import { showError } from '@nextcloud/dialogs'; import { getAlbumFileInfos } from './albums'; import client, { remotePath } from './client'; +import * as nativex from '@native'; import { API } from '@services/API'; import { translate as t } from '@services/l10n'; import * as utils from '@services/utils'; -import * as nativex from '@native'; import type { IFileInfo, IImageInfo, IPhoto } from '@typings'; import type { ResponseDataDetailed, SearchResult } from 'webdav'; @@ -298,30 +298,61 @@ export async function* movePhotos(photos: IPhoto[], destination: string, overwri return; } - // Set absolute target path - const prefixPath = `files/${utils.uid}`; - let targetPath = prefixPath + destination; - if (!targetPath.endsWith('/')) { - targetPath += '/'; + const destinations = new Map(); + destinations.set(destination, photos); + yield* movePhotosToMultiplePaths(destinations, overwrite) +} + +/** + * Move multiple files in given lists of Ids to corresponding destinations + * + * @param destinations 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* movePhotosToMultiplePaths(destinations: Map, overwrite: boolean) { + const filteredEntries = Array.from(destinations.entries()).filter(([_, value]) => { + return value.length > 0; + }); + if (filteredEntries.length === 0) { + return; } - // Also move the stack files - photos = await extendWithStack(photos); - const fileIdsSet = new Set(photos.map((p) => p.fileid)); + const prefixPath = `files/${utils.uid}`; - // Get files data - let fileInfos: IFileInfo[] = []; + let fileInfosByPath: [string, IFileInfo[]][]; try { - fileInfos = await getFiles(photos); + fileInfosByPath = await Promise.all(filteredEntries.map(async ([path, photos]) => { + let targetPath = prefixPath + path; + if (!targetPath.endsWith('/')) { + targetPath += '/'; + } + + photos = await extendWithStack(photos); + // Also move the stack files + const fileIdsSet = new Set(photos.map((p) => p.fileid)); + + // Get files data + let fileInfos: IFileInfo[] = []; + + // This can thrown some errors + fileInfos = await getFiles(photos); + fileInfos = fileInfos.filter((f) => fileIdsSet.has(f.fileid)); + + return [targetPath, fileInfos] + })) } catch (e) { - console.error('Failed to get file info for files to move', photos, e); + console.error('Failed to get file info for files to move', filteredEntries, e); showError(t('memories', 'Failed to move files.')); return; } + let flattened: [string, IFileInfo][] = fileInfosByPath.flatMap(([key, values]) => + values.map(value => [key, value] as [string, IFileInfo]) + ); + // Move each file - fileInfos = fileInfos.filter((f) => fileIdsSet.has(f.fileid)); - const calls = fileInfos.map((fileInfo) => async () => { + const calls = flattened.map(([targetPath, fileInfo]) => async () => { try { await client.moveFile( fileInfo.originalFilename, diff --git a/src/services/utils/dialog.ts b/src/services/utils/dialog.ts index 7c1f6cf67..869b95a49 100644 --- a/src/services/utils/dialog.ts +++ b/src/services/utils/dialog.ts @@ -157,6 +157,18 @@ export async function prompt(opts: PromptOptions): Promise { ); } +function chooseButtonFactory(nodes: Node[]): IFilePickerButton[] { + const fileName = nodes?.[0]?.attributes?.displayName || nodes?.[0]?.basename; + let label = nodes.length === 1 ? t('memories', 'Choose {file}', { file: fileName }) : t('memories', 'Choose'); + return [ + { + callback: () => {}, + type: 'primary', + label: label, + }, + ]; +} + /** * Choose a folder using the NC file picker * @@ -169,17 +181,7 @@ export async function prompt(opts: PromptOptions): Promise { export async function chooseNcFolder( title: string, initial: string = '/', - buttonFactory: IFilePickerButtonFactory = (i, _) => { - const r = i?.[0]?.attributes?.displayName || i?.[0]?.basename; - let a = i.length === 1 ? t('memories', 'Choose {file}', { file: r }) : t('memories', 'Choose'); - return [ - { - callback: () => {}, - type: 'primary', - label: a, - }, - ]; - }, + buttonFactory: IFilePickerButtonFactory = chooseButtonFactory, ): Promise { const picker = getFilePickerBuilder(title) .setMultiSelect(false) From 2ffa384f62e0daa85eb85ed7352d10fba54de9eb Mon Sep 17 00:00:00 2001 From: "airo.pi_" <47398145+AiroPi@users.noreply.github.com> Date: Sun, 14 Apr 2024 21:31:49 +0200 Subject: [PATCH 3/8] first implementation of organized move Signed-off-by: airo.pi_ <47398145+AiroPi@users.noreply.github.com> --- src/components/modal/MoveToFolderModal.vue | 61 ++++----- src/services/dav/base.ts | 144 ++++++++++++++++----- 2 files changed, 140 insertions(+), 65 deletions(-) diff --git a/src/components/modal/MoveToFolderModal.vue b/src/components/modal/MoveToFolderModal.vue index de4e21937..691191c96 100644 --- a/src/components/modal/MoveToFolderModal.vue +++ b/src/components/modal/MoveToFolderModal.vue @@ -13,7 +13,6 @@