summaryrefslogtreecommitdiff
path: root/packages/frontend/src/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/frontend/src/scripts')
-rw-r--r--packages/frontend/src/scripts/file-drop.ts121
-rw-r--r--packages/frontend/src/scripts/key-event.ts153
-rw-r--r--packages/frontend/src/scripts/select-file.ts20
3 files changed, 291 insertions, 3 deletions
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<DroppedItem[]> {
+ 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<DroppedFile>();
+ 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<DroppedItem[]> {
+ async function readEntry(entry: FileSystemEntry): Promise<DroppedItem> {
+ 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<File> {
+ return new Promise((resolve, reject) => {
+ fileSystemFileEntry.file(resolve, reject);
+ });
+ }
+
+ function readDirectory(fileSystemDirectoryEntry: FileSystemDirectoryEntry): Promise<DroppedItem[]> {
+ return new Promise(async (resolve) => {
+ const allEntries = Array.of<FileSystemEntry>();
+ const reader = fileSystemDirectoryEntry.createReader();
+ while (true) {
+ const entries = await new Promise<FileSystemEntry[]>((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<DataTransferItem>();
+ 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<DroppedFile>();
+ 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<Misskey.entities.DriveFile[]> {
+export function chooseFileFromPc(
+ multiple: boolean,
+ options?: {
+ uploadFolder?: string | null;
+ keepOriginal?: boolean;
+ nameConverter?: (file: File) => string | undefined;
+ },
+): Promise<Misskey.entities.DriveFile[]> {
+ 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',