diff --git a/src/components/Photo.vue b/src/components/Photo.vue index f04ef429..15153273 100644 --- a/src/components/Photo.vue +++ b/src/components/Photo.vue @@ -224,7 +224,7 @@ export default class Photo extends Mixins(GlobalMixin) { &.exit-left { transition: all 0.2s ease-in; transform: translateX(-20%); - opacity: 0.4; + opacity: 0.8; } &.enter-right { animation: enter-right 0.2s ease-out forwards; @@ -232,7 +232,7 @@ export default class Photo extends Mixins(GlobalMixin) { } @keyframes enter-right { - from { transform: translateX(20%); opacity: 0.4; } + from { transform: translateX(20%); opacity: 0.8; } to { transform: translateX(0); opacity: 1; } } diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue index 95c9982a..35c254c5 100644 --- a/src/components/Timeline.vue +++ b/src/components/Timeline.vue @@ -870,14 +870,13 @@ export default class Timeline extends Mixins(GlobalMixin) { } /** Clear all selected photos */ - clearSelection() { + clearSelection(only?: Set) { const heads = new Set(); - for (const photo of this.selection) { + new Set(only || this.selection).forEach((photo: IPhoto) => { photo.flag &= ~this.c.FLAG_SELECTED; heads.add(this.heads[photo.d.dayid]); - } - - this.selection.clear(); + this.selection.delete(photo); + }); heads.forEach(this.updateHeadSelected); this.$forceUpdate(); } @@ -935,11 +934,12 @@ export default class Timeline extends Mixins(GlobalMixin) { this.loading = true; const list = [...this.selection]; - const delIds = await dav.deleteFilesByIds(list.map(p => p.fileid)); + for await (const delIds of dav.deleteFilesByIds(list.map(p => p.fileid))) { + const delIdsSet = new Set(delIds.filter(i => i)); + const updatedDays = new Set(list.filter(f => delIdsSet.has(f.fileid)).map(f => f.d)); + await this.deleteFromViewWithAnimation(delIdsSet, updatedDays); + } this.loading = false; - - const updatedDays = new Set(list.filter(f => delIds.has(f.fileid)).map(f => f.d)); - await this.deleteFromViewWithAnimation(delIds, updatedDays); } /** @@ -959,12 +959,16 @@ export default class Timeline extends Mixins(GlobalMixin) { return; } + // Set of photos that are being deleted + const delPhotos = new Set(); + // Animate the deletion for (const day of updatedDays) { for (const row of day.rows) { for (const photo of row.photos) { if (delIds.has(photo.fileid)) { photo.flag |= this.c.FLAG_LEAVING; + delPhotos.add(photo); } } } @@ -973,8 +977,11 @@ export default class Timeline extends Mixins(GlobalMixin) { // wait for 200ms await new Promise(resolve => setTimeout(resolve, 200)); + // clear selection at this point + this.clearSelection(delPhotos); + // Speculate day reflow for animation - const exitedLeft = new Set(); + const exitedLeft = new Set(); for (const day of updatedDays) { let nextExit = false; for (const row of day.rows) { @@ -1005,9 +1012,6 @@ export default class Timeline extends Mixins(GlobalMixin) { photo.flag |= this.c.FLAG_ENTER_RIGHT; }); - // clear selection at this point - this.clearSelection(); - // wait for 200ms await new Promise(resolve => setTimeout(resolve, 200)); diff --git a/src/services/DavRequests.ts b/src/services/DavRequests.ts index 7cb60520..3c82311b 100644 --- a/src/services/DavRequests.ts +++ b/src/services/DavRequests.ts @@ -150,6 +150,20 @@ export async function getFolderPreviewFileIds(folderPath: string, limit: number) })); } +/** + * Run promises in parallel, but only n at a time + * @param promises Array of promise generator funnction (async functions) + * @param n Number of promises to run in parallel + */ +export async function* runInParallel(promises: (() => Promise)[], n: number) { + while (promises.length > 0) { + const promisesToRun = promises.splice(0, n); + const resultsForThisBatch = await Promise.all(promisesToRun.map(p => p())); + yield resultsForThisBatch; + } + return; +} + /** * Delete a single file * @@ -166,12 +180,11 @@ export async function deleteFile(path: string) { * @param fileIds list of file ids * @returns list of file ids that were deleted */ -export async function deleteFilesByIds(fileIds: number[]) { - const delIds = new Set(); +export async function* deleteFilesByIds(fileIds: number[]) { const fileIdsSet = new Set(fileIds); if (fileIds.length === 0) { - return delIds; + return; } // Get files data @@ -180,31 +193,22 @@ export async function deleteFilesByIds(fileIds: number[]) { fileInfos = await getFiles(fileIds.filter(f => f)); } catch (e) { console.error('Failed to get file info for files to delete', fileIds, e); - return delIds; + return; } - // Run all promises together - const promises: Promise[] = []; - // Delete each file - for (const fileInfo of fileInfos) { - if (!fileIdsSet.has(fileInfo.fileid)) { - continue + fileInfos = fileInfos.filter((f) => fileIdsSet.has(f.fileid)); + const calls = fileInfos.map((fileInfo) => async () => { + try { + await deleteFile(fileInfo.filename); + return fileInfo.fileid as number; + } catch { + console.error('Failed to delete', fileInfo.filename) + return 0; } + }); - promises.push((async () => { - try { - await deleteFile(fileInfo.filename); - delIds.add(fileInfo.fileid); - } catch { - console.error('Failed to delete', fileInfo.filename) - } - })()); - } - - await Promise.allSettled(promises); - - return delIds; + yield* runInParallel(calls, 10); }