上書き防止
@@ -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);