diff --git a/src/backend/browser/sandbox.ts b/src/backend/browser/sandbox.ts index de3279fb63..f1ef8025ab 100644 --- a/src/backend/browser/sandbox.ts +++ b/src/backend/browser/sandbox.ts @@ -280,4 +280,7 @@ export const api: Sandbox = { reloadApp(/* obj: { isMultiEngineOffMode: boolean } */) { throw new Error(`Not supported on Browser version: reloadApp`); }, + getPathForFile(/* file: File */) { + throw new Error(`Not supported on Browser version: getPathForFile`); + }, }; diff --git a/src/backend/electron/preload.ts b/src/backend/electron/preload.ts index 878c8f6448..23a3370a4f 100644 --- a/src/backend/electron/preload.ts +++ b/src/backend/electron/preload.ts @@ -1,4 +1,4 @@ -import { contextBridge, ipcRenderer } from "electron"; +import { contextBridge, ipcRenderer, webUtils } from "electron"; import type { IpcRendererInvoke } from "./ipc"; import { ConfigType, @@ -228,6 +228,11 @@ const api: Sandbox = { reloadApp: async ({ isMultiEngineOffMode }) => { await ipcRendererInvokeProxy.RELOAD_APP({ isMultiEngineOffMode }); }, + + /** webUtils.getPathForFileを呼ぶ */ + getPathForFile: (file) => { + return webUtils.getPathForFile(file); + }, }; contextBridge.exposeInMainWorld(SandboxKey, api); diff --git a/src/components/App.vue b/src/components/App.vue index f49b89c372..51cdbfea1b 100644 --- a/src/components/App.vue +++ b/src/components/App.vue @@ -165,6 +165,7 @@ onMounted(async () => { // プロジェクトファイルが指定されていればロード if (typeof projectFilePath === "string" && projectFilePath !== "") { isProjectFileLoaded.value = await store.actions.LOAD_PROJECT_FILE({ + type: "path", filePath: projectFilePath, }); } else { diff --git a/src/components/Menu/MenuBar/MenuBar.vue b/src/components/Menu/MenuBar/MenuBar.vue index a94d5d6678..5df82f18d2 100644 --- a/src/components/Menu/MenuBar/MenuBar.vue +++ b/src/components/Menu/MenuBar/MenuBar.vue @@ -157,7 +157,7 @@ const saveProjectAs = async () => { const importProject = () => { if (!uiLocked.value) { - void store.actions.LOAD_PROJECT_FILE({}); + void store.actions.LOAD_PROJECT_FILE({ type: "dialog" }); } }; @@ -198,6 +198,7 @@ const updateRecentProjects = async () => { label: projectFilePath, onClick: () => { void store.actions.LOAD_PROJECT_FILE({ + type: "path", filePath: projectFilePath, }); }, diff --git a/src/components/Talk/TalkEditor.vue b/src/components/Talk/TalkEditor.vue index ecf68b6041..dda58caba7 100644 --- a/src/components/Talk/TalkEditor.vue +++ b/src/components/Talk/TalkEditor.vue @@ -143,6 +143,7 @@ import { actionPostfixSelectNthCharacter, HotkeyActionNameType, } from "@/domain/hotkeyAction"; +import { isElectron } from "@/helpers/platform"; const props = defineProps<{ isEnginesReady: boolean; @@ -574,13 +575,27 @@ const dragEventCounter = ref(0); const loadDraggedFile = (event: { dataTransfer: DataTransfer | null }) => { if (!event.dataTransfer || event.dataTransfer.files.length === 0) return; const file = event.dataTransfer.files[0]; + + // electronの場合のみファイルパスを取得できる + const filePath = isElectron ? window.backend.getPathForFile(file) : undefined; + switch (path.extname(file.name)) { case ".txt": - void store.actions.COMMAND_IMPORT_FROM_FILE({ filePath: file.path }); + if (filePath) { + void store.actions.COMMAND_IMPORT_FROM_FILE({ type: "path", filePath }); + } else { + void store.actions.COMMAND_IMPORT_FROM_FILE({ type: "file", file }); + } break; + case ".vvproj": - void store.actions.LOAD_PROJECT_FILE({ filePath: file.path }); + if (filePath) { + void store.actions.LOAD_PROJECT_FILE({ type: "path", filePath }); + } else { + void store.actions.LOAD_PROJECT_FILE({ type: "file", file }); + } break; + default: void store.actions.SHOW_ALERT_DIALOG({ title: "対応していないファイルです", diff --git a/src/components/Talk/ToolBar.vue b/src/components/Talk/ToolBar.vue index 5bbb6e1e2c..12b1bbbc86 100644 --- a/src/components/Talk/ToolBar.vue +++ b/src/components/Talk/ToolBar.vue @@ -145,7 +145,7 @@ const saveProject = async () => { await store.actions.SAVE_PROJECT_FILE({ overwrite: true }); }; const importTextFile = () => { - void store.actions.COMMAND_IMPORT_FROM_FILE({}); + void store.actions.COMMAND_IMPORT_FROM_FILE({ type: "dialog" }); }; const usableButtons: Record< diff --git a/src/components/Talk/menuBarData.ts b/src/components/Talk/menuBarData.ts index 5a9b73c2ea..cea0fd3a8c 100644 --- a/src/components/Talk/menuBarData.ts +++ b/src/components/Talk/menuBarData.ts @@ -46,7 +46,7 @@ export const useMenuBarData = () => { type: "button", label: "テキスト読み込み", onClick: () => { - void store.actions.COMMAND_IMPORT_FROM_FILE({}); + void store.actions.COMMAND_IMPORT_FROM_FILE({ type: "dialog" }); }, disableWhenUiLocked: true, }, diff --git a/src/plugins/ipcMessageReceiverPlugin.ts b/src/plugins/ipcMessageReceiverPlugin.ts index 1ae8393ca6..78313ee5f3 100644 --- a/src/plugins/ipcMessageReceiverPlugin.ts +++ b/src/plugins/ipcMessageReceiverPlugin.ts @@ -9,8 +9,11 @@ export const ipcMessageReceiver: Plugin = { options: { store: Store }, ) => { window.backend.onReceivedIPCMsg({ - LOAD_PROJECT_FILE: (_, { filePath } = {}) => - void options.store.actions.LOAD_PROJECT_FILE({ filePath }), + LOAD_PROJECT_FILE: (_, { filePath }) => + void options.store.actions.LOAD_PROJECT_FILE({ + type: "path", + filePath, + }), DETECT_MAXIMIZED: () => options.store.actions.DETECT_MAXIMIZED(), diff --git a/src/store/audio.ts b/src/store/audio.ts index d433b55ef6..371b0616f7 100644 --- a/src/store/audio.ts +++ b/src/store/audio.ts @@ -2886,24 +2886,36 @@ export const audioCommandStore = transformCommandStore( prevAudioKey: undefined, }); }, + /** + * セリフテキストファイルを読み込む。 + * ファイル選択ダイアログを表示するか、ファイルパス指定するか、Fileインスタンスを渡すか選べる。 + */ action: createUILockAction( - async ( - { state, mutations, actions, getters }, - { filePath }: { filePath?: string }, - ) => { - if (!filePath) { + async ({ state, mutations, actions, getters }, payload) => { + let filePath: undefined | string; + if (payload.type == "dialog") { filePath = await window.backend.showImportFileDialog({ title: "セリフ読み込み", }); if (!filePath) return; + } else if (payload.type == "path") { + filePath = payload.filePath; } - let body = new TextDecoder("utf-8").decode( - await window.backend.readFile({ filePath }).then(getValueOrThrow), - ); + + let buf: ArrayBuffer; + if (filePath != undefined) { + buf = await window.backend + .readFile({ filePath }) + .then(getValueOrThrow); + } else { + if (payload.type != "file") + throw new UnreachableError("payload.type != 'file'"); + buf = await payload.file.arrayBuffer(); + } + + let body = new TextDecoder("utf-8").decode(buf); if (body.includes("\ufffd")) { - body = new TextDecoder("shift-jis").decode( - await window.backend.readFile({ filePath }).then(getValueOrThrow), - ); + body = new TextDecoder("shift-jis").decode(buf); } const audioItems: AudioItem[] = []; let baseAudioItem: AudioItem | undefined = undefined; diff --git a/src/store/project.ts b/src/store/project.ts index 48da368714..3e5a0862d4 100755 --- a/src/store/project.ts +++ b/src/store/project.ts @@ -21,7 +21,7 @@ import { DEFAULT_TPQN, } from "@/sing/domain"; import { EditorType } from "@/type/preload"; -import { IsEqual } from "@/type/utility"; +import { IsEqual, UnreachableError } from "@/type/utility"; import { showAlertDialog, showMessageDialog, @@ -166,15 +166,13 @@ export const projectStore = createPartialStore({ LOAD_PROJECT_FILE: { /** * プロジェクトファイルを読み込む。読み込めたかの成否が返る。 + * ファイル選択ダイアログを表示するか、ファイルパス指定するか、Fileインスタンスを渡すか選べる。 * エラー発生時はダイアログが表示される。 */ action: createUILockAction( - async ( - { actions, mutations, getters }, - { filePath }: { filePath?: string }, - ) => { - if (!filePath) { - // Select and load a project File. + async ({ actions, mutations, getters }, payload) => { + let filePath: undefined | string; + if (payload.type == "dialog") { const ret = await window.backend.showProjectLoadDialog({ title: "プロジェクトファイルの選択", }); @@ -182,17 +180,25 @@ export const projectStore = createPartialStore({ return false; } filePath = ret[0]; + } else if (payload.type == "path") { + filePath = payload.filePath; } - let buf: ArrayBuffer; try { - buf = await window.backend - .readFile({ filePath }) - .then(getValueOrThrow); + let buf: ArrayBuffer; + if (filePath != undefined) { + buf = await window.backend + .readFile({ filePath }) + .then(getValueOrThrow); - await actions.APPEND_RECENTLY_USED_PROJECT({ - filePath, - }); + await actions.APPEND_RECENTLY_USED_PROJECT({ + filePath, + }); + } else { + if (payload.type != "file") + throw new UnreachableError("payload.type != 'file'"); + buf = await payload.file.arrayBuffer(); + } const text = new TextDecoder("utf-8").decode(buf).trim(); const parsedProjectData = await actions.PARSE_PROJECT_FILE({ diff --git a/src/store/type.ts b/src/store/type.ts index 1e83ab6a0b..a57e243881 100644 --- a/src/store/type.ts +++ b/src/store/type.ts @@ -690,7 +690,12 @@ export type AudioCommandStoreTypes = { mutation: { audioKeyItemPairs: { audioItem: AudioItem; audioKey: AudioKey }[]; }; - action(payload: { filePath?: string }): void; + action( + payload: + | { type: "dialog" } + | { type: "path"; filePath: string } + | { type: "file"; file: File }, + ): void; }; COMMAND_PUT_TEXTS: { @@ -1837,7 +1842,12 @@ export type ProjectStoreTypes = { }; LOAD_PROJECT_FILE: { - action(payload: { filePath?: string }): boolean; + action( + payload: + | { type: "dialog" } + | { type: "path"; filePath: string } + | { type: "file"; file: File }, + ): boolean; }; SAVE_PROJECT_FILE: { diff --git a/src/type/ipc.ts b/src/type/ipc.ts index 3414f186fd..8c35a089f5 100644 --- a/src/type/ipc.ts +++ b/src/type/ipc.ts @@ -241,7 +241,7 @@ export type IpcIHData = { */ export type IpcSOData = { LOAD_PROJECT_FILE: { - args: [obj: { filePath?: string }]; + args: [obj: { filePath: string }]; return: void; }; diff --git a/src/type/preload.ts b/src/type/preload.ts index f916bc7b29..a144d2c64d 100644 --- a/src/type/preload.ts +++ b/src/type/preload.ts @@ -141,6 +141,7 @@ export interface Sandbox { uninstallVvppEngine(engineId: EngineId): Promise; validateEngineDir(engineDir: string): Promise; reloadApp(obj: { isMultiEngineOffMode?: boolean }): Promise; + getPathForFile(file: File): string; } export type AppInfos = {