From 64863fc773a918f38fd946c3e502f43e777587c5 Mon Sep 17 00:00:00 2001 From: Nanashi Date: Sat, 14 Jan 2023 05:37:44 +0900 Subject: [PATCH] =?UTF-8?q?Change:=20zod=E3=82=92=E4=BD=BF=E3=81=86?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=20(#1078)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Hiroshiba close undefined --- package-lock.json | 31 +++- package.json | 4 +- src/background.ts | 298 ++----------------------------- src/background/engineManager.ts | 36 ++-- src/type/preload.ts | 299 +++++++++++++++++++++++++------- 5 files changed, 290 insertions(+), 378 deletions(-) diff --git a/package-lock.json b/package-lock.json index 88bef05e00..90673bfa3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,9 @@ "vue": "3.2.45", "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", @@ -27849,6 +27851,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": { @@ -49819,6 +49837,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 ea06559386..ddeb98dd1b 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,9 @@ "vue": "3.2.45", "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 8b5a4eef3e..994eca5517 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; @@ -100,293 +104,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 e1990400c0..23bbc986a2 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 2d99a9163c..0be29a5036 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; @@ -182,11 +260,6 @@ export type DefaultStyleId = { defaultStyleId: number; }; -export type HotkeySetting = { - action: HotkeyAction; - combination: HotkeyCombo; -}; - export type EngineInfo = { uuid: string; host: string; @@ -216,50 +289,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" @@ -301,15 +386,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 {