From f9ad127aaf7875bad8fdf55f5ac98bff05997525 Mon Sep 17 00:00:00 2001 From: おさむのひと <46447427+samunohito@users.noreply.github.com> Date: Mon, 20 Jan 2025 20:35:37 +0900 Subject: feat: 新カスタム絵文字管理画面(β)の追加 (#13473) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * wip * wip * wip * wip * wip * wip * wip * fix * fix * fix * fix size * fix register logs * fix img autosize * fix row selection * support delete * fix border rendering * fix display:none * tweak comments * support choose pc file and drive file * support directory drag-drop * fix * fix comment * support context menu on data area * fix autogen * wip イベント整理 * イベントの整理 * refactor grid * fix cell re-render bugs * fix row remove * fix comment * fix validation * fix utils * list maximum * add mimetype check * fix * fix number cell focus * fix over 100 file drop * remove log * fix patchData * fix performance * fix * support update and delete * support remote import * fix layout * heightやめる * fix performance * add list v2 endpoint * support pagination * fix api call * fix no clickable input text * fix limit * fix paging * fix * fix * support search * tweak logs * tweak cell selection * fix range select * block delete * add comment * fix * support import log * fix dialog * refactor * add confirm dialog * fix name * fix autogen * wip * support image change and highlight row * add columns * wip * support sort * add role name * add index to emoji * refine context menu setting * support role select * remove unused buttons * fix url * fix MkRoleSelectDialog.vue * add route * refine remote page * enter key search * fix paste bugs * fix copy/paste * fix keyEvent * fix copy/paste and delete * fix comment * fix MkRoleSelectDialog.vue and storybook scenario * fix MkRoleSelectDialog.vue and storybook scenario * add MkGrid.stories.impl.ts * fix * [wip] add custom-emojis-manager2.stories.impl.ts * [wip] add custom-emojis-manager2.stories.impl.ts * wip * 課題はまだ残っているが、ひとまず完了 * fix validation and register roles * fix upload * optimize import * patch from dev * i18n * revert excess fixes * separate sort order component * add SPDX * revert excess fixes * fix pre test * fix bugs * add type column * fix types * fix CHANGELOG.md * fix lit * lint * tweak style * refactor * fix ci * autogen * Update types.ts * CSS Module化 * fix log * 縦スクロールを無効化 * MkStickyContainer化 * regenerate locales index.d.ts * fix * fix * テスト * ランダム値によるUI変更の抑制 * テスト * tableタグやめる * fix last-child css * fix overflow css * fix endpoint.ts * tweak css * 最新への追従とレイアウト微調整 * ソートキーの指定方法を他と合わせた * fix focus * fix layout * v2エンドポイントのルールに対応 * 表示条件などを微調整 * fix MkDataCell.vue * fix error code * fix error * add comment to MkModal.vue * Update index.d.ts * fix CHANGELOG.md * fix color theme * fix CHANGELOG.md * fix CHANGELOG.md * fix center * fix: テーブルにフォーカスがあり、通常状態であるときはキーイベントの伝搬を止める * fix: ロール選択用のダイアログにてコンディショナルロールを×ボタンで除外できなかったのを修正 * fix remote list folder * sticky footers * chore: fix ci error(just single line-break diff) * fix loading * fix like * comma to space * fix ci * fix ci * removed align-center --------- Co-authored-by: osamu <46447427+sam-osamu@users.noreply.github.com> Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> Co-authored-by: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com> --- packages/frontend/src/scripts/file-drop.ts | 121 +++++++++++++++++++++ packages/frontend/src/scripts/key-event.ts | 153 +++++++++++++++++++++++++++ packages/frontend/src/scripts/select-file.ts | 20 +++- 3 files changed, 291 insertions(+), 3 deletions(-) create mode 100644 packages/frontend/src/scripts/file-drop.ts create mode 100644 packages/frontend/src/scripts/key-event.ts (limited to 'packages/frontend/src/scripts') diff --git a/packages/frontend/src/scripts/file-drop.ts b/packages/frontend/src/scripts/file-drop.ts new file mode 100644 index 0000000000..c2e863c0dc --- /dev/null +++ b/packages/frontend/src/scripts/file-drop.ts @@ -0,0 +1,121 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export type DroppedItem = DroppedFile | DroppedDirectory; + +export type DroppedFile = { + isFile: true; + path: string; + file: File; +}; + +export type DroppedDirectory = { + isFile: false; + path: string; + children: DroppedItem[]; +} + +export async function extractDroppedItems(ev: DragEvent): Promise { + const dropItems = ev.dataTransfer?.items; + if (!dropItems || dropItems.length === 0) { + return []; + } + + const apiTestItem = dropItems[0]; + if ('webkitGetAsEntry' in apiTestItem) { + return readDataTransferItems(dropItems); + } else { + // webkitGetAsEntryに対応していない場合はfilesから取得する(ディレクトリのサポートは出来ない) + const dropFiles = ev.dataTransfer.files; + if (dropFiles.length === 0) { + return []; + } + + const droppedFiles = Array.of(); + for (let i = 0; i < dropFiles.length; i++) { + const file = dropFiles.item(i); + if (file) { + droppedFiles.push({ + isFile: true, + path: file.name, + file, + }); + } + } + + return droppedFiles; + } +} + +/** + * ドラッグ&ドロップされたファイルのリストからディレクトリ構造とファイルへの参照({@link File})を取得する。 + */ +export async function readDataTransferItems(itemList: DataTransferItemList): Promise { + async function readEntry(entry: FileSystemEntry): Promise { + if (entry.isFile) { + return { + isFile: true, + path: entry.fullPath, + file: await readFile(entry as FileSystemFileEntry), + }; + } else { + return { + isFile: false, + path: entry.fullPath, + children: await readDirectory(entry as FileSystemDirectoryEntry), + }; + } + } + + function readFile(fileSystemFileEntry: FileSystemFileEntry): Promise { + return new Promise((resolve, reject) => { + fileSystemFileEntry.file(resolve, reject); + }); + } + + function readDirectory(fileSystemDirectoryEntry: FileSystemDirectoryEntry): Promise { + return new Promise(async (resolve) => { + const allEntries = Array.of(); + const reader = fileSystemDirectoryEntry.createReader(); + while (true) { + const entries = await new Promise((res, rej) => reader.readEntries(res, rej)); + if (entries.length === 0) { + break; + } + allEntries.push(...entries); + } + + resolve(await Promise.all(allEntries.map(readEntry))); + }); + } + + // 扱いにくいので配列に変換 + const items = Array.of(); + for (let i = 0; i < itemList.length; i++) { + items.push(itemList[i]); + } + + return Promise.all( + items + .map(it => it.webkitGetAsEntry()) + .filter(it => it) + .map(it => readEntry(it!)), + ); +} + +/** + * {@link DroppedItem}のリストからディレクトリを再帰的に検索し、ファイルのリストを取得する。 + */ +export function flattenDroppedFiles(items: DroppedItem[]): DroppedFile[] { + const result = Array.of(); + for (const item of items) { + if (item.isFile) { + result.push(item); + } else { + result.push(...flattenDroppedFiles(item.children)); + } + } + return result; +} diff --git a/packages/frontend/src/scripts/key-event.ts b/packages/frontend/src/scripts/key-event.ts new file mode 100644 index 0000000000..a72776d48c --- /dev/null +++ b/packages/frontend/src/scripts/key-event.ts @@ -0,0 +1,153 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +/** + * {@link KeyboardEvent.code} の値を表す文字列。不足分は適宜追加する + * @see https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values + */ +export type KeyCode = + | 'Backspace' + | 'Tab' + | 'Enter' + | 'Shift' + | 'Control' + | 'Alt' + | 'Pause' + | 'CapsLock' + | 'Escape' + | 'Space' + | 'PageUp' + | 'PageDown' + | 'End' + | 'Home' + | 'ArrowLeft' + | 'ArrowUp' + | 'ArrowRight' + | 'ArrowDown' + | 'Insert' + | 'Delete' + | 'Digit0' + | 'Digit1' + | 'Digit2' + | 'Digit3' + | 'Digit4' + | 'Digit5' + | 'Digit6' + | 'Digit7' + | 'Digit8' + | 'Digit9' + | 'KeyA' + | 'KeyB' + | 'KeyC' + | 'KeyD' + | 'KeyE' + | 'KeyF' + | 'KeyG' + | 'KeyH' + | 'KeyI' + | 'KeyJ' + | 'KeyK' + | 'KeyL' + | 'KeyM' + | 'KeyN' + | 'KeyO' + | 'KeyP' + | 'KeyQ' + | 'KeyR' + | 'KeyS' + | 'KeyT' + | 'KeyU' + | 'KeyV' + | 'KeyW' + | 'KeyX' + | 'KeyY' + | 'KeyZ' + | 'MetaLeft' + | 'MetaRight' + | 'ContextMenu' + | 'F1' + | 'F2' + | 'F3' + | 'F4' + | 'F5' + | 'F6' + | 'F7' + | 'F8' + | 'F9' + | 'F10' + | 'F11' + | 'F12' + | 'NumLock' + | 'ScrollLock' + | 'Semicolon' + | 'Equal' + | 'Comma' + | 'Minus' + | 'Period' + | 'Slash' + | 'Backquote' + | 'BracketLeft' + | 'Backslash' + | 'BracketRight' + | 'Quote' + | 'Meta' + | 'AltGraph' + ; + +/** + * 修飾キーを表す文字列。不足分は適宜追加する。 + */ +export type KeyModifier = + | 'Shift' + | 'Control' + | 'Alt' + | 'Meta' + ; + +/** + * 押下されたキー以外の状態を表す文字列。不足分は適宜追加する。 + */ +export type KeyState = + | 'composing' + | 'repeat' + ; + +export type KeyEventHandler = { + modifiers?: KeyModifier[]; + states?: KeyState[]; + code: KeyCode | 'any'; + handler: (event: KeyboardEvent) => void; +} + +export function handleKeyEvent(event: KeyboardEvent, handlers: KeyEventHandler[]) { + function checkModifier(ev: KeyboardEvent, modifiers? : KeyModifier[]) { + if (modifiers) { + return modifiers.every(modifier => ev.getModifierState(modifier)); + } + return true; + } + + function checkState(ev: KeyboardEvent, states?: KeyState[]) { + if (states) { + return states.every(state => ev.getModifierState(state)); + } + return true; + } + + let hit = false; + for (const handler of handlers.filter(it => it.code === event.code)) { + if (checkModifier(event, handler.modifiers) && checkState(event, handler.states)) { + handler.handler(event); + hit = true; + break; + } + } + + if (!hit) { + for (const handler of handlers.filter(it => it.code === 'any')) { + handler.handler(event); + } + } +} diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts index b037aa8acc..c25b4d73bd 100644 --- a/packages/frontend/src/scripts/select-file.ts +++ b/packages/frontend/src/scripts/select-file.ts @@ -12,14 +12,28 @@ import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; import { uploadFile } from '@/scripts/upload.js'; -export function chooseFileFromPc(multiple: boolean, keepOriginal = false): Promise { +export function chooseFileFromPc( + multiple: boolean, + options?: { + uploadFolder?: string | null; + keepOriginal?: boolean; + nameConverter?: (file: File) => string | undefined; + }, +): Promise { + const uploadFolder = options?.uploadFolder ?? defaultStore.state.uploadFolder; + const keepOriginal = options?.keepOriginal ?? defaultStore.state.keepOriginalUploading; + const nameConverter = options?.nameConverter ?? (() => undefined); + return new Promise((res, rej) => { const input = document.createElement('input'); input.type = 'file'; input.multiple = multiple; input.onchange = () => { if (!input.files) return res([]); - const promises = Array.from(input.files, file => uploadFile(file, defaultStore.state.uploadFolder, undefined, keepOriginal)); + const promises = Array.from( + input.files, + file => uploadFile(file, uploadFolder, nameConverter(file), keepOriginal), + ); Promise.all(promises).then(driveFiles => { res(driveFiles); @@ -94,7 +108,7 @@ function select(src: HTMLElement | EventTarget | null, label: string | null, mul }, { text: i18n.ts.upload, icon: 'ti ti-upload', - action: () => chooseFileFromPc(multiple, keepOriginal.value).then(files => res(files)), + action: () => chooseFileFromPc(multiple, { keepOriginal: keepOriginal.value }).then(files => res(files)), }, { text: i18n.ts.fromDrive, icon: 'ti ti-cloud', -- cgit v1.2.3-freya