diff --git a/src/backend/browser/browserConfig.ts b/src/backend/browser/browserConfig.ts index a24d3b1c1e..6508d9af8e 100644 --- a/src/backend/browser/browserConfig.ts +++ b/src/backend/browser/browserConfig.ts @@ -5,6 +5,7 @@ import { BaseConfigManager, Metadata } from "@/backend/common/ConfigManager"; import { ConfigType, EngineId, engineSettingSchema } from "@/type/preload"; import { ensureNotNullish } from "@/helpers/errorHelper"; import { UnreachableError } from "@/type/utility"; +import { isMac } from "@/helpers/platform"; const dbName = `${import.meta.env.VITE_APP_NAME}-web`; const settingStoreKey = "config"; @@ -20,7 +21,7 @@ const defaultEngineId = EngineId(defaultEngine.uuid); export async function getConfigManager() { await configManagerLock.acquire("configManager", async () => { if (!configManager) { - configManager = new BrowserConfigManager(); + configManager = new BrowserConfigManager(isMac); await configManager.initialize(); } }); diff --git a/src/backend/common/ConfigManager.ts b/src/backend/common/ConfigManager.ts index 7c216464b1..b5ddabb706 100644 --- a/src/backend/common/ConfigManager.ts +++ b/src/backend/common/ConfigManager.ts @@ -3,7 +3,7 @@ import AsyncLock from "async-lock"; import { AcceptTermsStatus, ConfigType, - configSchema, + getConfigSchema, DefaultStyleId, ExperimentalSettingType, VoiceId, @@ -13,9 +13,10 @@ import { ensureNotNullish } from "@/helpers/errorHelper"; import { loadEnvEngineInfos } from "@/domain/defaultEngine/envEngineInfo"; import { HotkeyCombination, - defaultHotkeySettings, + getDefaultHotkeySettings, HotkeySettingType, } from "@/domain/hotkeyAction"; +import { isMac } from "@/helpers/platform"; const lockKey = "save"; @@ -308,6 +309,8 @@ export abstract class BaseConfigManager { protected abstract getAppVersion(): string; + constructor(protected isMac: boolean) {} + public reset() { this.config = this.getDefaultConfig(); this._save(); @@ -322,7 +325,9 @@ export abstract class BaseConfigManager { migration(data); } } - this.config = this.migrateHotkeySettings(configSchema.parse(data)); + this.config = this.migrateHotkeySettings( + getConfigSchema(this.isMac).parse(data), + ); this._save(); } else { this.reset(); @@ -352,7 +357,7 @@ export abstract class BaseConfigManager { private _save() { void this.lock.acquire(lockKey, async () => { await this.save({ - ...configSchema.parse({ + ...getConfigSchema(this.isMac).parse({ ...this.config, }), __internal__: { @@ -387,7 +392,7 @@ export abstract class BaseConfigManager { private migrateHotkeySettings(data: ConfigType): ConfigType { const COMBINATION_IS_NONE = HotkeyCombination("####"); const loadedHotkeys = structuredClone(data.hotkeySettings); - const hotkeysWithoutNewCombination = defaultHotkeySettings.map( + const hotkeysWithoutNewCombination = getDefaultHotkeySettings(isMac).map( (defaultHotkey) => { const loadedHotkey = loadedHotkeys.find( (loadedHotkey) => loadedHotkey.action === defaultHotkey.action, @@ -402,7 +407,7 @@ export abstract class BaseConfigManager { const migratedHotkeys = hotkeysWithoutNewCombination.map((hotkey) => { if (hotkey.combination === COMBINATION_IS_NONE) { const newHotkey = ensureNotNullish( - defaultHotkeySettings.find( + getDefaultHotkeySettings(isMac).find( (defaultHotkey) => defaultHotkey.action === hotkey.action, ), ); @@ -429,6 +434,6 @@ export abstract class BaseConfigManager { } protected getDefaultConfig(): ConfigType { - return configSchema.parse({}); + return getConfigSchema(this.isMac).parse({}); } } diff --git a/src/backend/electron/electronConfig.ts b/src/backend/electron/electronConfig.ts index 59dbfba4a5..2c39a083e4 100644 --- a/src/backend/electron/electronConfig.ts +++ b/src/backend/electron/electronConfig.ts @@ -4,6 +4,7 @@ import { app } from "electron"; import { writeFileSafely } from "./fileHelper"; import { BaseConfigManager, Metadata } from "@/backend/common/ConfigManager"; import { ConfigType } from "@/type/preload"; +import { isMac } from "@/helpers/platform"; export class ElectronConfigManager extends BaseConfigManager { protected getAppVersion() { @@ -35,7 +36,7 @@ let configManager: ElectronConfigManager | undefined; export function getConfigManager(): ElectronConfigManager { if (!configManager) { - configManager = new ElectronConfigManager(); + configManager = new ElectronConfigManager(isMac); } return configManager; } diff --git a/src/components/Dialog/HotkeySettingDialog.vue b/src/components/Dialog/HotkeySettingDialog.vue index 44900bddc8..47ebf88726 100644 --- a/src/components/Dialog/HotkeySettingDialog.vue +++ b/src/components/Dialog/HotkeySettingDialog.vue @@ -130,8 +130,9 @@ import { useHotkeyManager, eventToCombination } from "@/plugins/hotkeyPlugin"; import { HotkeyCombination, HotkeyActionNameType, - defaultHotkeySettings, + getDefaultHotkeySettings, } from "@/domain/hotkeyAction"; +import { isMac } from "@/helpers/platform"; const props = defineProps<{ modelValue: boolean; @@ -226,7 +227,7 @@ const setHotkeyDialogOpened = () => { }; const isDefaultCombination = (action: string) => { - const defaultSetting = defaultHotkeySettings.find( + const defaultSetting = getDefaultHotkeySettings(isMac).find( (value) => value.action === action, ); const hotkeySetting = hotkeySettings.value.find( @@ -244,7 +245,9 @@ const resetHotkey = async (action: string) => { if (result !== "OK") return; - const setting = defaultHotkeySettings.find((value) => value.action == action); + const setting = getDefaultHotkeySettings(isMac).find( + (value) => value.action == action, + ); if (setting == undefined) { return; } diff --git a/src/domain/hotkeyAction.ts b/src/domain/hotkeyAction.ts index 5d62a4aa5a..41ddd8f850 100644 --- a/src/domain/hotkeyAction.ts +++ b/src/domain/hotkeyAction.ts @@ -1,5 +1,4 @@ import { z } from "zod"; -import { isMac } from "@/helpers/platform"; const hotkeyCombinationSchema = z.string().brand("HotkeyCombination"); export type HotkeyCombination = z.infer; @@ -61,138 +60,141 @@ export const hotkeySettingSchema = z.object({ }); export type HotkeySettingType = z.infer; -// ホットキーを追加したときは設定のマイグレーションが必要 -export const defaultHotkeySettings: HotkeySettingType[] = [ - { - action: "音声書き出し", - combination: HotkeyCombination(!isMac ? "Ctrl E" : "Meta E"), - }, - { - action: "選択音声を書き出し", - combination: HotkeyCombination("E"), - }, - { - action: "音声を繋げて書き出し", - combination: HotkeyCombination(""), - }, - { - action: "再生/停止", - combination: HotkeyCombination("Space"), - }, - { - action: "連続再生/停止", - combination: HotkeyCombination("Shift Space"), - }, - { - action: "アクセント欄を表示", - combination: HotkeyCombination("1"), - }, - { - action: "イントネーション欄を表示", - combination: HotkeyCombination("2"), - }, - { - action: "長さ欄を表示", - combination: HotkeyCombination("3"), - }, - { - action: "テキスト欄を追加", - combination: HotkeyCombination("Shift Enter"), - }, - { - action: "テキスト欄を複製", - combination: HotkeyCombination(!isMac ? "Ctrl D" : "Meta D"), - }, - { - action: "テキスト欄を削除", - combination: HotkeyCombination("Shift Delete"), - }, - { - action: "テキスト欄からフォーカスを外す", - combination: HotkeyCombination("Escape"), - }, - { - action: "テキスト欄にフォーカスを戻す", - combination: HotkeyCombination("Enter"), - }, - { - action: "元に戻す", - combination: HotkeyCombination(!isMac ? "Ctrl Z" : "Meta Z"), - }, - { - action: "やり直す", - combination: HotkeyCombination(!isMac ? "Ctrl Y" : "Shift Meta Z"), - }, - { - action: "拡大", - combination: HotkeyCombination(""), - }, - { - action: "縮小", - combination: HotkeyCombination(""), - }, - { - action: "拡大率のリセット", - combination: HotkeyCombination(""), - }, - { - action: "新規プロジェクト", - combination: HotkeyCombination(!isMac ? "Ctrl N" : "Meta N"), - }, - { - action: "全画面表示を切り替え", - combination: HotkeyCombination(!isMac ? "F11" : "Ctrl Meta F"), - }, - { - action: "プロジェクトを名前を付けて保存", - combination: HotkeyCombination(!isMac ? "Ctrl Shift S" : "Shift Meta S"), - }, - { - action: "プロジェクトを上書き保存", - combination: HotkeyCombination(!isMac ? "Ctrl S" : "Meta S"), - }, - { - action: "プロジェクトを読み込む", - combination: HotkeyCombination(!isMac ? "Ctrl O" : "Meta O"), - }, - { - action: "テキストを読み込む", - combination: HotkeyCombination(""), - }, - { - action: "全体のイントネーションをリセット", - combination: HotkeyCombination(!isMac ? "Ctrl G" : "Meta G"), - }, - { - action: "選択中のアクセント句のイントネーションをリセット", - combination: HotkeyCombination("R"), - }, - { - action: "コピー", - combination: HotkeyCombination(!isMac ? "Ctrl C" : "Meta C"), - }, - { - action: "切り取り", - combination: HotkeyCombination(!isMac ? "Ctrl X" : "Meta X"), - }, - { - action: "貼り付け", - combination: HotkeyCombination(!isMac ? "Ctrl V" : "Meta V"), - }, - { - action: "すべて選択", - combination: HotkeyCombination(!isMac ? "Ctrl A" : "Meta A"), - }, - { - action: "選択解除", - combination: HotkeyCombination("Escape"), - }, - ...Array.from({ length: 10 }, (_, index) => { - const roleKey = index == 9 ? 0 : index + 1; - return { - action: - `${index + 1}${actionPostfixSelectNthCharacter}` as HotkeyActionNameType, - combination: HotkeyCombination(`${!isMac ? "Ctrl" : "Meta"} ${roleKey}`), - }; - }), -]; +export function getDefaultHotkeySettings(isMac: boolean): HotkeySettingType[] { + return [ + { + action: "音声書き出し", + combination: HotkeyCombination(!isMac ? "Ctrl E" : "Meta E"), + }, + { + action: "選択音声を書き出し", + combination: HotkeyCombination("E"), + }, + { + action: "音声を繋げて書き出し", + combination: HotkeyCombination(""), + }, + { + action: "再生/停止", + combination: HotkeyCombination("Space"), + }, + { + action: "連続再生/停止", + combination: HotkeyCombination("Shift Space"), + }, + { + action: "アクセント欄を表示", + combination: HotkeyCombination("1"), + }, + { + action: "イントネーション欄を表示", + combination: HotkeyCombination("2"), + }, + { + action: "長さ欄を表示", + combination: HotkeyCombination("3"), + }, + { + action: "テキスト欄を追加", + combination: HotkeyCombination("Shift Enter"), + }, + { + action: "テキスト欄を複製", + combination: HotkeyCombination(!isMac ? "Ctrl D" : "Meta D"), + }, + { + action: "テキスト欄を削除", + combination: HotkeyCombination("Shift Delete"), + }, + { + action: "テキスト欄からフォーカスを外す", + combination: HotkeyCombination("Escape"), + }, + { + action: "テキスト欄にフォーカスを戻す", + combination: HotkeyCombination("Enter"), + }, + { + action: "元に戻す", + combination: HotkeyCombination(!isMac ? "Ctrl Z" : "Meta Z"), + }, + { + action: "やり直す", + combination: HotkeyCombination(!isMac ? "Ctrl Y" : "Shift Meta Z"), + }, + { + action: "拡大", + combination: HotkeyCombination(""), + }, + { + action: "縮小", + combination: HotkeyCombination(""), + }, + { + action: "拡大率のリセット", + combination: HotkeyCombination(""), + }, + { + action: "新規プロジェクト", + combination: HotkeyCombination(!isMac ? "Ctrl N" : "Meta N"), + }, + { + action: "全画面表示を切り替え", + combination: HotkeyCombination(!isMac ? "F11" : "Ctrl Meta F"), + }, + { + action: "プロジェクトを名前を付けて保存", + combination: HotkeyCombination(!isMac ? "Ctrl Shift S" : "Shift Meta S"), + }, + { + action: "プロジェクトを上書き保存", + combination: HotkeyCombination(!isMac ? "Ctrl S" : "Meta S"), + }, + { + action: "プロジェクトを読み込む", + combination: HotkeyCombination(!isMac ? "Ctrl O" : "Meta O"), + }, + { + action: "テキストを読み込む", + combination: HotkeyCombination(""), + }, + { + action: "全体のイントネーションをリセット", + combination: HotkeyCombination(!isMac ? "Ctrl G" : "Meta G"), + }, + { + action: "選択中のアクセント句のイントネーションをリセット", + combination: HotkeyCombination("R"), + }, + { + action: "コピー", + combination: HotkeyCombination(!isMac ? "Ctrl C" : "Meta C"), + }, + { + action: "切り取り", + combination: HotkeyCombination(!isMac ? "Ctrl X" : "Meta X"), + }, + { + action: "貼り付け", + combination: HotkeyCombination(!isMac ? "Ctrl V" : "Meta V"), + }, + { + action: "すべて選択", + combination: HotkeyCombination(!isMac ? "Ctrl A" : "Meta A"), + }, + { + action: "選択解除", + combination: HotkeyCombination("Escape"), + }, + ...Array.from({ length: 10 }, (_, index) => { + const roleKey = index == 9 ? 0 : index + 1; + return { + action: + `${index + 1}${actionPostfixSelectNthCharacter}` as HotkeyActionNameType, + combination: HotkeyCombination( + `${!isMac ? "Ctrl" : "Meta"} ${roleKey}`, + ), + }; + }), + ]; +} diff --git a/src/type/preload.ts b/src/type/preload.ts index 24e4afc361..cd1e6cf9e4 100644 --- a/src/type/preload.ts +++ b/src/type/preload.ts @@ -5,7 +5,7 @@ import { Result } from "@/type/result"; import { HotkeySettingType, hotkeySettingSchema, - defaultHotkeySettings, + getDefaultHotkeySettings, } from "@/domain/hotkeyAction"; const urlStringSchema = z.string().url().brand("URL"); @@ -404,91 +404,95 @@ export const rootMiscSettingSchema = z.object({ }); export type RootMiscSettingType = z.infer; -export const configSchema = z - .object({ - 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(""), // NOTE: ファイル名パターンは拡張子を含まない - 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), - audioOutputDevice: z.string().default(""), - songTrackFileNamePattern: z.string().default(""), - }) - .default({}), - hotkeySettings: hotkeySettingSchema.array().default(defaultHotkeySettings), - toolbarSetting: toolbarSettingSchema - .array() - .default(defaultToolbarButtonSetting), - engineSettings: z.record(engineIdSchema, engineSettingSchema).default({}), - userCharacterOrder: speakerIdSchema.array().default([]), - defaultStyleIds: z - .object({ - engineId: engineIdSchema - .or(z.literal(EngineId("00000000-0000-0000-0000-000000000000"))) - .default(EngineId("00000000-0000-0000-0000-000000000000")), - speakerUuid: speakerIdSchema, - defaultStyleId: styleIdSchema, - }) - .array() - .default([]), - presets: z - .object({ - items: z - .record( - presetKeySchema, - z.object({ - name: z.string(), - speedScale: z.number(), - pitchScale: z.number(), - intonationScale: z.number(), - volumeScale: z.number(), - pauseLengthScale: z.number(), - prePhonemeLength: z.number(), - postPhonemeLength: z.number(), - morphingInfo: z - .object({ - rate: z.number(), - targetEngineId: engineIdSchema, - targetSpeakerId: speakerIdSchema, - targetStyleId: styleIdSchema, - }) - .optional(), - }), - ) - .default({}), - keys: presetKeySchema.array().default([]), - }) - .default({}), - defaultPresetKeys: z.record(voiceIdSchema, presetKeySchema).default({}), - currentTheme: z.string().default("Default"), - experimentalSetting: experimentalSettingSchema.default({}), - acceptRetrieveTelemetry: z - .enum(["Unconfirmed", "Accepted", "Refused"]) - .default("Unconfirmed"), - acceptTerms: z - .enum(["Unconfirmed", "Accepted", "Rejected"]) - .default("Unconfirmed"), - confirmedTips: z - .object({ - tweakableSliderByScroll: z.boolean().default(false), - engineStartedOnAltPort: z.boolean().default(false), - notifyOnGenerate: z.boolean().default(false), - }) - .default({}), - registeredEngineDirs: z.string().array().default([]), - recentlyUsedProjects: z.string().array().default([]), - }) - .merge(rootMiscSettingSchema); -export type ConfigType = z.infer; +export function getConfigSchema(isMac: boolean) { + return z + .object({ + 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(""), // NOTE: ファイル名パターンは拡張子を含まない + 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), + audioOutputDevice: z.string().default(""), + songTrackFileNamePattern: z.string().default(""), + }) + .default({}), + hotkeySettings: hotkeySettingSchema + .array() + .default(getDefaultHotkeySettings(isMac)), + toolbarSetting: toolbarSettingSchema + .array() + .default(defaultToolbarButtonSetting), + engineSettings: z.record(engineIdSchema, engineSettingSchema).default({}), + userCharacterOrder: speakerIdSchema.array().default([]), + defaultStyleIds: z + .object({ + engineId: engineIdSchema + .or(z.literal(EngineId("00000000-0000-0000-0000-000000000000"))) + .default(EngineId("00000000-0000-0000-0000-000000000000")), + speakerUuid: speakerIdSchema, + defaultStyleId: styleIdSchema, + }) + .array() + .default([]), + presets: z + .object({ + items: z + .record( + presetKeySchema, + z.object({ + name: z.string(), + speedScale: z.number(), + pitchScale: z.number(), + intonationScale: z.number(), + volumeScale: z.number(), + pauseLengthScale: z.number(), + prePhonemeLength: z.number(), + postPhonemeLength: z.number(), + morphingInfo: z + .object({ + rate: z.number(), + targetEngineId: engineIdSchema, + targetSpeakerId: speakerIdSchema, + targetStyleId: styleIdSchema, + }) + .optional(), + }), + ) + .default({}), + keys: presetKeySchema.array().default([]), + }) + .default({}), + defaultPresetKeys: z.record(voiceIdSchema, presetKeySchema).default({}), + currentTheme: z.string().default("Default"), + experimentalSetting: experimentalSettingSchema.default({}), + acceptRetrieveTelemetry: z + .enum(["Unconfirmed", "Accepted", "Refused"]) + .default("Unconfirmed"), + acceptTerms: z + .enum(["Unconfirmed", "Accepted", "Rejected"]) + .default("Unconfirmed"), + confirmedTips: z + .object({ + tweakableSliderByScroll: z.boolean().default(false), + engineStartedOnAltPort: z.boolean().default(false), + notifyOnGenerate: z.boolean().default(false), + }) + .default({}), + registeredEngineDirs: z.string().array().default([]), + recentlyUsedProjects: z.string().array().default([]), + }) + .merge(rootMiscSettingSchema); +} +export type ConfigType = z.infer>; // workaround. SystemError(https://nodejs.org/api/errors.html#class-systemerror)が2022/05/19時点ではNodeJSの型定義に記述されていないためこれを追加しています。 export class SystemError extends Error { diff --git a/tests/unit/backend/common/configManager.spec.ts b/tests/unit/backend/common/configManager.spec.ts index d1109ada57..eed9aa0aaa 100644 --- a/tests/unit/backend/common/configManager.spec.ts +++ b/tests/unit/backend/common/configManager.spec.ts @@ -1,11 +1,10 @@ import pastConfigs from "./pastConfigs"; import configBugDefaultPreset1996 from "./pastConfigs/0.19.1-bug_default_preset.json"; -import { isMac } from "@/helpers/platform"; import { BaseConfigManager } from "@/backend/common/ConfigManager"; -import { configSchema } from "@/type/preload"; +import { getConfigSchema } from "@/type/preload"; const configBase = { - ...configSchema.parse({}), + ...getConfigSchema(false).parse({}), __internal__: { migrations: { version: "999.999.999", @@ -50,7 +49,7 @@ it("新規作成できる", async () => { async () => undefined, ); - const configManager = new TestConfigManager(); + const configManager = new TestConfigManager(false); await configManager.initialize(); expect(configManager).toBeTruthy(); }); @@ -63,7 +62,7 @@ it("バージョンが保存される", async () => { .spyOn(TestConfigManager.prototype, "save") .mockImplementation(async () => undefined); - const configManager = new TestConfigManager(); + const configManager = new TestConfigManager(false); await configManager.initialize(); await configManager.ensureSaved(); expect(saveSpy).toHaveBeenCalled(); @@ -83,16 +82,12 @@ for (const [version, data] of pastConfigs) { async () => data, ); - const configManager = new TestConfigManager(); + const configManager = new TestConfigManager(false); await configManager.initialize(); expect(configManager).toBeTruthy(); // マイグレーション後のデータが正しいことをスナップショットで確認 - // NOTE: Macはショートカットキーが異なるためパス - // TODO: ConfigManagerにOSを引数指定できるようにしてテストを分ける - if (!isMac) { - expect(configManager.getAll()).toMatchSnapshot(); - } + expect(configManager.getAll()).toMatchSnapshot(); }); } @@ -127,7 +122,7 @@ it("0.19.1からのマイグレーション時にハミング・ソングスタ ).map((key) => getStyleIdFromVoiceId(key)); // マイグレーション - const configManager = new TestConfigManager(); + const configManager = new TestConfigManager(false); await configManager.initialize(); const presets = configManager.get("presets"); const defaultPresetKeys = configManager.get("defaultPresetKeys"); @@ -169,7 +164,7 @@ it("getできる", async () => { }), ); - const configManager = new TestConfigManager(); + const configManager = new TestConfigManager(false); await configManager.initialize(); expect(configManager.get("inheritAudioInfo")).toBe(false); }); @@ -188,7 +183,7 @@ it("setできる", async () => { }), ); - const configManager = new TestConfigManager(); + const configManager = new TestConfigManager(false); await configManager.initialize(); configManager.set("inheritAudioInfo", true); expect(configManager.get("inheritAudioInfo")).toBe(true);