Skip to content

Commit

Permalink
Add: ブラウザ版にファイル読み込みを追加 (#2092)
Browse files Browse the repository at this point in the history
* Add: ブラウザ版にファイル読み込みを追加

* Code: 意図を追加

* Change: uuidを足す

* Code: コメントを足す

* Refactor: checkOsにまとめる

* Change: electron側と合わせる

* Add: 型を足す

* Change: パスの形式を変える

* Add: ファイルパスをエラーに追加

Co-Authored-By: Hiroshiba <[email protected]>

* Code: メモを追加

Co-Authored-By: Hiroshiba <[email protected]>

* Apply suggestions from code review

---------

Co-authored-by: Hiroshiba <[email protected]>
Co-authored-by: Hiroshiba <[email protected]>
  • Loading branch information
3 people authored Jun 15, 2024
1 parent 5d96ba7 commit d1aff61
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 21 deletions.
45 changes: 45 additions & 0 deletions src/backend/browser/fileImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -176,3 +179,45 @@ export const checkFileExistsImpl: (typeof window)[typeof SandboxKey]["checkFileE

return Promise.resolve(fileEntries.includes(fileName));
};

// FileSystemFileHandleを保持するMap。キーは生成した疑似パス。
const fileHandleMap: Map<string, FileSystemFileHandle> = new Map();

// ファイル選択ダイアログを開く
// 返り値はファイルパスではなく、疑似パスを返す
export const showOpenFilePickerImpl = async (options: {
multiple: boolean;
fileTypes: {
description: string;
accept: Record<string, string[]>;
}[];
}) => {
try {
const handles = await showOpenFilePicker({
excludeAcceptAllOption: true,
multiple: options.multiple,
types: options.fileTypes,
});
const paths = [];
for (const handle of handles) {
const fakePath = `<browser-dummy-${crypto.randomUUID()}>-${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);
};
51 changes: 39 additions & 12 deletions src/backend/browser/sandbox.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { defaultEngine } from "./contract";
import {
checkFileExistsImpl,
readFileImpl,
showOpenDirectoryDialogImpl,
showOpenFilePickerImpl,
writeFileImpl,
} from "./fileImpl";
import { getConfigManager } from "./browserConfig";
Expand Down Expand Up @@ -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";
Expand All @@ -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版をサポートする時に実装する
Expand Down
21 changes: 12 additions & 9 deletions src/type/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof urlStringSchema>;
Expand Down

0 comments on commit d1aff61

Please sign in to comment.