From 4157aecff6dea3824b550d89163fcde49945a27b Mon Sep 17 00:00:00 2001 From: kchop Date: Wed, 11 May 2022 01:02:47 +0900 Subject: [PATCH] =?UTF-8?q?=E6=9B=B8=E3=81=8D=E5=87=BA=E3=81=97=E3=83=95?= =?UTF-8?q?=E3=82=A1=E3=82=A4=E3=83=AB=E5=90=8D=E3=82=92=E3=82=AB=E3=82=B9?= =?UTF-8?q?=E3=82=BF=E3=83=9E=E3=82=A4=E3=82=BA=E5=8F=AF=E8=83=BD=E3=81=AB?= =?UTF-8?q?=E3=81=99=E3=82=8B=20(#789)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 設定値の型等に項目追加 * 与えたパターンからファイル名が決定できるようにした * 値を取ってくるのとファイル名のために加工するメソッドを分ける * utilityへ移動 * ツールチップが特定条件でしか出てこなかったのを修正 * コンポーネントの実装 * スタイルの調整 * 重複し得るファイル名指定で一括出力した場合に上書きされないようにする * 日付も挿入可能にする * デバッグ出力の削除 * 連番を必須にしつつコンポーネントを多少リファクタ * テストの修正 * Revert "重複し得るファイル名指定で一括出力した場合に上書きされないようにする" This reverts commit 8a1be6ba003ca1bf8326feeae7c44081376d8fc9. * サンプルテキストの修正 * テキストにタグ文字列を含めたとき悪いことができないようにする * 変数名などのリファクタ * 別名つけただけの型を削除 * 表示される文言の調整 * 出力ファイル名に$を使えなくしてマッチ対象の問題をちょっと強引に解決 * nextTickを使う * 日付がyyyymmddじゃなかった問題を修正 * 型定義をシンプルに * styleNameタグにかっこを含めない * 初回に設定ダイアログを開いたときデフォルト値をフォームに入れるように --- src/background.ts | 5 + src/components/FileNamePatternDialog.vue | 257 +++++++++++++++++++++++ src/components/SettingDialog.vue | 53 ++++- src/store/audio.ts | 27 ++- src/store/setting.ts | 1 + src/store/utility.ts | 77 +++++++ src/type/preload.d.ts | 1 + tests/unit/store/Vuex.spec.ts | 2 + 8 files changed, 407 insertions(+), 16 deletions(-) create mode 100644 src/components/FileNamePatternDialog.vue diff --git a/src/background.ts b/src/background.ts index b2778eb2dc..eed5659919 100644 --- a/src/background.ts +++ b/src/background.ts @@ -233,6 +233,10 @@ const store = new Store<{ 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: "" }, @@ -244,6 +248,7 @@ const store = new Store<{ }, default: { fileEncoding: "UTF-8", + fileNamePattern: "", fixedExportEnabled: false, avoidOverwrite: false, fixedExportDir: "", diff --git a/src/components/FileNamePatternDialog.vue b/src/components/FileNamePatternDialog.vue new file mode 100644 index 0000000000..63bac543c6 --- /dev/null +++ b/src/components/FileNamePatternDialog.vue @@ -0,0 +1,257 @@ + + + + + diff --git a/src/components/SettingDialog.vue b/src/components/SettingDialog.vue index 793a69532a..7a3eb0494b 100644 --- a/src/components/SettingDialog.vue +++ b/src/components/SettingDialog.vue @@ -293,7 +293,6 @@ self="center right" transition-show="jump-left" transition-hide="jump-right" - v-if="!savingSetting.fixedExportEnabled" > 音声ファイルを設定したフォルダに書き出す @@ -343,6 +342,44 @@ + + + +
書き出しファイル名パターン
+
+ + + 書き出すファイル名のパターンをカスタマイズする + + +
+ +
+ {{ savingSetting.fileNamePattern }} +
+ +
+
上書き防止
@@ -680,10 +717,15 @@ import { ActivePointScrollMode, SplitTextWhenPasteType, } from "@/type/preload"; +import FileNamePatternDialog from "./FileNamePatternDialog.vue"; export default defineComponent({ name: "SettingDialog", + components: { + FileNamePatternDialog, + }, + props: { modelValue: { type: Boolean, @@ -958,6 +1000,8 @@ export default defineComponent({ store.dispatch("SET_SPLIT_TEXT_WHEN_PASTE", { splitTextWhenPaste }); }; + const showsFilePatternEditDialog = ref(false); + return { settingDialogOpenedComputed, engineMode, @@ -979,6 +1023,7 @@ export default defineComponent({ acceptRetrieveTelemetryComputed, splitTextWhenPaste, changeSplitTextWhenPaste, + showsFilePatternEditDialog, }; }, }); @@ -1015,6 +1060,12 @@ export default defineComponent({ background: colors.$primary; } +.text-ellipsis { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + .scroll-mode-button:hover { background: rgba(colors.$primary-rgb, 0.2); } diff --git a/src/store/audio.ts b/src/store/audio.ts index fdbe6bd429..8fdef5311e 100644 --- a/src/store/audio.ts +++ b/src/store/audio.ts @@ -29,11 +29,12 @@ import { import Encoding from "encoding-japanese"; import { PromiseType } from "./vuex"; import { + buildFileNameFromRawData, buildProjectFileName, convertHiraToKana, convertLongVowel, createKanaRegex, - sanitizeFileName, + currentDateString, } from "./utility"; async function generateUniqueIdAndQuery( @@ -103,9 +104,12 @@ function parseTextFile( } function buildFileName(state: State, audioKey: string) { + const fileNamePattern = state.savingSetting.fileNamePattern; + const index = state.audioKeys.indexOf(audioKey); const audioItem = state.audioItems[audioKey]; let styleName: string | undefined = ""; + const character = state.characterInfos?.find((info) => { const result = info.metas.styles.findIndex( (style) => style.styleId === audioItem.styleId @@ -122,20 +126,13 @@ function buildFileName(state: State, audioKey: string) { throw new Error(); } - const characterName = sanitizeFileName(character.metas.speakerName); - let text = sanitizeFileName(audioItem.text); - if (text.length > 10) { - text = text.substring(0, 9) + "…"; - } - - const preFileName = (index + 1).toString().padStart(3, "0"); - // デフォルトのスタイルだとstyleIdが定義されていないのでundefinedになる。なのでファイル名に入れてしまうことを回避する目的で分岐させています。 - if (styleName === undefined) { - return preFileName + `_${characterName}_${text}.wav`; - } - - const sanitizedStyleName = sanitizeFileName(styleName); - return preFileName + `_${characterName}(${sanitizedStyleName})_${text}.wav`; + return buildFileNameFromRawData(fileNamePattern, { + characterName: character.metas.speakerName, + index, + styleName, + text: audioItem.text, + date: currentDateString(), + }); } const audioBlobCache: Record = {}; diff --git a/src/store/setting.ts b/src/store/setting.ts index ace681b83a..bbd209ea4c 100644 --- a/src/store/setting.ts +++ b/src/store/setting.ts @@ -24,6 +24,7 @@ const hotkeyFunctionCache: Record HotkeyReturnType> = {}; export const settingStoreState: SettingStoreState = { savingSetting: { fileEncoding: "UTF-8", + fileNamePattern: "", fixedExportEnabled: false, fixedExportDir: "", avoidOverwrite: false, diff --git a/src/store/utility.ts b/src/store/utility.ts index c9aad029b2..3672b71a33 100644 --- a/src/store/utility.ts +++ b/src/store/utility.ts @@ -45,6 +45,83 @@ export function buildProjectFileName(state: State, extension?: string): string { : defaultFileNameStem; } +export const replaceTagIdToTagString = { + index: "連番", + characterName: "キャラ", + styleName: "スタイル", + text: "テキスト", + date: "日付", +}; +const replaceTagStringToTagId: { [tagString: string]: string } = Object.entries( + replaceTagIdToTagString +).reduce((prev, [k, v]) => ({ ...prev, [v]: k }), {}); + +export const DEFAULT_FILE_NAME_TEMPLATE = + "$連番$_$キャラ$($スタイル$)_$テキスト$.wav"; +const DEFAULT_FILE_NAME_VARIABLES = { + index: 0, + characterName: "四国めたん", + text: "テキストテキストテキスト", + styleName: "ノーマル", + date: currentDateString(), +}; + +export function currentDateString(): string { + const currentDate = new Date(); + const year = currentDate.getFullYear(); + const month = currentDate.getMonth().toString().padStart(2, "0"); + const date = currentDate.getDate().toString().padStart(2, "0"); + + return `${year}${month}${date}`; +} + +function replaceTag( + template: string, + replacer: { [key: string]: string } +): string { + const result = template.replace(/\$(.+?)\$/g, (match, p1) => { + const replaceTagId = replaceTagStringToTagId[p1]; + if (replaceTagId === undefined) { + return match; + } + return replacer[replaceTagId] ?? ""; + }); + + return result; +} + +export function buildFileNameFromRawData( + fileNamePattern = DEFAULT_FILE_NAME_TEMPLATE, + vars = DEFAULT_FILE_NAME_VARIABLES +): string { + let pattern = fileNamePattern; + if (pattern === "") { + // ファイル名指定のオプションが初期値("")ならデフォルトテンプレートを使う + pattern = DEFAULT_FILE_NAME_TEMPLATE; + } + + let text = sanitizeFileName(vars.text); + if (text.length > 10) { + text = text.substring(0, 9) + "…"; + } + + const characterName = sanitizeFileName(vars.characterName); + + const index = (vars.index + 1).toString().padStart(3, "0"); + + const styleName = sanitizeFileName(vars.styleName); + + const date = currentDateString(); + + return replaceTag(pattern, { + index, + characterName, + styleName: styleName, + text, + date, + }); +} + export const getToolbarButtonName = (tag: ToolbarButtonTagType): string => { const tag2NameObj: Record = { PLAY_CONTINUOUSLY: "連続再生", diff --git a/src/type/preload.d.ts b/src/type/preload.d.ts index 46dbe03bb4..62a4d704ab 100644 --- a/src/type/preload.d.ts +++ b/src/type/preload.d.ts @@ -146,6 +146,7 @@ export type SplitTextWhenPasteType = "PERIOD_AND_NEW_LINE" | "NEW_LINE" | "OFF"; export type SavingSetting = { exportLab: boolean; fileEncoding: Encoding; + fileNamePattern: string; fixedExportEnabled: boolean; fixedExportDir: string; avoidOverwrite: boolean; diff --git a/tests/unit/store/Vuex.spec.ts b/tests/unit/store/Vuex.spec.ts index e32308ceca..22b73beab6 100644 --- a/tests/unit/store/Vuex.spec.ts +++ b/tests/unit/store/Vuex.spec.ts @@ -46,6 +46,7 @@ describe("store/vuex.js test", () => { savedLastCommandUnixMillisec: null, savingSetting: { fileEncoding: "UTF-8", + fileNamePattern: "", fixedExportEnabled: false, fixedExportDir: "", avoidOverwrite: false, @@ -161,6 +162,7 @@ describe("store/vuex.js test", () => { assert.propertyVal(store.state.savingSetting, "fixedExportDir", ""); assert.propertyVal(store.state.savingSetting, "avoidOverwrite", false); assert.propertyVal(store.state.savingSetting, "exportLab", false); + assert.propertyVal(store.state.savingSetting, "fileNamePattern", ""); assert.equal(store.state.isPinned, false); assert.isObject(store.state.presetItems); assert.isEmpty(store.state.presetItems);