diff --git a/package-lock.json b/package-lock.json index 29f69d6f25..39d1f770cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,9 @@ "vue": "3.0.11", "vue-router": "4.0.8", "vuedraggable": "4.1.0", - "vuex": "4.0.2" + "vuex": "4.0.2", + "zod": "3.20.2", + "zod-to-json-schema": "3.20.1" }, "devDependencies": { "@openapitools/openapi-generator-cli": "2.3.3", @@ -28055,6 +28057,22 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", "dev": true + }, + "node_modules/zod": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.20.2.tgz", + "integrity": "sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.20.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.20.1.tgz", + "integrity": "sha512-U+zmNJUKqzv92E+LdEYv0g2LxBLks4HAwfC6cue8jXby5PAeSEPGO4xV9Sl4zmLYyFvJkm0FOfOs6orUO+AI1w==", + "peerDependencies": { + "zod": "^3.20.0" + } } }, "dependencies": { @@ -50198,6 +50216,17 @@ "dev": true } } + }, + "zod": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.20.2.tgz", + "integrity": "sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ==" + }, + "zod-to-json-schema": { + "version": "3.20.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.20.1.tgz", + "integrity": "sha512-U+zmNJUKqzv92E+LdEYv0g2LxBLks4HAwfC6cue8jXby5PAeSEPGO4xV9Sl4zmLYyFvJkm0FOfOs6orUO+AI1w==", + "requires": {} } } } diff --git a/package.json b/package.json index 55befa15e4..e89262aa49 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,9 @@ "vue": "3.0.11", "vue-router": "4.0.8", "vuedraggable": "4.1.0", - "vuex": "4.0.2" + "vuex": "4.0.2", + "zod": "3.20.2", + "zod-to-json-schema": "3.20.1" }, "devDependencies": { "@openapitools/openapi-generator-cli": "2.3.3", diff --git a/src/background.ts b/src/background.ts index 889bbfc013..559c12d2e5 100644 --- a/src/background.ts +++ b/src/background.ts @@ -1,7 +1,7 @@ "use strict"; import dotenv from "dotenv"; -import Store from "electron-store"; +import Store, { Schema } from "electron-store"; import { app, @@ -25,10 +25,13 @@ import { HotkeySetting, ThemeConf, AcceptTermsStatus, - ToolbarSetting, EngineInfo, ElectronStoreType, SystemError, + electronStoreSchema, + defaultHotkeySettings, + isMac, + defaultToolbarButtonSetting, } from "./type/preload"; import log from "electron-log"; @@ -36,6 +39,7 @@ import dayjs from "dayjs"; import windowStateKeeper from "electron-window-state"; import EngineManager from "./background/engineManager"; import VvppManager from "./background/vvppManager"; +import zodToJsonSchema from "zod-to-json-schema"; type SingleInstanceLockData = { filePath: string | undefined; @@ -92,293 +96,13 @@ protocol.registerSchemesAsPrivileged([ { scheme: "app", privileges: { secure: true, standard: true, stream: true } }, ]); -const isMac = process.platform === "darwin"; - -const defaultHotkeySettings: HotkeySetting[] = [ - { - action: "音声書き出し", - combination: !isMac ? "Ctrl E" : "Meta E", - }, - { - action: "一つだけ書き出し", - combination: "E", - }, - { - action: "音声を繋げて書き出し", - combination: "", - }, - { - action: "再生/停止", - combination: "Space", - }, - { - action: "連続再生/停止", - combination: "Shift Space", - }, - { - action: "アクセント欄を表示", - combination: "1", - }, - { - action: "イントネーション欄を表示", - combination: "2", - }, - { - action: "長さ欄を表示", - combination: "3", - }, - { - action: "テキスト欄を追加", - combination: "Shift Enter", - }, - { - action: "テキスト欄を削除", - combination: "Shift Delete", - }, - { - action: "テキスト欄からフォーカスを外す", - combination: "Escape", - }, - { - action: "テキスト欄にフォーカスを戻す", - combination: "Enter", - }, - { - action: "元に戻す", - combination: !isMac ? "Ctrl Z" : "Meta Z", - }, - { - action: "やり直す", - combination: !isMac ? "Ctrl Y" : "Shift Meta Z", - }, - { - action: "新規プロジェクト", - combination: !isMac ? "Ctrl N" : "Meta N", - }, - { - action: "プロジェクトを名前を付けて保存", - combination: !isMac ? "Ctrl Shift S" : "Shift Meta S", - }, - { - action: "プロジェクトを上書き保存", - combination: !isMac ? "Ctrl S" : "Meta S", - }, - { - action: "プロジェクト読み込み", - combination: !isMac ? "Ctrl O" : "Meta O", - }, - { - action: "テキスト読み込む", - combination: "", - }, - { - action: "全体のイントネーションをリセット", - combination: !isMac ? "Ctrl G" : "Meta G", - }, - { - action: "選択中のアクセント句のイントネーションをリセット", - combination: "R", - }, -]; - -const defaultToolbarButtonSetting: ToolbarSetting = [ - "PLAY_CONTINUOUSLY", - "STOP", - "EXPORT_AUDIO_ONE", - "EMPTY", - "UNDO", - "REDO", -]; - // 設定ファイル +const electronStoreJsonSchema = zodToJsonSchema(electronStoreSchema); +if (!("properties" in electronStoreJsonSchema)) { + throw new Error("electronStoreJsonSchema must be object"); +} const store = new Store({ - schema: { - useGpu: { - type: "boolean", - default: false, - }, - inheritAudioInfo: { - type: "boolean", - default: true, - }, - activePointScrollMode: { - type: "string", - enum: ["CONTINUOUSLY", "PAGE", "OFF"], - default: "OFF", - }, - savingSetting: { - type: "object", - properties: { - fileEncoding: { - type: "string", - enum: ["UTF-8", "Shift_JIS"], - default: "UTF-8", - }, - fileNamePattern: { - type: "string", - default: "", - }, - fixedExportEnabled: { type: "boolean", default: false }, - avoidOverwrite: { type: "boolean", default: false }, - fixedExportDir: { type: "string", default: "" }, - exportLab: { type: "boolean", default: false }, - exportText: { type: "boolean", default: false }, - outputStereo: { type: "boolean", default: false }, - outputSamplingRate: { - oneOf: [{ type: "number" }, { const: "engineDefault" }], - default: "engineDefault", - }, - audioOutputDevice: { type: "string", default: "default" }, - }, - default: { - fileEncoding: "UTF-8", - fileNamePattern: "", - fixedExportEnabled: false, - avoidOverwrite: false, - fixedExportDir: "", - exportLab: false, - exportText: false, - outputStereo: false, - outputSamplingRate: "engineDefault", - audioOutputDevice: "default", - splitTextWhenPaste: "PERIOD_AND_NEW_LINE", - }, - }, - // To future developers: if you are to modify the store schema with array type, - // for example, the hotkeySettings below, - // please remember to add a corresponding migration - // Learn more: https://github.com/sindresorhus/electron-store#migrations - hotkeySettings: { - type: "array", - items: { - type: "object", - properties: { - action: { type: "string" }, - combination: { type: "string" }, - }, - }, - default: defaultHotkeySettings, - }, - toolbarSetting: { - type: "array", - items: { - type: "string", - }, - default: defaultToolbarButtonSetting, - }, - userCharacterOrder: { - type: "array", - items: { - type: "string", - }, - default: [], - }, - defaultStyleIds: { - type: "array", - items: { - type: "object", - properties: { - speakerUuid: { type: "string" }, - defaultStyleId: { type: "number" }, - }, - }, - default: [], - }, - presets: { - type: "object", - properties: { - items: { - type: "object", - patternProperties: { - // uuid - "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}": { - type: "object", - properties: { - name: { type: "string" }, - speedScale: { type: "number" }, - pitchScale: { type: "number" }, - intonationScale: { type: "number" }, - volumeScale: { type: "number" }, - prePhonemeLength: { type: "number" }, - postPhonemeLength: { type: "number" }, - }, - }, - }, - additionalProperties: false, - }, - keys: { - type: "array", - items: { - type: "string", - pattern: - "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", - }, - }, - }, - default: { items: {}, keys: [] }, - }, - currentTheme: { - type: "string", - default: "Default", - }, - editorFont: { - anyOf: [{ const: "default" }, { const: "os" }], - }, - experimentalSetting: { - type: "object", - properties: { - enablePreset: { type: "boolean", default: false }, - enableInterrogativeUpspeak: { - type: "boolean", - default: false, - }, - }, - default: { - enablePreset: false, - enableInterrogativeUpspeak: false, - }, - }, - acceptRetrieveTelemetry: { - type: "string", - enum: ["Unconfirmed", "Accepted", "Refused"], - default: "Unconfirmed", - }, - acceptTerms: { - type: "string", - enum: ["Unconfirmed", "Accepted", "Rejected"], - default: "Unconfirmed", - }, - splitTextWhenPaste: { - type: "string", - enum: ["PERIOD_AND_NEW_LINE", "NEW_LINE", "OFF"], - default: "PERIOD_AND_NEW_LINE", - }, - splitterPosition: { - type: "object", - properties: { - portraitPaneWidth: { type: "number" }, - audioInfoPaneWidth: { type: "number" }, - audioDetailPaneHeight: { type: "number" }, - }, - default: {}, - }, - confirmedTips: { - type: "object", - properties: { - tweakableSliderByScroll: { type: "boolean", default: false }, - }, - default: { - tweakableSliderByScroll: false, - }, - }, - engineDirs: { - type: "array", - items: { - type: "string", - }, - default: [], - }, - }, + schema: electronStoreJsonSchema.properties as Schema, migrations: { ">=0.13": (store) => { // acceptTems -> acceptTerms diff --git a/src/background/engineManager.ts b/src/background/engineManager.ts index 936c6a8199..bb47cc4d83 100644 --- a/src/background/engineManager.ts +++ b/src/background/engineManager.ts @@ -16,7 +16,7 @@ import { } from "@/type/preload"; import log from "electron-log"; -import Ajv from "ajv/dist/jtd"; +import { z } from "zod"; type MinimumEngineManifest = { name: string; @@ -38,28 +38,18 @@ function createDefaultEngineInfos(defaultEngineDir: string): EngineInfo[] { // TODO: envから直接ではなく、envに書いたengine_manifest.jsonから情報を得るようにする const defaultEngineInfosEnv = process.env.DEFAULT_ENGINE_INFOS ?? "[]"; - const envSchema = { - elements: { - properties: { - uuid: { type: "string" }, - host: { type: "string" }, - name: { type: "string" }, - executionEnabled: { type: "boolean" }, - executionFilePath: { type: "string" }, - executionArgs: { elements: { type: "string" } }, - }, - optionalProperties: { - path: { type: "string" }, - }, - }, - } as const; - const ajv = new Ajv(); - const validate = ajv.compile(envSchema); - - const engines = JSON.parse(defaultEngineInfosEnv); - if (!validate(engines)) { - throw validate.errors; - } + const envSchema = z + .object({ + uuid: z.string().uuid(), + host: z.string(), + name: z.string(), + executionEnabled: z.boolean(), + executionFilePath: z.string(), + executionArgs: z.array(z.string()), + path: z.string().optional(), + }) + .array(); + const engines = envSchema.parse(JSON.parse(defaultEngineInfosEnv)); return engines.map((engineInfo) => { return { diff --git a/src/type/preload.ts b/src/type/preload.ts index ad4d49b006..a6059cc599 100644 --- a/src/type/preload.ts +++ b/src/type/preload.ts @@ -1,26 +1,104 @@ import { IpcRenderer, IpcRendererEvent } from "electron"; import { IpcSOData } from "./ipc"; - -export interface ElectronStoreType { - useGpu: boolean; - inheritAudioInfo: boolean; - activePointScrollMode: ActivePointScrollMode; - savingSetting: SavingSetting; - presets: PresetConfig; - hotkeySettings: HotkeySetting[]; - toolbarSetting: ToolbarSetting; - userCharacterOrder: string[]; - defaultStyleIds: DefaultStyleId[]; - currentTheme: string; - editorFont: EditorFontType; - experimentalSetting: ExperimentalSetting; - acceptRetrieveTelemetry: AcceptRetrieveTelemetryStatus; - acceptTerms: AcceptTermsStatus; - splitTextWhenPaste: SplitTextWhenPasteType; - splitterPosition: SplitterPosition; - confirmedTips: ConfirmedTips; - engineDirs: string[]; -} +import { z } from "zod"; + +export const isMac = process.platform === "darwin"; +// ホットキーを追加したときは設定のマイグレーションが必要 +export const defaultHotkeySettings: HotkeySetting[] = [ + { + action: "音声書き出し", + combination: !isMac ? "Ctrl E" : "Meta E", + }, + { + action: "一つだけ書き出し", + combination: "E", + }, + { + action: "音声を繋げて書き出し", + combination: "", + }, + { + action: "再生/停止", + combination: "Space", + }, + { + action: "連続再生/停止", + combination: "Shift Space", + }, + { + action: "アクセント欄を表示", + combination: "1", + }, + { + action: "イントネーション欄を表示", + combination: "2", + }, + { + action: "長さ欄を表示", + combination: "3", + }, + { + action: "テキスト欄を追加", + combination: "Shift Enter", + }, + { + action: "テキスト欄を削除", + combination: "Shift Delete", + }, + { + action: "テキスト欄からフォーカスを外す", + combination: "Escape", + }, + { + action: "テキスト欄にフォーカスを戻す", + combination: "Enter", + }, + { + action: "元に戻す", + combination: !isMac ? "Ctrl Z" : "Meta Z", + }, + { + action: "やり直す", + combination: !isMac ? "Ctrl Y" : "Shift Meta Z", + }, + { + action: "新規プロジェクト", + combination: !isMac ? "Ctrl N" : "Meta N", + }, + { + action: "プロジェクトを名前を付けて保存", + combination: !isMac ? "Ctrl Shift S" : "Shift Meta S", + }, + { + action: "プロジェクトを上書き保存", + combination: !isMac ? "Ctrl S" : "Meta S", + }, + { + action: "プロジェクト読み込み", + combination: !isMac ? "Ctrl O" : "Meta O", + }, + { + action: "テキスト読み込む", + combination: "", + }, + { + action: "全体のイントネーションをリセット", + combination: !isMac ? "Ctrl G" : "Meta G", + }, + { + action: "選択中のアクセント句のイントネーションをリセット", + combination: "R", + }, +]; + +export const defaultToolbarButtonSetting: ToolbarSetting = [ + "PLAY_CONTINUOUSLY", + "STOP", + "EXPORT_AUDIO_ONE", + "EMPTY", + "UNDO", + "REDO", +]; export interface Sandbox { getAppInfos(): Promise; @@ -181,11 +259,6 @@ export type DefaultStyleId = { defaultStyleId: number; }; -export type HotkeySetting = { - action: HotkeyAction; - combination: HotkeyCombo; -}; - export type EngineInfo = { uuid: string; host: string; @@ -215,50 +288,62 @@ export type PresetConfig = { items: Record; keys: string[]; }; -export type HotkeyAction = - | "音声書き出し" - | "一つだけ書き出し" - | "音声を繋げて書き出し" - | "再生/停止" - | "連続再生/停止" - | "アクセント欄を表示" - | "イントネーション欄を表示" - | "長さ欄を表示" - | "テキスト欄を追加" - | "テキスト欄を削除" - | "テキスト欄からフォーカスを外す" - | "テキスト欄にフォーカスを戻す" - | "元に戻す" - | "やり直す" - | "新規プロジェクト" - | "プロジェクトを名前を付けて保存" - | "プロジェクトを上書き保存" - | "プロジェクト読み込み" - | "テキスト読み込む" - | "全体のイントネーションをリセット" - | "選択中のアクセント句のイントネーションをリセット"; +export const hotkeyActionSchema = z.enum([ + "音声書き出し", + "一つだけ書き出し", + "音声を繋げて書き出し", + "再生/停止", + "連続再生/停止", + "アクセント欄を表示", + "イントネーション欄を表示", + "長さ欄を表示", + "テキスト欄を追加", + "テキスト欄を削除", + "テキスト欄からフォーカスを外す", + "テキスト欄にフォーカスを戻す", + "元に戻す", + "やり直す", + "新規プロジェクト", + "プロジェクトを名前を付けて保存", + "プロジェクトを上書き保存", + "プロジェクト読み込み", + "テキスト読み込む", + "全体のイントネーションをリセット", + "選択中のアクセント句のイントネーションをリセット", +]); + +export type HotkeyAction = z.infer; export type HotkeyCombo = string; +export const hotkeySettingSchema = z.object({ + action: hotkeyActionSchema, + combination: z.string(), +}); +export type HotkeySetting = z.infer; + export type HotkeyReturnType = | void | boolean | Promise | Promise; -export type ToolbarButtonTagType = - | "PLAY_CONTINUOUSLY" - | "STOP" - | "EXPORT_AUDIO_ONE" - | "EXPORT_AUDIO_ALL" - | "EXPORT_AUDIO_CONNECT_ALL" - | "SAVE_PROJECT" - | "UNDO" - | "REDO" - | "IMPORT_TEXT" - | "EMPTY"; - -export type ToolbarSetting = ToolbarButtonTagType[]; +export const toolbarButtonTagSchema = z.enum([ + "PLAY_CONTINUOUSLY", + "STOP", + "EXPORT_AUDIO_ONE", + "EXPORT_AUDIO_ALL", + "EXPORT_AUDIO_CONNECT_ALL", + "SAVE_PROJECT", + "UNDO", + "REDO", + "IMPORT_TEXT", + "EMPTY", +]); +export type ToolbarButtonTagType = z.infer; + +export const toolbarSettingSchema = toolbarButtonTagSchema; +export type ToolbarSetting = z.infer[]; export type MoraDataType = | "consonant" @@ -300,15 +385,97 @@ export type ExperimentalSetting = { enableInterrogativeUpspeak: boolean; }; -export type SplitterPosition = { - portraitPaneWidth: number | undefined; - audioInfoPaneWidth: number | undefined; - audioDetailPaneHeight: number | undefined; -}; +export const splitterPositionSchema = z.object({ + portraitPaneWidth: z.number().optional(), + audioInfoPaneWidth: z.number().optional(), + audioDetailPaneHeight: z.number().optional(), +}); +export type SplitterPosition = z.infer; export type ConfirmedTips = { tweakableSliderByScroll: boolean; }; +export const electronStoreSchema = z + .object({ + useGpu: z.boolean().default(false), + inheritAudioInfo: z.boolean().default(true), + activePointScrollMode: z + .enum(["CONTINUOUSLY", "PAGE", "OFF"]) + .default("OFF"), + savingSetting: z + .object({ + fileEncoding: z.enum(["UTF-8", "Shift_JIS"]).default("UTF-8"), + fileNamePattern: z.string().default(""), + fixedExportEnabled: z.boolean().default(false), + avoidOverwrite: z.boolean().default(false), + fixedExportDir: z.string().default(""), + exportLab: z.boolean().default(false), + exportText: z.boolean().default(false), + outputStereo: z.boolean().default(false), + outputSamplingRate: z + .union([z.number(), z.literal("engineDefault")]) + .default("engineDefault"), + audioOutputDevice: z.string().default(""), + }) + .passthrough() // 別のブランチでの開発中の設定項目があるコンフィグで死ぬのを防ぐ + .default({}), + hotkeySettings: hotkeySettingSchema.array().default(defaultHotkeySettings), + toolbarSetting: toolbarSettingSchema + .array() + .default(defaultToolbarButtonSetting), + userCharacterOrder: z.string().array().default([]), + defaultStyleIds: z + .object({ speakerUuid: z.string(), defaultStyleId: z.number() }) + .array() + .default([]), + presets: z + .object({ + items: z + .record( + z.string().uuid(), + z.object({ + name: z.string(), + speedScale: z.number(), + pitchScale: z.number(), + intonationScale: z.number(), + volumeScale: z.number(), + prePhonemeLength: z.number(), + postPhonemeLength: z.number(), + }) + ) + .default({}), + keys: z.string().uuid().array().default([]), + }) + .default({}), + currentTheme: z.string().default("Default"), + editorFont: z.enum(["default", "os"]).default("default"), + experimentalSetting: z + .object({ + enablePreset: z.boolean().default(false), + enableInterrogativeUpspeak: z.boolean().default(false), + }) + .passthrough() + .default({}), + acceptRetrieveTelemetry: z + .enum(["Unconfirmed", "Accepted", "Refused"]) + .default("Unconfirmed"), + acceptTerms: z + .enum(["Unconfirmed", "Accepted", "Rejected"]) + .default("Unconfirmed"), + splitTextWhenPaste: z + .enum(["PERIOD_AND_NEW_LINE", "NEW_LINE", "OFF"]) + .default("PERIOD_AND_NEW_LINE"), + splitterPosition: splitterPositionSchema.default({}), + confirmedTips: z + .object({ + tweakableSliderByScroll: z.boolean().default(false), + }) + .passthrough() + .default({}), + engineDirs: z.string().array().default([]), + }) + .passthrough(); // release-0.14直前で消す +export type ElectronStoreType = z.infer; // workaround. SystemError(https://nodejs.org/api/errors.html#class-systemerror)が2022/05/19時点ではNodeJSの型定義に記述されていないためこれを追加しています。 export class SystemError extends Error {