Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 0 additions & 17 deletions src/sing/fileUtils.ts

This file was deleted.

21 changes: 4 additions & 17 deletions src/store/audio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
TuningTranscription,
filterCharacterInfosByStyleType,
DEFAULT_PROJECT_NAME,
generateUniqueFilePath,
} from "./utility";
import { createPartialStore } from "./vuex";
import { determineNextPresetKey } from "./preset";
Expand Down Expand Up @@ -130,20 +131,6 @@ function parseTextFile(
return audioItems;
}

// TODO: src/sing/fileUtils.tsのgenerateUniqueFilePathと統合する
async function changeFileTailToNonExistent(
filePath: string,
extension: string,
) {
let tail = 1;
const name = filePath.slice(0, filePath.length - 1 - extension.length);
while (await window.backend.checkFileExists(filePath)) {
filePath = `${name}[${tail}].${extension}`;
tail += 1;
}
return filePath;
}

export async function writeTextFile(obj: {
filePath: string;
text: string;
Expand Down Expand Up @@ -1396,7 +1383,7 @@ export const audioStore = createPartialStore<AudioStoreTypes>({
}

if (state.savingSetting.avoidOverwrite) {
filePath = await changeFileTailToNonExistent(filePath, "wav");
filePath = await generateUniqueFilePath(filePath, "wav");
}

let fetchAudioResult: FetchAudioResult;
Expand Down Expand Up @@ -1546,7 +1533,7 @@ export const audioStore = createPartialStore<AudioStoreTypes>({
}

if (state.savingSetting.avoidOverwrite) {
filePath = await changeFileTailToNonExistent(filePath, "wav");
filePath = await generateUniqueFilePath(filePath, "wav");
}

const encodedBlobs: string[] = [];
Expand Down Expand Up @@ -1679,7 +1666,7 @@ export const audioStore = createPartialStore<AudioStoreTypes>({
}

if (state.savingSetting.avoidOverwrite) {
filePath = await changeFileTailToNonExistent(filePath, "txt");
filePath = await generateUniqueFilePath(filePath, "txt");
}

const characters = new Map<string, string>();
Expand Down
8 changes: 2 additions & 6 deletions src/store/singing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
DEFAULT_STYLE_NAME,
generateLabelFileData,
PhonemeTimingLabel,
generateUniqueFilePath,
sanitizeFileName,
} from "./utility";
import {
Expand Down Expand Up @@ -105,7 +106,6 @@ import { generateWavFileData } from "@/helpers/fileDataGenerator";
import path from "@/helpers/path";
import { showAlertDialog } from "@/components/Dialog/Dialog";
import { ufProjectFromVoicevox } from "@/sing/utaformatixProject/fromVoicevox";
import { generateUniqueFilePath } from "@/sing/fileUtils";
import {
isMultiFileProjectFormat,
isSingleFileProjectFormat,
Expand Down Expand Up @@ -3263,11 +3263,7 @@ export const singingStore = createPartialStore<SingingStoreTypes>({
if (!filePath) {
return { result: "CANCELED", path: "" };
}
filePath = await generateUniqueFilePath(
// 拡張子を除いたファイル名を取得
filePath.slice(0, -(extension.length + 1)),
extension,
);
filePath = await generateUniqueFilePath(filePath, extension);

return await actions.EXPORT_FILE({
filePath,
Expand Down
35 changes: 35 additions & 0 deletions src/store/utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -541,3 +541,38 @@ export async function generateLabelFileData(labels: PhonemeTimingLabel[]) {

return await generateTextFileData({ text: labString });
}

/**
* 指定されたファイルパスに対応するファイルが既に存在する場合、
* ファイル名に連番のサフィックスを追加してユニークなファイルパスを生成する。
*
* NOTE: extension はドットなし(例: "wav")を想定する。
*/
export async function generateUniqueFilePath(
filePath: string,
extension: string,
): Promise<string> {
let currentPath = filePath;
let baseName: string;

if (extension && filePath.endsWith(`.${extension}`)) {
// 拡張子あり → 拡張子を除いたベース名
baseName = filePath.slice(0, -(extension.length + 1));
} else {
// 拡張子なし → 全体をベース名として扱う
baseName = filePath;

if (extension) {
currentPath = `${filePath}.${extension}`;
}
}

let tail = 1;
while (await window.backend.checkFileExists(currentPath)) {
currentPath = extension
? `${baseName}[${tail}].${extension}`
: `${baseName}[${tail}]`;
tail += 1;
}
return currentPath;
}
82 changes: 81 additions & 1 deletion tests/unit/store/utility.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { it, expect, describe, test } from "vitest";
import { it, expect, describe, test, vi } from "vitest";
import { AccentPhrase, Mora } from "@/openapi";
import {
CharacterInfo,
Expand All @@ -20,6 +20,7 @@ import {
getToolbarButtonName,
isOnCommandOrCtrlKeyDown,
filterCharacterInfosByStyleType,
generateUniqueFilePath,
} from "@/store/utility";
import { uuid4 } from "@/helpers/random";
import { isMac } from "@/helpers/platform";
Expand Down Expand Up @@ -362,3 +363,82 @@ describe("filterCharacterInfosByStyleType", () => {
expect(filtered[2].metas.styles.length).toBe(1);
});
});

describe("generateUniqueFilePath", () => {
it("ファイルが存在しない場合は元のパスを返す", async () => {
const checkFileExists = vi.fn().mockResolvedValue(false);
vi.stubGlobal("window", {
backend: {
checkFileExists,
},
});

const result = await generateUniqueFilePath("test.wav", "wav");
expect(result).toBe("test.wav");
expect(checkFileExists).toHaveBeenCalledWith("test.wav");
});

it("拡張子なしのパスが渡された場合、拡張子を付与してチェックする", async () => {
const checkFileExists = vi.fn().mockResolvedValue(false);
vi.stubGlobal("window", {
backend: {
checkFileExists,
},
});

const result = await generateUniqueFilePath("test", "wav");
expect(result).toBe("test.wav");
expect(checkFileExists).toHaveBeenCalledWith("test.wav");
});

it("ファイルが存在する場合は連番を付与する", async () => {
const checkFileExists = vi
.fn()
.mockResolvedValueOnce(true) // test.wav exists
.mockResolvedValueOnce(false); // test[1].wav does not exist

vi.stubGlobal("window", {
backend: {
checkFileExists,
},
});

const result = await generateUniqueFilePath("test.wav", "wav");
expect(result).toBe("test[1].wav");
expect(checkFileExists).toHaveBeenCalledWith("test.wav");
expect(checkFileExists).toHaveBeenCalledWith("test[1].wav");
});

it("連番ファイルも存在する場合は次の連番を付与する", async () => {
const checkFileExists = vi
.fn()
.mockResolvedValueOnce(true) // test.wav exists
.mockResolvedValueOnce(true) // test[1].wav exists
.mockResolvedValueOnce(false); // test[2].wav does not exist

vi.stubGlobal("window", {
backend: {
checkFileExists,
},
});

const result = await generateUniqueFilePath("test.wav", "wav");
expect(result).toBe("test[2].wav");
});

it("拡張子が空の場合は連番のみ付与する", async () => {
const checkFileExists = vi
.fn()
.mockResolvedValueOnce(true) // test exists
.mockResolvedValueOnce(false); // test[1] does not exist

vi.stubGlobal("window", {
backend: {
checkFileExists,
},
});

const result = await generateUniqueFilePath("test", "");
expect(result).toBe("test[1]");
});
});
Loading