Skip to content

Commit

Permalink
書き出しファイル名をカスタマイズ可能にする (VOICEVOX#789)
Browse files Browse the repository at this point in the history
* 設定値の型等に項目追加

* 与えたパターンからファイル名が決定できるようにした

* 値を取ってくるのとファイル名のために加工するメソッドを分ける

* utilityへ移動

* ツールチップが特定条件でしか出てこなかったのを修正

* コンポーネントの実装

* スタイルの調整

* 重複し得るファイル名指定で一括出力した場合に上書きされないようにする

* 日付も挿入可能にする

* デバッグ出力の削除

* 連番を必須にしつつコンポーネントを多少リファクタ

* テストの修正

* Revert "重複し得るファイル名指定で一括出力した場合に上書きされないようにする"

This reverts commit 8a1be6b.

* サンプルテキストの修正

* テキストにタグ文字列を含めたとき悪いことができないようにする

* 変数名などのリファクタ

* 別名つけただけの型を削除

* 表示される文言の調整

* 出力ファイル名に$を使えなくしてマッチ対象の問題をちょっと強引に解決

* nextTickを使う

* 日付がyyyymmddじゃなかった問題を修正

* 型定義をシンプルに

* styleNameタグにかっこを含めない

* 初回に設定ダイアログを開いたときデフォルト値をフォームに入れるように
  • Loading branch information
k-chop committed May 10, 2022
1 parent fc04a18 commit 4157aec
Show file tree
Hide file tree
Showing 8 changed files with 407 additions and 16 deletions.
5 changes: 5 additions & 0 deletions src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: "" },
Expand All @@ -244,6 +248,7 @@ const store = new Store<{
},
default: {
fileEncoding: "UTF-8",
fileNamePattern: "",
fixedExportEnabled: false,
avoidOverwrite: false,
fixedExportDir: "",
Expand Down
257 changes: 257 additions & 0 deletions src/components/FileNamePatternDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
<template>
<q-dialog
:model-value="openDialog"
@update:model-value="updateOpenDialog"
@before-show="initializeInput"
>
<q-card class="q-pa-md dialog-card">
<q-card-section>
<div class="text-h5">書き出しファイル名パターン</div>
<div class="text-body2 text-grey-8">
「$キャラ$」のようなタグを使って書き出すファイル名をカスタマイズできます
</div>
</q-card-section>
<q-card-actions class="setting-card q-px-md q-py-sm">
<div class="row full-width justify-between">
<div class="col">
<q-input
dense
outlined
bg-color="background-light"
label="ファイル名パターン"
suffix=".wav"
:maxlength="maxLength"
:error="hasError"
:error-message="errorMessage"
v-model="currentFileNamePattern"
ref="patternInput"
>
<template v-slot:after>
<q-btn
label="デフォルトにリセット"
unelevated
color="background-light"
text-color="display-dark"
class="text-no-wrap q-mr-sm"
@click="resetToDefault"
/>
</template>
</q-input>
</div>
</div>
<div class="text-body2 text-ellipsis">
出力例){{ previewFileName }}
</div>
<div class="row full-width q-my-md">
<q-btn
v-for="tagString in tagStrings"
:key="tagString"
:label="`$${tagString}$`"
unelevated
color="background-light"
text-color="display-dark"
class="text-no-wrap q-mr-sm"
@click="insertTagToCurrentPosition(`$${tagString}$`)"
/>
</div>
<div class="row full-width justify-end">
<q-btn
label="キャンセル"
unelevated
color="background-light"
text-color="display-dark"
class="text-no-wrap text-bold q-mr-sm col-2"
@click="updateOpenDialog(false)"
/>
<q-btn
label="確定"
unelevated
color="background-light"
text-color="display-dark"
class="text-no-wrap text-bold q-mr-sm col-2"
:disable="hasError"
@click="submit"
/>
</div>
</q-card-actions>
</q-card>
</q-dialog>
</template>

<script lang="ts">
import { defineComponent, computed, ref, nextTick } from "vue";
import { QInput } from "quasar";
import { useStore } from "@/store";
import {
buildFileNameFromRawData,
DEFAULT_FILE_NAME_TEMPLATE,
replaceTagIdToTagString,
sanitizeFileName,
} from "@/store/utility";
export default defineComponent({
name: "FileNamePatternDialog",
props: {
openDialog: Boolean,
},
emits: ["update:openDialog"],
setup(props, context) {
const updateOpenDialog = (isOpen: boolean) =>
context.emit("update:openDialog", isOpen);
const store = useStore();
const patternInput = ref<QInput>();
const maxLength = 128;
const tagStrings = Object.values(replaceTagIdToTagString);
const savingSetting = computed(() => store.state.savingSetting);
const currentFileNamePattern = ref(savingSetting.value.fileNamePattern);
const sanitizedFileNamePattern = computed(() =>
sanitizeFileName(currentFileNamePattern.value)
);
const hasInvalidChar = computed(
() => currentFileNamePattern.value !== sanitizedFileNamePattern.value
);
const hasNotIndexTagString = computed(
() =>
!currentFileNamePattern.value.includes(replaceTagIdToTagString["index"])
);
const invalidChar = computed(() => {
if (!hasInvalidChar.value) return "";
const a = currentFileNamePattern.value;
const b = sanitizedFileNamePattern.value;
let diffAt = "";
for (let i = 0; i < a.length; i++) {
if (b[i] !== a[i]) {
diffAt = a[i];
break;
}
}
return diffAt;
});
const errorMessage = computed(() => {
if (currentFileNamePattern.value.length === 0) {
return "何か入力してください";
}
const result: string[] = [];
if (invalidChar.value !== "") {
result.push(
`使用できない文字が含まれています:「${invalidChar.value}」`
);
}
if (previewFileName.value.includes("$")) {
result.push(`不正なタグが存在するか、$が単体で含まれています`);
}
if (hasNotIndexTagString.value) {
result.push(`$${replaceTagIdToTagString["index"]}$は必須です`);
}
return result.join(", ");
});
const hasError = computed(() => errorMessage.value !== "");
const previewFileName = computed(() =>
buildFileNameFromRawData(currentFileNamePattern.value + ".wav")
);
const removeExtension = (str: string) => {
return str.replace(/\.wav$/, "");
};
const initializeInput = () => {
const pattern = savingSetting.value.fileNamePattern;
currentFileNamePattern.value = removeExtension(pattern);
if (currentFileNamePattern.value.length === 0) {
currentFileNamePattern.value = removeExtension(
DEFAULT_FILE_NAME_TEMPLATE
);
}
};
const resetToDefault = () => {
currentFileNamePattern.value = removeExtension(
DEFAULT_FILE_NAME_TEMPLATE
);
patternInput.value?.focus();
};
const insertTagToCurrentPosition = (tag: string) => {
const elem = patternInput.value?.getNativeElement() as HTMLInputElement;
if (elem) {
const text = elem.value;
if (text.length + tag.length > maxLength) {
return;
}
const from = elem.selectionStart ?? 0;
const to = elem.selectionEnd ?? 0;
const newText = text.substring(0, from) + tag + text.substring(to);
currentFileNamePattern.value = newText;
// キャレットの位置を挿入した後の位置にずらす
nextTick(() => {
elem.selectionStart = from + tag.length;
elem.selectionEnd = from + tag.length;
elem.focus();
});
}
};
const submit = async () => {
await store.dispatch("SET_SAVING_SETTING", {
data: {
...savingSetting.value,
fileNamePattern: currentFileNamePattern.value + ".wav",
},
});
updateOpenDialog(false);
};
return {
patternInput,
tagStrings,
maxLength,
updateOpenDialog,
resetToDefault,
insertTagToCurrentPosition,
submit,
initializeInput,
savingSetting,
currentFileNamePattern,
errorMessage,
hasError,
previewFileName,
};
},
});
</script>

<style scoped lang="scss">
@use '@/styles/colors' as colors;
.setting-card {
width: 100%;
min-width: 475px;
background: colors.$setting-item;
}
.text-ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.dialog-card {
width: 700px;
max-width: 80vw;
}
</style>
53 changes: 52 additions & 1 deletion src/components/SettingDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,6 @@
self="center right"
transition-show="jump-left"
transition-hide="jump-right"
v-if="!savingSetting.fixedExportEnabled"
>
音声ファイルを設定したフォルダに書き出す
</q-tooltip>
Expand Down Expand Up @@ -343,6 +342,44 @@
</q-toggle>
</q-card-actions>

<file-name-pattern-dialog
v-model:open-dialog="showsFilePatternEditDialog"
/>

<q-card-actions class="q-px-md q-py-sm bg-setting-item">
<div>書き出しファイル名パターン</div>
<div>
<q-icon
name="help_outline"
color="grey-8"
size="sm"
class="help-hover-icon"
>
<q-tooltip
:delay="500"
anchor="center left"
self="center right"
transition-show="jump-left"
transition-hide="jump-right"
>
書き出すファイル名のパターンをカスタマイズする
</q-tooltip>
</q-icon>
</div>
<q-space />
<div class="q-px-sm text-ellipsis">
{{ savingSetting.fileNamePattern }}
</div>
<q-btn
label="編集"
unelevated
color="background-light"
text-color="display-dark"
class="text-no-wrap q-mr-sm"
@click="showsFilePatternEditDialog = true"
/>
</q-card-actions>

<q-card-actions class="q-px-md q-py-none bg-setting-item">
<div>上書き防止</div>
<div>
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -958,6 +1000,8 @@ export default defineComponent({
store.dispatch("SET_SPLIT_TEXT_WHEN_PASTE", { splitTextWhenPaste });
};
const showsFilePatternEditDialog = ref(false);
return {
settingDialogOpenedComputed,
engineMode,
Expand All @@ -979,6 +1023,7 @@ export default defineComponent({
acceptRetrieveTelemetryComputed,
splitTextWhenPaste,
changeSplitTextWhenPaste,
showsFilePatternEditDialog,
};
},
});
Expand Down Expand Up @@ -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);
}
Expand Down

0 comments on commit 4157aec

Please sign in to comment.