From 7b39483966b7c5cd9fa43aa573dc2b387ade34a7 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Thu, 22 Dec 2022 15:39:18 +0100 Subject: [PATCH 01/41] server: drive endpoint to fetch files and folders Changelog: Added --- packages/backend/src/server/api/endpoints.ts | 2 + .../src/server/api/endpoints/drive/show.ts | 70 +++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 packages/backend/src/server/api/endpoints/drive/show.ts diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index 6e69b3b11..4f4f2c16f 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -105,6 +105,7 @@ import * as ep___clips_show from './endpoints/clips/show.js'; import * as ep___clips_update from './endpoints/clips/update.js'; import * as ep___drive from './endpoints/drive.js'; import * as ep___drive_files from './endpoints/drive/files.js'; +import * as ep___drive_show from './endpoints/drive/show.js'; import * as ep___drive_files_attachedNotes from './endpoints/drive/files/attached-notes.js'; import * as ep___drive_files_checkExistence from './endpoints/drive/files/check-existence.js'; import * as ep___drive_files_create from './endpoints/drive/files/create.js'; @@ -414,6 +415,7 @@ const eps = [ ['clips/update', ep___clips_update], ['drive', ep___drive], ['drive/files', ep___drive_files], + ['drive/show', ep___drive_show], ['drive/files/attached-notes', ep___drive_files_attachedNotes], ['drive/files/check-existence', ep___drive_files_checkExistence], ['drive/files/create', ep___drive_files_create], diff --git a/packages/backend/src/server/api/endpoints/drive/show.ts b/packages/backend/src/server/api/endpoints/drive/show.ts new file mode 100644 index 000000000..3d88ed5f5 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/drive/show.ts @@ -0,0 +1,70 @@ +import { DriveFiles, DriveFolders } from '@/models/index.js'; +import define from '../../define.js'; +import { makePaginationQuery } from '../../common/make-pagination-query.js'; + +export const meta = { + tags: ['drive'], + + description: "Lists all folders and files in the authenticated user's drive. Folders are always listed first. The limit, if specified, is applied over the total number of elements.", + + requireCredential: true, + + kind: 'read:drive', + + res: { + type: 'array', + optional: false, nullable: false, + items: { + oneOf: [{ + type: 'object', + optional: false, nullable: false, + ref: 'DriveFile', + }, { + type: 'object', + optional: false, nullable: false, + ref: 'DriveFolder', + }], + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 }, + sinceId: { type: 'string', format: 'misskey:id' }, + untilId: { type: 'string', format: 'misskey:id' }, + folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null }, + }, + required: [], +} as const; + +// eslint-disable-next-line import/no-default-export +export default define(meta, paramDef, async (ps, user) => { + const foldersQuery = makePaginationQuery(DriveFolders.createQueryBuilder('folder'), ps.sinceId, ps.untilId) + .andWhere('folder.userId = :userId', { userId: user.id }); + const filesQuery = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId) + .andWhere('file.userId = :userId', { userId: user.id }); + + if (ps.folderId) { + foldersQuery.andWhere('folder.parentId = :parentId', { parentId: ps.folderId }); + filesQuery.andWhere('file.folderId = :folderId', { folderId: ps.folderId }); + } else { + foldersQuery.andWhere('folder.parentId IS NULL'); + filesQuery.andWhere('file.folderId IS NULL'); + } + + const folders = await foldersQuery.take(ps.limit).getMany(); + + const [files, ...packedFolders] = await Promise.all([ + filesQuery.take(ps.limit - folders.length).getMany(), + ...(folders.map(folder => DriveFolders.pack(folder))), + ]); + + const packedFiles = await DriveFiles.packMany(files, { detail: false, self: true }); + + return [ + ...packedFolders, + ...packedFiles, + ]; +}); From 240cf9892079b5d834e123184501ae7282a1aef9 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Thu, 22 Dec 2022 17:06:52 +0100 Subject: [PATCH 02/41] client: refactor drive drag&drop --- packages/client/src/components/drive.vue | 27 +++++++++--------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/packages/client/src/components/drive.vue b/packages/client/src/components/drive.vue index c01c328af..adce71b79 100644 --- a/packages/client/src/components/drive.vue +++ b/packages/client/src/components/drive.vue @@ -210,9 +210,8 @@ function onStreamDriveFolderDeleted(folderId: string) { function onDragover(ev: DragEvent): any { if (!ev.dataTransfer) return; - // ドラッグ元が自分自身の所有するアイテムだったら if (isDragSource) { - // 自分自身にはドロップさせない + // We are the drag source, do not allow to drop. ev.dataTransfer.dropEffect = 'none'; return; } @@ -257,17 +256,16 @@ function onDrop(ev: DragEvent): any { if (!ev.dataTransfer) return; - // ドロップされてきたものがファイルだったら + const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); + const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_); + if (ev.dataTransfer.files.length > 0) { + // dropping operating system files for (const file of Array.from(ev.dataTransfer.files)) { upload(file, folder); } - return; - } - - //#region ドライブのファイル - const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_); - if (driveFile != null && driveFile !== '') { + } else if (driveFile != null && driveFile !== '') { + // dropping drive files const file = JSON.parse(driveFile); // cannot move file within parent folder @@ -278,18 +276,14 @@ function onDrop(ev: DragEvent): any { fileId: file.id, folderId: folder?.id ?? null, }); - } - //#endregion - - //#region ドライブのフォルダ - const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_); - if (driveFolder != null && driveFolder !== '') { + } else if (driveFolder != null && driveFolder !== '') { + // dropping drive folders const droppedFolder = JSON.parse(driveFolder); // cannot move folder into itself if (droppedFolder.id === folder?.id) return false; // cannot move folder within parent folder - if (foldersPaginationElem.items.some(f => f.id === droppedFolder.id)) return false; + if (folder.id === droppedFolder.parentId) return false; removeFolder(droppedFolder.id); os.api('drive/folders/update', { @@ -311,7 +305,6 @@ function onDrop(ev: DragEvent): any { } }); } - //#endregion } function selectLocalFile() { From d96070bc8057a0a3988af87309a5e340ef78bd76 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Thu, 22 Dec 2022 17:32:05 +0100 Subject: [PATCH 03/41] client: fix duplicate folder when creating new folder --- packages/client/src/components/drive.vue | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/client/src/components/drive.vue b/packages/client/src/components/drive.vue index adce71b79..02d46f185 100644 --- a/packages/client/src/components/drive.vue +++ b/packages/client/src/components/drive.vue @@ -339,8 +339,6 @@ function createFolder() { os.api('drive/folders/create', { name, parentId: folder?.id ?? undefined, - }).then(createdFolder => { - addFolder(createdFolder, true); }); }); } From c983c4860c5257694360181b72e07f66ad86f562 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Thu, 22 Dec 2022 17:55:13 +0100 Subject: [PATCH 04/41] client: use combined drive endpoint --- packages/client/src/components/drive.vue | 172 ++++++++--------------- 1 file changed, 55 insertions(+), 117 deletions(-) diff --git a/packages/client/src/components/drive.vue b/packages/client/src/components/drive.vue index 02d46f185..10f0be183 100644 --- a/packages/client/src/components/drive.vue +++ b/packages/client/src/components/drive.vue @@ -35,27 +35,35 @@ @drop.prevent.stop="onDrop" @contextmenu.stop="onContextmenu" > -
- - + + - +
@@ -145,13 +116,7 @@ const emit = defineEmits<{ (ev: 'open-folder', v: foundkey.entities.DriveFolder): void; }>(); -let foldersPaginationElem = $ref>(); -let filesPaginationElem = $ref>(); - -let foldersLoading = $ref(true); -let filesLoading = $ref(true); -const empty = $computed(() => !foldersLoading && !filesLoading - && foldersPaginationElem?.items.length === 0 && filesPaginationElem?.items.length === 0); +let paginationElem = $ref>(); let fileInput = $ref(); @@ -434,10 +399,6 @@ function chooseFolder(folderToChoose: foundkey.entities.DriveFolder) { } function move(target?: string | foundkey.entities.DriveFolder) { - // reset loading state - foldersLoading = true; - filesLoading = true; - if (!target) { goRoot(); return; @@ -466,13 +427,13 @@ function addFolder(folderToAdd: foundkey.entities.DriveFolder, unshift = false) const current = folder?.id ?? null; if (current !== folderToAdd.parentId) return; - const exist = foldersPaginationElem.items.some(f => f.id === folderToAdd.id); + const exist = paginationElem.items.some(f => f.id === folderToAdd.id); if (exist) { - foldersPaginationElem.updateItem(folderToAdd.id, () => folderToAdd); + paginationElem.updateItem(folderToAdd.id, () => folderToAdd); } else if (unshift) { - foldersPaginationElem.prepend(folderToAdd); + paginationElem.prepend(folderToAdd); } else { - foldersPaginationElem.append(folderToAdd); + paginationElem.append(folderToAdd); } } @@ -480,24 +441,24 @@ function addFile(fileToAdd: foundkey.entities.DriveFile, unshift = false) { const current = folder?.id ?? null; if (current !== fileToAdd.folderId) return; - const exist = filesPaginationElem.items.some(f => f.id === fileToAdd.id); + const exist = paginationElem.items.some(f => f.id === fileToAdd.id); if (exist) { - filesPaginationElem.updateItem(fileToAdd.id, () => fileToAdd); + paginationElem.updateItem(fileToAdd.id, () => fileToAdd); } else if (unshift) { - filesPaginationElem.prepend(fileToAdd); + paginationElem.prepend(fileToAdd); } else { - filesPaginationElem.append(fileToAdd); + paginationElem.append(fileToAdd); } } function removeFolder(folderToRemove: foundkey.entities.DriveFolder | string): void { const folderIdToRemove = typeof folderToRemove === 'object' ? folderToRemove.id : folderToRemove; - foldersPaginationElem.removeItem(item => item.id === folderIdToRemove); + paginationElem.removeItem(item => item.id === folderIdToRemove); } function removeFile(fileToRemove: foundkey.entities.DriveFile | string): void { const fileIdToRemove = typeof fileToRemove === 'object' ? fileToRemove.id : fileToRemove; - filesPaginationElem.removeItem(item => item.id === fileIdToRemove); + paginationElem.removeItem(item => item.id === fileIdToRemove); } function goRoot() { @@ -509,23 +470,14 @@ function goRoot() { emit('move-root'); } -const foldersPagination = { - endpoint: 'drive/folders' as const, +const pagination = { + endpoint: 'drive/show' as const, limit: 30, params: computed(() => ({ folderId: folder?.id ?? null, })), }; -const filesPagination = { - endpoint: 'drive/files' as const, - limit: 30, - params: computed(() => ({ - folderId: folder?.id ?? null, - type: props.type, - })), -}; - function getMenu() { return [{ type: 'switch', @@ -669,37 +621,23 @@ onBeforeUnmount(() => { } > .contents { + display: flex; + flex-wrap: wrap; - > .folders, - > .files { - display: flex; - flex-wrap: wrap; - - > .folder, - > .file { - flex-grow: 1; - width: 128px; - margin: 4px; - box-sizing: border-box; - } - - > .padding { - flex-grow: 1; - pointer-events: none; - width: 128px + 8px; - } + > .item { + flex-grow: 1; + width: 128px; + margin: 4px; + box-sizing: border-box; } + } - > .empty { - padding: 16px; - text-align: center; - pointer-events: none; - opacity: 0.5; - - > p { - margin: 0; - } - } + > .empty { + padding: 16px; + text-align: center; + pointer-events: none; + opacity: 0.5; + margin: 0; } } From f7c4107ca4076e9a838991c43c013d0d2f630118 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Thu, 22 Dec 2022 18:55:06 +0100 Subject: [PATCH 05/41] client: drive uses grid instead of flexbox --- packages/client/src/components/drive.vue | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/packages/client/src/components/drive.vue b/packages/client/src/components/drive.vue index 10f0be183..e4611a2f8 100644 --- a/packages/client/src/components/drive.vue +++ b/packages/client/src/components/drive.vue @@ -51,7 +51,6 @@ v-if="'size' in f" :key="f.id" v-anim="i" - class="item" :file="f" :select-mode="select === 'file'" :is-selected="selectedFiles.some(x => x.id === f.id)" @@ -63,7 +62,6 @@ v-else :key="f.id" v-anim="i" - class="item" :folder="f" :select-mode="select === 'folder'" :is-selected="selectedFolders.some(x => x.id === f.id)" @@ -621,15 +619,9 @@ onBeforeUnmount(() => { } > .contents { - display: flex; - flex-wrap: wrap; - - > .item { - flex-grow: 1; - width: 128px; - margin: 4px; - box-sizing: border-box; - } + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: .5em; } > .empty { From df9064c284f06d331d57a75998178853d3707546 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Fri, 23 Dec 2022 02:02:20 +0100 Subject: [PATCH 06/41] client: remove driveFolderBg theme color Changelog: Removed --- packages/client/src/components/drive.folder.vue | 1 - packages/client/src/components/drive.vue | 2 -- packages/client/src/themes/_dark.json5 | 1 - packages/client/src/themes/_light.json5 | 1 - packages/client/src/themes/d-astro.json5 | 1 - packages/client/src/themes/d-pumpkin.json5 | 1 - packages/client/src/themes/l-vivid.json5 | 1 - 7 files changed, 8 deletions(-) diff --git a/packages/client/src/components/drive.folder.vue b/packages/client/src/components/drive.folder.vue index 028dc7f2b..1584b15c0 100644 --- a/packages/client/src/components/drive.folder.vue +++ b/packages/client/src/components/drive.folder.vue @@ -261,7 +261,6 @@ function onContextmenu(ev: MouseEvent) { position: relative; padding: 8px; height: 64px; - background: var(--driveFolderBg); border-radius: 4px; &, * { diff --git a/packages/client/src/components/drive.vue b/packages/client/src/components/drive.vue index e4611a2f8..1ca4f8c57 100644 --- a/packages/client/src/components/drive.vue +++ b/packages/client/src/components/drive.vue @@ -52,7 +52,6 @@ :key="f.id" v-anim="i" :file="f" - :select-mode="select === 'file'" :is-selected="selectedFiles.some(x => x.id === f.id)" @chosen="chooseFile" @dragstart="isDragSource = true" @@ -63,7 +62,6 @@ :key="f.id" v-anim="i" :folder="f" - :select-mode="select === 'folder'" :is-selected="selectedFolders.some(x => x.id === f.id)" @chosen="chooseFolder" @move="move" diff --git a/packages/client/src/themes/_dark.json5 b/packages/client/src/themes/_dark.json5 index cda66a508..a4c51819b 100644 --- a/packages/client/src/themes/_dark.json5 +++ b/packages/client/src/themes/_dark.json5 @@ -64,7 +64,6 @@ inputBorder: 'rgba(255, 255, 255, 0.1)', inputBorderHover: 'rgba(255, 255, 255, 0.2)', listItemHoverBg: 'rgba(255, 255, 255, 0.03)', - driveFolderBg: ':alpha<0.3<@accent', wallpaperOverlay: 'rgba(0, 0, 0, 0.5)', badge: '#31b1ce', messageBg: '@bg', diff --git a/packages/client/src/themes/_light.json5 b/packages/client/src/themes/_light.json5 index 50c5388e0..1aae2b8cf 100644 --- a/packages/client/src/themes/_light.json5 +++ b/packages/client/src/themes/_light.json5 @@ -64,7 +64,6 @@ inputBorder: 'rgba(0, 0, 0, 0.1)', inputBorderHover: 'rgba(0, 0, 0, 0.2)', listItemHoverBg: 'rgba(0, 0, 0, 0.03)', - driveFolderBg: ':alpha<0.3<@accent', wallpaperOverlay: 'rgba(255, 255, 255, 0.5)', badge: '#31b1ce', messageBg: '@bg', diff --git a/packages/client/src/themes/d-astro.json5 b/packages/client/src/themes/d-astro.json5 index 7b9034020..873636981 100644 --- a/packages/client/src/themes/d-astro.json5 +++ b/packages/client/src/themes/d-astro.json5 @@ -46,7 +46,6 @@ buttonHoverBg: 'rgba(255, 255, 255, 0.1)', buttonGradateA: '@accent', buttonGradateB: ':hue<-20<@accent', - driveFolderBg: ':alpha<0.3<@accent', fgHighlighted: ':lighten<3<@fg', panelHeaderBg: ':lighten<3<@panel', panelHeaderFg: '@fg', diff --git a/packages/client/src/themes/d-pumpkin.json5 b/packages/client/src/themes/d-pumpkin.json5 index ff2bc9509..e466eb688 100644 --- a/packages/client/src/themes/d-pumpkin.json5 +++ b/packages/client/src/themes/d-pumpkin.json5 @@ -66,7 +66,6 @@ navIndicator: '@indicator', accentLighten: ':lighten<10<@accent', buttonHoverBg: 'rgba(255, 255, 255, 0.1)', - driveFolderBg: ':alpha<0.3<@accent', fgHighlighted: ':lighten<3<@fg', fgTransparent: ':alpha<0.5<@fg', panelHeaderBg: ':lighten<3<@panel', diff --git a/packages/client/src/themes/l-vivid.json5 b/packages/client/src/themes/l-vivid.json5 index 66d97f3a0..ede4e0571 100644 --- a/packages/client/src/themes/l-vivid.json5 +++ b/packages/client/src/themes/l-vivid.json5 @@ -47,7 +47,6 @@ navIndicator: '@accent', accentLighten: ':lighten<10<@accent', buttonHoverBg: 'rgba(0, 0, 0, 0.1)', - driveFolderBg: ':alpha<0.3<@accent', fgHighlighted: ':darken<3<@fg', fgTransparent: ':alpha<0.5<@fg', panelHeaderBg: ':lighten<3<@panel', From d26e2588e3f2662a488664d8dac8ffb386f22106 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Fri, 23 Dec 2022 02:20:36 +0100 Subject: [PATCH 07/41] client: combine selection of files & folders --- packages/client/src/components/drive.file.vue | 8 +- .../client/src/components/drive.folder.vue | 100 ++++++++++-------- packages/client/src/components/drive.vue | 86 ++++++++------- 3 files changed, 105 insertions(+), 89 deletions(-) diff --git a/packages/client/src/components/drive.file.vue b/packages/client/src/components/drive.file.vue index 6e3d38612..f6713947e 100644 --- a/packages/client/src/components/drive.file.vue +++ b/packages/client/src/components/drive.file.vue @@ -52,7 +52,7 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (ev: 'chosen', r: foundkey.entities.DriveFile): void; + (ev: 'chosen', r: foundkey.entities.DriveFile, extendSelection: boolean): void; (ev: 'dragstart'): void; (ev: 'dragend'): void; }>(); @@ -95,9 +95,7 @@ function getMenu(): MenuItem[] { function onClick(ev: MouseEvent): void { if (props.selectMode) { - emit('chosen', props.file); - } else { - os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined); + emit('chosen', props.file, ev.ctrlKey); } } @@ -330,7 +328,7 @@ async function deleteFile(): Promise { overflow: hidden; > .ext { - opacity: 0.5; + opacity: 0.7; } } } diff --git a/packages/client/src/components/drive.folder.vue b/packages/client/src/components/drive.folder.vue index 1584b15c0..c613571f5 100644 --- a/packages/client/src/components/drive.folder.vue +++ b/packages/client/src/components/drive.folder.vue @@ -1,10 +1,10 @@ @@ -44,7 +45,7 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (ev: 'chosen', v: foundkey.entities.DriveFolder): void; + (ev: 'chosen', v: foundkey.entities.DriveFolder, extendSelection: boolean): void; (ev: 'move', v: foundkey.entities.DriveFolder): void; (ev: 'upload', file: File, folder: foundkey.entities.DriveFolder); (ev: 'removeFile', v: foundkey.entities.DriveFile['id']): void; @@ -59,20 +60,10 @@ const isDragging = ref(false); const title = computed(() => props.folder.name); -function checkboxClicked() { - emit('chosen', props.folder); -} - -function onClick() { - emit('move', props.folder); -} - -function onMouseover() { - hover.value = true; -} - -function onMouseout() { - hover.value = false; +function selected(ev: MouseEvent) { + if (props.selectMode) { + emit('chosen', props.folder, ev.ctrlKey); + } } function onDragover(ev: DragEvent) { @@ -260,29 +251,34 @@ function onContextmenu(ev: MouseEvent) { .rghtznwe { position: relative; padding: 8px; - height: 64px; - border-radius: 4px; + min-height: 180px; + border-radius: 8px; &, * { cursor: pointer; } - *:not(.checkbox) { - pointer-events: none; - } + > .thumbnail { + width: 110px; + height: 110px; + margin: auto; - > .checkbox { - position: absolute; - bottom: 8px; - right: 8px; - width: 16px; - height: 16px; - background: #fff; - border: solid 1px #000; + /* same style as drive-file-thumbnail.vue */ + position: relative; + display: flex; + background: var(--panel); + border-radius: 8px; + overflow: clip; - &.checked { - background: var(--accent); + > i { + pointer-events: none; + margin: auto; + font-size: 33px; + color: #777; } + + &:not(:hover) > i.hover, + &:hover > i:not(.hover) { display: none; } } &.draghover { @@ -299,23 +295,37 @@ function onContextmenu(ev: MouseEvent) { } } - > .name { - margin: 0; - font-size: 0.9em; - color: var(--desktopDriveFolderFg); + &.isSelected { + background: var(--accent); - > i { - margin-right: 4px; - margin-left: 2px; - text-align: left; + &:hover { + background: var(--accentLighten); } + + > .name { + color: #fff; + } + + > .thumbnail { + color: #fff; + } + } + + > .name { + display: block; + margin: 4px 0 0 0; + font-size: 0.8em; + text-align: center; + word-break: break-all; + color: var(--fg); + overflow: hidden; } > .upload { margin: 4px 4px; font-size: 0.8em; - text-align: right; - color: var(--desktopDriveFolderFg); + text-align: center; + opacity: 0.7; } } diff --git a/packages/client/src/components/drive.vue b/packages/client/src/components/drive.vue index 1ca4f8c57..ec985ea9d 100644 --- a/packages/client/src/components/drive.vue +++ b/packages/client/src/components/drive.vue @@ -52,8 +52,9 @@ :key="f.id" v-anim="i" :file="f" - :is-selected="selectedFiles.some(x => x.id === f.id)" - @chosen="chooseFile" + :select-mode="select !== 'folder'" + :is-selected="selected.some(x => x.id === f.id)" + @chosen="choose" @dragstart="isDragSource = true" @dragend="isDragSource = false" /> @@ -62,8 +63,9 @@ :key="f.id" v-anim="i" :folder="f" - :is-selected="selectedFolders.some(x => x.id === f.id)" - @chosen="chooseFolder" + :select-mode="select !== 'file'" + :is-selected="selected.some(x => x.id === f.id)" + @chosen="choose" @move="move" @upload="upload" @removeFile="removeFile" @@ -106,7 +108,7 @@ const props = withDefaults(defineProps<{ const emit = defineEmits<{ (ev: 'selected', v: foundkey.entities.DriveFile | foundkey.entities.DriveFolder): void; - (ev: 'change-selection', v: foundkey.entities.DriveFile[] | foundkey.entities.DriveFolder[]): void; + (ev: 'change-selection', v: Array): void; (ev: 'move-root'): void; (ev: 'cd', v: foundkey.entities.DriveFolder | null): void; (ev: 'open-folder', v: foundkey.entities.DriveFolder): void; @@ -121,8 +123,7 @@ const connection = stream.useChannel('drive'); let folder = $ref(null); let hierarchyFolders = $ref([]); -let selectedFiles = $ref([]); -let selectedFolders = $ref([]); +let selected = $ref>([]); let keepOriginal = $ref(defaultStore.state.keepOriginalUploading); // ドロップされようとしているか @@ -356,42 +357,49 @@ function upload(file: File, folderToUpload?: foundkey.entities.DriveFolder | nul uploadFile(file, folderToUpload?.id ?? null, undefined, keepOriginal); } -function chooseFile(file: foundkey.entities.DriveFile) { - const isAlreadySelected = selectedFiles.some(f => f.id === file.id); - if (props.multiple) { - if (isAlreadySelected) { - selectedFiles = selectedFiles.filter(f => f.id !== file.id); - } else { - selectedFiles.push(file); - } - emit('change-selection', selectedFiles); - } else { - if (isAlreadySelected) { - emit('selected', file); - } else { - selectedFiles = [file]; - emit('change-selection', [file]); - } - } -} +function choose(choice: foundkey.entities.DriveFile | foundkey.entities.DriveFolder, extendSelection: boolean) { + const alreadySelected = selectedFiles.some(f => f.id === file.id); -function chooseFolder(folderToChoose: foundkey.entities.DriveFolder) { - const isAlreadySelected = selectedFolders.some(f => f.id === folderToChoose.id); - if (props.multiple) { - if (isAlreadySelected) { - selectedFolders = selectedFolders.filter(f => f.id !== folderToChoose.id); + const action = (() => { + if (props.select != null) { + // file picker mode, extendSelection is disregarded + if (props.multiple && alreadySelected) { + return 'remove'; + } else if (props.multiple) { + return 'add'; + } else if (!props.multiple && alreadySelected) { + return 'emit'; + } else { + return 'set'; + } } else { - selectedFolders.push(folderToChoose); - } - emit('change-selection', selectedFolders); - } else { - if (isAlreadySelected) { - emit('selected', folderToChoose); - } else { - selectedFolders = [folderToChoose]; - emit('change-selection', [folderToChoose]); + // explorer mode, props.multiple is disregarded + if (extendSelection && alreadySelected) { + return 'remove'; + } else if (extendSelection) { + return 'add'; + } else if (!alreadySelected) { + return 'set'; + } + // already selected && ! extend selection is a noop } + })(); + + switch (action) { + case 'emit': + emit('selected', choice); + return; // don't emit the change-selection event + case 'add': + selected.push(choice); + break; + case 'set': + selected = [choice]; + break; + case 'remove': + selected = selected.filter(f => f.id !== choice.id); + break; } + emit('change-selection', selected); } function move(target?: string | foundkey.entities.DriveFolder) { From 02aaee6050758dcce7ee9728464056de9b790d35 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Fri, 23 Dec 2022 02:21:52 +0100 Subject: [PATCH 08/41] client: select folder when entering it As a convenience when a user is in the "select a folder" dialog, opens a folder an then clicks on the checkmark, the currently open folder is selected. --- packages/client/src/components/drive.vue | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/client/src/components/drive.vue b/packages/client/src/components/drive.vue index ec985ea9d..3b9ebd85c 100644 --- a/packages/client/src/components/drive.vue +++ b/packages/client/src/components/drive.vue @@ -98,7 +98,6 @@ import { uploadFile, uploads } from '@/scripts/upload'; const props = withDefaults(defineProps<{ initialFolder?: foundkey.entities.DriveFolder; - type?: string; multiple?: boolean; select?: 'file' | 'folder' | null; }>(), { @@ -133,7 +132,14 @@ let draghover = $ref(false); // (自分自身の階層にドロップできないようにするためのフラグ) let isDragSource = $ref(false); -watch($$(folder), () => emit('cd', folder)); +watch($$(folder), () => { + emit('cd', folder) + if (props.select === 'folder') { + // convenience: entering a folder selects it + selected = [folder]; + emit('change-selection', selected); + } +}); function onStreamDriveFileCreated(file: foundkey.entities.DriveFile) { addFile(file, true); From 559a17cf26619c119cf4b84f24ef44aa83f6a22f Mon Sep 17 00:00:00 2001 From: Johann150 Date: Fri, 3 Mar 2023 23:54:42 +0100 Subject: [PATCH 09/41] add FIXME comment --- packages/client/src/components/key-value.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/client/src/components/key-value.vue b/packages/client/src/components/key-value.vue index 2b9c735dd..079c7fbb8 100644 --- a/packages/client/src/components/key-value.vue +++ b/packages/client/src/components/key-value.vue @@ -5,6 +5,7 @@
+
From 5a263ec2c39a357f334d64f9523b054b76028168 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Sat, 4 Mar 2023 00:01:17 +0100 Subject: [PATCH 10/41] client: refactor pleaseLogin to pleaseLoginOrPage --- packages/client/src/nirax.ts | 4 ++-- packages/client/src/scripts/please-login.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/client/src/nirax.ts b/packages/client/src/nirax.ts index 51f1dc20a..795e27eb5 100644 --- a/packages/client/src/nirax.ts +++ b/packages/client/src/nirax.ts @@ -2,7 +2,7 @@ import { EventEmitter } from 'eventemitter3'; import { Component, shallowRef, ShallowRef } from 'vue'; -import { pleaseLogin } from '@/scripts/please-login'; +import { pleaseLoginOrPage } from '@/scripts/please-login'; import { safeURIDecode } from '@/scripts/safe-uri-decode'; type RouteDef = { @@ -174,7 +174,7 @@ export class Router extends EventEmitter<{ } if (res.route.loginRequired) { - pleaseLogin('/'); + pleaseLoginOrPage('/'); } const isSamePath = beforePath === path; diff --git a/packages/client/src/scripts/please-login.ts b/packages/client/src/scripts/please-login.ts index 52862427b..660876070 100644 --- a/packages/client/src/scripts/please-login.ts +++ b/packages/client/src/scripts/please-login.ts @@ -3,7 +3,7 @@ import { $i } from '@/account'; import { i18n } from '@/i18n'; import { popup } from '@/os'; -export function pleaseLogin(path?: string) { +export function pleaseLoginOrPage(path?: string) { if ($i) return; popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), { From 49ae56a9e97d85d18c8fdadfa9f76d6cdeeaa161 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Sat, 4 Mar 2023 00:08:12 +0100 Subject: [PATCH 11/41] client: refactor to pleaseLoginOrRemote --- .../client/src/components/note-detailed.vue | 8 +++++--- packages/client/src/components/note.vue | 8 +++++--- packages/client/src/components/poll.vue | 4 ++-- .../client/src/components/renote-button.vue | 5 +++-- packages/client/src/scripts/please-login.ts | 18 ++++++++++++++++++ 5 files changed, 33 insertions(+), 10 deletions(-) diff --git a/packages/client/src/components/note-detailed.vue b/packages/client/src/components/note-detailed.vue index 3549ef953..83d8b02e2 100644 --- a/packages/client/src/components/note-detailed.vue +++ b/packages/client/src/components/note-detailed.vue @@ -126,7 +126,7 @@ import XRenoteButton from './renote-button.vue'; import MkUrlPreview from '@/components/url-preview.vue'; import MkInstanceTicker from '@/components/instance-ticker.vue'; import MkVisibility from '@/components/visibility.vue'; -import { pleaseLogin } from '@/scripts/please-login'; +import { pleaseLoginOrRemote, urlForNote } from '@/scripts/please-login'; import { checkWordMute } from '@/scripts/check-word-mute'; import { userPage } from '@/filters/user'; import { notePage } from '@/filters/note'; @@ -195,7 +195,8 @@ useNoteCapture({ }); function reply(viaKeyboard = false): void { - pleaseLogin(); + pleaseLoginOrRemote(urlForNote(appearNote)); + os.post({ reply: appearNote, animation: !viaKeyboard, @@ -205,7 +206,8 @@ function reply(viaKeyboard = false): void { } function react(): void { - pleaseLogin(); + pleaseLoginOrRemote(urlForNote(appearNote)); + blur(); reactionPicker.show(reactButton.value, reaction => { os.api('notes/reactions/create', { diff --git a/packages/client/src/components/note.vue b/packages/client/src/components/note.vue index b0938b3c0..8324f083a 100644 --- a/packages/client/src/components/note.vue +++ b/packages/client/src/components/note.vue @@ -115,7 +115,7 @@ import XRenoteButton from './renote-button.vue'; import MkUrlPreview from '@/components/url-preview.vue'; import MkInstanceTicker from '@/components/instance-ticker.vue'; import MkVisibility from '@/components/visibility.vue'; -import { pleaseLogin } from '@/scripts/please-login'; +import { pleaseLoginOrRemote, urlForNote } from '@/scripts/please-login'; import { focusPrev, focusNext } from '@/scripts/focus'; import { checkWordMute } from '@/scripts/check-word-mute'; import { userPage } from '@/filters/user'; @@ -188,7 +188,8 @@ useNoteCapture({ }); function reply(viaKeyboard = false): void { - pleaseLogin(); + pleaseLoginOrRemote(urlForNote(appearNote)); + os.post({ reply: appearNote, animation: !viaKeyboard, @@ -198,7 +199,8 @@ function reply(viaKeyboard = false): void { } function react(): void { - pleaseLogin(); + pleaseLoginOrRemote(urlForNote(appearNote)); + blur(); reactionPicker.show(reactButton.value, reaction => { os.api('notes/reactions/create', { diff --git a/packages/client/src/components/poll.vue b/packages/client/src/components/poll.vue index 92f0961c9..db1363d28 100644 --- a/packages/client/src/components/poll.vue +++ b/packages/client/src/components/poll.vue @@ -25,7 +25,7 @@ import { computed, ref } from 'vue'; import * as foundkey from 'foundkey-js'; import { sum } from '@/scripts/array'; -import { pleaseLogin } from '@/scripts/please-login'; +import { pleaseLoginOrRemote, urlForNote } from '@/scripts/please-login'; import * as os from '@/os'; import { i18n } from '@/i18n'; import { useInterval } from '@/scripts/use-interval'; @@ -68,7 +68,7 @@ if (props.note.poll.expiresAt) { } const vote = async (id) => { - pleaseLogin(); + pleaseLoginOrRemote(urlForNote(props.note)); if (props.readOnly || closed.value || isVoted.value) return; diff --git a/packages/client/src/components/renote-button.vue b/packages/client/src/components/renote-button.vue index 9e4dbb3e6..4f84b783a 100644 --- a/packages/client/src/components/renote-button.vue +++ b/packages/client/src/components/renote-button.vue @@ -17,7 +17,7 @@ import { computed, ref } from 'vue'; import { Note } from 'foundkey-js/built/entities'; import XDetails from '@/components/users-tooltip.vue'; -import { pleaseLogin } from '@/scripts/please-login'; +import { pleaseLoginOrRemote, urlForNote } from '@/scripts/please-login'; import * as os from '@/os'; import { $i } from '@/account'; import { useTooltip } from '@/scripts/use-tooltip'; @@ -51,7 +51,8 @@ useTooltip(buttonRef, async (showing) => { }); function renote(viaKeyboard = false): void { - pleaseLogin(); + pleaseLoginOrRemote(urlForNote(props.note)); + os.popupMenu([{ text: i18n.ts.renote, icon: 'fas fa-retweet', diff --git a/packages/client/src/scripts/please-login.ts b/packages/client/src/scripts/please-login.ts index 660876070..89c0376ca 100644 --- a/packages/client/src/scripts/please-login.ts +++ b/packages/client/src/scripts/please-login.ts @@ -2,6 +2,8 @@ import { defineAsyncComponent } from 'vue'; import { $i } from '@/account'; import { i18n } from '@/i18n'; import { popup } from '@/os'; +import { url } from '@/config'; +import { entities } from 'foundkey-js'; export function pleaseLoginOrPage(path?: string) { if ($i) return; @@ -19,3 +21,19 @@ export function pleaseLoginOrPage(path?: string) { if (!path) throw new Error('signin required'); } + +export function pleaseLoginOrRemote(url: string) { + if ($i) return; + + popup(defineAsyncComponent(() => import('@/components/remote-interact.vue')), { + remoteUrl, + }, {}, 'closed'); + + throw new Error('signin required'); +} + +export function urlForNote(note: entities.Note): string { + return note.url + ?? note.uri + ?? `${url}/notes/${note.id}`; +} From f2350e6ebad7403e9c1921a5ebe9c185405925b3 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Sat, 4 Mar 2023 00:37:28 +0100 Subject: [PATCH 12/41] client: add remote interaction dialog --- locales/en-US.yml | 5 ++ .../client/src/components/remote-interact.vue | 80 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 packages/client/src/components/remote-interact.vue diff --git a/locales/en-US.yml b/locales/en-US.yml index 36a733b94..998648c8b 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1339,3 +1339,8 @@ _translationService: _libreTranslate: endpoint: "LibreTranslate API Endpoint" authKey: "LibreTranslate Auth Key (optional)" +_remoteInteract: + title: "And you are...?" + description: "You cannot perform this action right now. You probably need to do it on your own instance, or sign in." + urlInstructions: "You can copy this URL. If you paste it into the search field on your instance, you should be taken to the right location." + url: "URL" diff --git a/packages/client/src/components/remote-interact.vue b/packages/client/src/components/remote-interact.vue new file mode 100644 index 000000000..5601afc44 --- /dev/null +++ b/packages/client/src/components/remote-interact.vue @@ -0,0 +1,80 @@ + + + + + From 59a7f10fdc4395febb295cc0387a71de941b8a13 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Sun, 5 Mar 2023 12:15:15 +0100 Subject: [PATCH 13/41] client: change remote interaction dialog title --- locales/en-US.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales/en-US.yml b/locales/en-US.yml index 998648c8b..45ffe4cb2 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1340,7 +1340,7 @@ _translationService: endpoint: "LibreTranslate API Endpoint" authKey: "LibreTranslate Auth Key (optional)" _remoteInteract: - title: "And you are...?" + title: "I'm sorry, I'm afraid I can't do that." description: "You cannot perform this action right now. You probably need to do it on your own instance, or sign in." urlInstructions: "You can copy this URL. If you paste it into the search field on your instance, you should be taken to the right location." url: "URL" From 79ec44aa2c10fbaabdee91f85218229bb9903b48 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Sun, 5 Mar 2023 12:24:35 +0100 Subject: [PATCH 14/41] fix variable name --- packages/client/src/scripts/please-login.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/scripts/please-login.ts b/packages/client/src/scripts/please-login.ts index 89c0376ca..597f32e16 100644 --- a/packages/client/src/scripts/please-login.ts +++ b/packages/client/src/scripts/please-login.ts @@ -22,7 +22,7 @@ export function pleaseLoginOrPage(path?: string) { if (!path) throw new Error('signin required'); } -export function pleaseLoginOrRemote(url: string) { +export function pleaseLoginOrRemote(remoteUrl: string) { if ($i) return; popup(defineAsyncComponent(() => import('@/components/remote-interact.vue')), { From 74c2b79df1cad56e6b7184e6ea05e4f12dc45d27 Mon Sep 17 00:00:00 2001 From: Johann150 Date: Sun, 5 Mar 2023 14:10:41 +0100 Subject: [PATCH 15/41] remove key-value component from remote interaction dialog --- .../client/src/components/remote-interact.vue | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/client/src/components/remote-interact.vue b/packages/client/src/components/remote-interact.vue index 5601afc44..b1c77db8a 100644 --- a/packages/client/src/components/remote-interact.vue +++ b/packages/client/src/components/remote-interact.vue @@ -10,13 +10,9 @@

{{ i18n.ts._remoteInteract.description }}

- {{ i18n.ts._remoteInteract.urlInstructions }} - - - - +

{{ i18n.ts._remoteInteract.urlInstructions }}

+ {{ remoteUrl }} +