diff --git a/src/backend/browser/fileImpl.ts b/src/backend/browser/fileImpl.ts index 25c3f6684c..d60f85f30f 100644 --- a/src/backend/browser/fileImpl.ts +++ b/src/backend/browser/fileImpl.ts @@ -3,6 +3,9 @@ import { directoryHandleStoreKey } from "./contract"; import { openDB } from "./browserConfig"; import { SandboxKey } from "@/type/preload"; import { failure, success } from "@/type/result"; +import { createLogger } from "@/domain/frontend/log"; + +const log = createLogger("fileImpl"); const storeDirectoryHandle = async ( directoryHandle: FileSystemDirectoryHandle, @@ -176,3 +179,45 @@ export const checkFileExistsImpl: (typeof window)[typeof SandboxKey]["checkFileE return Promise.resolve(fileEntries.includes(fileName)); }; + +// FileSystemFileHandleを保持するMap。キーは生成した疑似パス。 +const fileHandleMap: Map = new Map(); + +// ファイル選択ダイアログを開く +// 返り値はファイルパスではなく、疑似パスを返す +export const showOpenFilePickerImpl = async (options: { + multiple: boolean; + fileTypes: { + description: string; + accept: Record; + }[]; +}) => { + try { + const handles = await showOpenFilePicker({ + excludeAcceptAllOption: true, + multiple: options.multiple, + types: options.fileTypes, + }); + const paths = []; + for (const handle of handles) { + const fakePath = `-${handle.name}`; + fileHandleMap.set(fakePath, handle); + paths.push(fakePath); + } + return handles.length > 0 ? paths : undefined; + } catch (e) { + log.warn(`showOpenFilePicker error: ${e}`); + return undefined; + } +}; + +// 指定した疑似パスのファイルを読み込む +export const readFileImpl = async (filePath: string) => { + const fileHandle = fileHandleMap.get(filePath); + if (fileHandle == undefined) { + return failure(new Error(`ファイルが見つかりません: ${filePath}`)); + } + const file = await fileHandle.getFile(); + const buffer = await file.arrayBuffer(); + return success(buffer); +}; diff --git a/src/backend/browser/sandbox.ts b/src/backend/browser/sandbox.ts index f3e298054d..f2f5451085 100644 --- a/src/backend/browser/sandbox.ts +++ b/src/backend/browser/sandbox.ts @@ -1,7 +1,9 @@ import { defaultEngine } from "./contract"; import { checkFileExistsImpl, + readFileImpl, showOpenDirectoryDialogImpl, + showOpenFilePickerImpl, writeFileImpl, } from "./fileImpl"; import { getConfigManager } from "./browserConfig"; @@ -127,10 +129,18 @@ export const api: Sandbox = { } }); }, - showProjectLoadDialog(/* obj: { title: string } */) { - throw new Error( - "ブラウザ版では現在ファイルの読み込みをサポートしていません", - ); + async showProjectLoadDialog() { + return showOpenFilePickerImpl({ + multiple: false, + fileTypes: [ + { + description: "Voicevox Project File", + accept: { + "application/json": [".vvproj"], + }, + }, + ], + }); }, showMessageDialog(obj: { type: "none" | "info" | "error" | "question" | "warning"; @@ -156,18 +166,35 @@ export const api: Sandbox = { `Not implemented: showQuestionDialog, request: ${JSON.stringify(obj)}`, ); }, - showImportFileDialog(/* obj: { title: string } */) { - throw new Error( - "ブラウザ版では現在ファイルの読み込みをサポートしていません", - ); + async showImportFileDialog(obj: { + name?: string; + extensions?: string[]; + title: string; + }) { + const fileHandle = await showOpenFilePickerImpl({ + multiple: false, + fileTypes: [ + { + description: obj.name ?? "Text", + accept: obj.extensions + ? { + "application/octet-stream": obj.extensions.map( + (ext) => `.${ext}`, + ), + } + : { + "plain/text": [".txt"], + }, + }, + ], + }); + return fileHandle?.[0]; }, writeFile(obj: { filePath: string; buffer: ArrayBuffer }) { return writeFileImpl(obj); }, - readFile(/* obj: { filePath: string } */) { - throw new Error( - "ブラウザ版では現在ファイルの読み込みをサポートしていません", - ); + readFile(obj: { filePath: string }) { + return readFileImpl(obj.filePath); }, isAvailableGPUMode() { // TODO: WebAssembly版をサポートする時に実装する diff --git a/src/type/preload.ts b/src/type/preload.ts index 67b32c34cb..01a3bdecb7 100644 --- a/src/type/preload.ts +++ b/src/type/preload.ts @@ -7,25 +7,28 @@ export const isProduction = import.meta.env.MODE === "production"; export const isElectron = import.meta.env.VITE_TARGET === "electron"; export const isBrowser = import.meta.env.VITE_TARGET === "browser"; -// electronのメイン・レンダラープロセス内、ブラウザ内どこでも使用可能なmacOS判定 -function checkIsMac(): boolean { - let isMac: boolean | undefined = undefined; +// electronのメイン・レンダラープロセス内、ブラウザ内どこでも使用可能なOS判定 +function checkOs(os: "windows" | "mac"): boolean { + let isSpecifiedOs: boolean | undefined = undefined; if (process?.platform) { // electronのメインプロセス用 - isMac = process.platform === "darwin"; + isSpecifiedOs = + process.platform === (os === "windows" ? "win32" : "darwin"); } else if (navigator?.userAgentData) { // electronのレンダラープロセス用、Chrome系統が実装する実験的機能 - isMac = navigator.userAgentData.platform.toLowerCase().includes("mac"); + isSpecifiedOs = navigator.userAgentData.platform.toLowerCase().includes(os); } else if (navigator?.platform) { // ブラウザ用、非推奨機能 - isMac = navigator.platform.toLowerCase().includes("mac"); + isSpecifiedOs = navigator.platform.toLowerCase().includes(os); } else { // ブラウザ用、不正確 - isMac = navigator.userAgent.toLowerCase().includes("mac"); + isSpecifiedOs = navigator.userAgent.toLowerCase().includes(os); } - return isMac; + return isSpecifiedOs; } -export const isMac = checkIsMac(); + +export const isMac = checkOs("mac"); +export const isWindows = checkOs("windows"); const urlStringSchema = z.string().url().brand("URL"); export type UrlString = z.infer;