Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5090efc
feat: 複製として保存を追加
sevenc-nanashi Feb 27, 2025
e8f2fdc
test: snapshotを更新
sevenc-nanashi Feb 27, 2025
e1d6cae
test: .env.test.localが上書きしていた...
sevenc-nanashi Feb 27, 2025
79f66d9
chore: 順序を揃える
sevenc-nanashi Feb 27, 2025
02d5074
chore: 関数を分割
sevenc-nanashi Feb 27, 2025
3f80b21
refactor: プロジェクトファイルを保存する関数をユースケースに応じて3つに分割
Hiroshiba Feb 27, 2025
58fd8cb
project/index.tsに移動
Hiroshiba Feb 27, 2025
320318f
なくてもわかるコメントを省く
Hiroshiba Feb 27, 2025
aa913da
promptProjectSaveFilePathとwriteProjectFileを切り出し
Hiroshiba Feb 27, 2025
ac26e8e
updateCurrentProject関数を切り出し
Hiroshiba Feb 27, 2025
8cdce16
Merge branch 'main' into refactor--プロジェクトファイルを保存する関数をユースケースに応じて3つに分割
Hiroshiba Feb 27, 2025
a4edecc
エラーダイアログ表示のためのshowErrorDialog関数を追加
Hiroshiba Feb 27, 2025
c7b2843
変更漏れ
Hiroshiba Feb 27, 2025
f5b44fb
saveProjectHelperからのインポートを整理し、重複を削除
Hiroshiba Feb 27, 2025
f1f5518
Merge branch 'main' into refactor--プロジェクトファイルを保存する関数をユースケースに応じて3つに分割
Hiroshiba Feb 27, 2025
7ec83a9
Merge remote-tracking branch 'upstream/main' into refactor--プロジェクトファイ…
Hiroshiba Feb 27, 2025
bd26294
refactor: 不要な引数を削除し、プロジェクトファイル保存機能を簡素化
Hiroshiba Feb 27, 2025
a01bb0d
refactor: 不要な引数を削除し、プロジェクトファイル保存機能を簡素化
Hiroshiba Feb 27, 2025
5ecfbc7
refactor: 不要な引数を削除し、プロジェクト保存機能を簡素化
Hiroshiba Feb 27, 2025
ba07b36
refactor: 不要な引数を削除し、プロジェクトファイル保存機能を簡素化
Hiroshiba Feb 27, 2025
71e90b7
refactor: 現在のプロジェクト保存関数の名前を変更し、コードの可読性を向上
Hiroshiba Feb 27, 2025
039b119
refactor: ResultErrorクラスのコンストラクタを修正
Hiroshiba Feb 27, 2025
b597935
Merge remote-tracking branch 'upstream/main' into refactor--プロジェクトファイ…
Hiroshiba Mar 18, 2025
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
9 changes: 9 additions & 0 deletions src/components/Dialog/Dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "@/store/type";
import { DotNotationDispatch } from "@/store/vuex";
import { withProgress } from "@/store/ui";
import { errorToMessage } from "@/helpers/errorHelper";

type MediaType = "audio" | "text" | "project" | "label";

Expand Down Expand Up @@ -86,6 +87,14 @@ export const showAlertDialog = async (
});
};

/** 例外からエラーダイアログを表示する便利関数 */
export const showErrorDialog = async (title: string, e: unknown) => {
return showAlertDialog({
title,
message: errorToMessage(e),
});
};

/** 続行することが望まれそうな場合の質問ダイアログ */
export const showConfirmDialog = async (options: ConfirmDialogOptions) => {
options.cancel ??= "キャンセル";
Expand Down
6 changes: 3 additions & 3 deletions src/components/Menu/MenuBar/MenuBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -142,19 +142,19 @@ const createNewProject = async () => {

const saveProject = async () => {
if (!uiLocked.value) {
await store.actions.SAVE_PROJECT_FILE({ overwrite: true });
await store.actions.SAVE_PROJECT_FILE_OVERWRITE();
}
};

const saveProjectAs = async () => {
if (!uiLocked.value) {
await store.actions.SAVE_PROJECT_FILE({});
await store.actions.SAVE_PROJECT_FILE_AS();
}
};

const saveProjectCopy = async () => {
if (!uiLocked.value) {
await store.actions.SAVE_PROJECT_FILE_AS_COPY({});
await store.actions.SAVE_PROJECT_FILE_AS_COPY();
}
};

Expand Down
2 changes: 1 addition & 1 deletion src/components/Talk/ToolBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ const generateAndConnectAndSaveAudio = async () => {
});
};
const saveProject = async () => {
await store.actions.SAVE_PROJECT_FILE({ overwrite: true });
await store.actions.SAVE_PROJECT_FILE_OVERWRITE();
};
const importTextFile = () => {
void store.actions.COMMAND_IMPORT_FROM_FILE({ type: "dialog" });
Expand Down
192 changes: 62 additions & 130 deletions src/store/project.ts → src/store/project/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { createPartialStore, DotNotationDispatch } from "./vuex";
import { createPartialStore, DotNotationDispatch } from "../vuex";
import {
executeWritePromiseOrDialog,
promptProjectSaveFilePath,
markCurrentProjectAsSaved,
writeProjectFile,
} from "./saveProjectHelper";
import { createUILockAction } from "@/store/ui";
import {
AllActions,
Expand Down Expand Up @@ -28,8 +34,6 @@ import {
showQuestionDialog,
} from "@/components/Dialog/Dialog";
import { uuid4 } from "@/helpers/random";
import { getAppInfos } from "@/domain/appInfo";
import { errorToMessage } from "@/helpers/errorHelper";

export const projectStoreState: ProjectStoreState = {
savedLastCommandIds: { talk: null, song: null },
Expand Down Expand Up @@ -247,143 +251,73 @@ export const projectStore = createPartialStore<ProjectStoreTypes>({
),
},

SAVE_PROJECT_FILE_AS_COPY: {
SAVE_PROJECT_FILE_OVERWRITE: {
/**
* プロジェクトファイルを複製として保存する。保存の成否が返る。
* プロジェクトファイルを上書き保存する。
* 現在のプロジェクトファイルが未設定の場合は名前をつけて保存する。
* ファイルを保存できた場合はtrueが、キャンセルしたか例外が発生した場合はfalseが返る。
* エラー発生時はダイアログが表示される。
*/
action: createUILockAction(async (context, { filePath: filePathArg }) => {
let filePath = filePathArg;
try {
if (!filePath) {
filePath = await context.actions.PROMPT_PROJECT_SAVE_FILE_PATH({});
if (!filePath) {
return false;
}
}
await context.actions.WRITE_PROJECT_FILE({ filePath });

return true;
} catch (err) {
window.backend.logError(err);
await showAlertDialog({
title: "エラー",
message: `プロジェクトファイルの保存に失敗しました。\n${errorToMessage(err)}`,
});
return false;
action: createUILockAction(async (context) => {
const filePath = context.state.projectFilePath;
if (!filePath) {
return await context.actions.SAVE_PROJECT_FILE_AS();
}

const result = await executeWritePromiseOrDialog(
writeProjectFile(context, filePath),
);
if (!result) return false;

await markCurrentProjectAsSaved(context, filePath);
return true;
}),
},

SAVE_PROJECT_FILE: {
SAVE_PROJECT_FILE_AS: {
/**
* プロジェクトファイルを保存し、現在のプロジェクトファイルのパスを更新する。保存の成否が返る。
* プロジェクトファイルを名前をつけて保存し、現在のプロジェクトファイルのパスを更新する。
* ファイルを保存できた場合はtrueが、キャンセルしたか例外が発生した場合はfalseが返る。
* エラー発生時はダイアログが表示される。
*/
action: createUILockAction(
async (context, { overwrite }: { overwrite?: boolean }) => {
let filePath = context.state.projectFilePath;

try {
if (!overwrite || !filePath) {
filePath = await context.actions.PROMPT_PROJECT_SAVE_FILE_PATH({
defaultFilePath: filePath,
});
if (!filePath) {
return false;
}
}

if (
context.state.projectFilePath &&
context.state.projectFilePath != filePath
) {
await showMessageDialog({
type: "info",
title: "保存",
message: `編集中のプロジェクトが ${filePath} に切り替わりました。`,
});
}

await context.actions.APPEND_RECENTLY_USED_PROJECT({
filePath,
});
await context.actions.WRITE_PROJECT_FILE({ filePath });

context.mutations.SET_PROJECT_FILEPATH({ filePath });
context.mutations.SET_SAVED_LAST_COMMAND_IDS(
context.getters.LAST_COMMAND_IDS,
);

return true;
} catch (err) {
window.backend.logError(err);
await showAlertDialog({
title: "エラー",
message: `プロジェクトファイルの保存に失敗しました。\n${errorToMessage(err)}`,
});
return false;
}
},
),
},

PROMPT_PROJECT_SAVE_FILE_PATH: {
async action(context, { defaultFilePath }) {
let defaultPath: string;

if (!defaultFilePath) {
// if new project: use generated name
defaultPath = `${context.getters.DEFAULT_PROJECT_FILE_BASE_NAME}.vvproj`;
} else {
// if saveAs for existing project: use current project path
defaultPath = defaultFilePath;
action: createUILockAction(async (context) => {
const filePath = await promptProjectSaveFilePath(context);
if (!filePath) return false;

const result = await executeWritePromiseOrDialog(
writeProjectFile(context, filePath),
);
if (!result) return false;

if (context.state.projectFilePath !== filePath) {
context.mutations.SET_PROJECT_FILEPATH({ filePath });
await showMessageDialog({
type: "info",
title: "保存",
message: `編集中のプロジェクトが ${filePath} に切り替わりました。`,
});
}

// Write the current status to a project file.
return await window.backend.showSaveFileDialog({
title: "プロジェクトファイルの保存",
name: "VOICEVOX Project file",
extensions: ["vvproj"],
defaultPath,
});
},
await markCurrentProjectAsSaved(context, filePath);
return true;
}),
},

WRITE_PROJECT_FILE: {
action: async (context, { filePath }) => {
const appVersion = getAppInfos().version;
const {
audioItems,
audioKeys,
tpqn,
tempos,
timeSignatures,
tracks,
trackOrder,
} = context.state;
const projectData: LatestProjectType = {
appVersion,
talk: {
audioKeys,
audioItems,
},
song: {
tpqn,
tempos,
timeSignatures,
tracks: Object.fromEntries(tracks),
trackOrder,
},
};

const buf = new TextEncoder().encode(JSON.stringify(projectData)).buffer;
await window.backend
.writeFile({
filePath,
buffer: new Uint8Array(buf),
})
.then(getValueOrThrow);
},
SAVE_PROJECT_FILE_AS_COPY: {
/**
* プロジェクトファイルを複製として保存する。
* ファイルを保存できた場合はtrueが、キャンセルしたか例外が発生した場合はfalseが返る。
* エラー発生時はダイアログが表示される。
*/
action: createUILockAction(async (context) => {
const filePath = await promptProjectSaveFilePath(context);
if (!filePath) return false;

const result = await executeWritePromiseOrDialog(
writeProjectFile(context, filePath),
);
return result;
}),
},

/**
Expand All @@ -410,9 +344,7 @@ export const projectStore = createPartialStore<ProjectStoreTypes>({
cancel: 0,
});
if (result == 2) {
const saved = await actions.SAVE_PROJECT_FILE({
overwrite: true,
});
const saved = await actions.SAVE_PROJECT_FILE_OVERWRITE();
return saved ? "saved" : "canceled";
} else if (result == 1) {
return "discarded";
Expand Down
88 changes: 88 additions & 0 deletions src/store/project/saveProjectHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { ActionContext } from "../type";
import { showErrorDialog } from "@/components/Dialog/Dialog";
import { getAppInfos } from "@/domain/appInfo";
import { LatestProjectType } from "@/domain/project/schema";
import { DisplayableError } from "@/helpers/errorHelper";
import { ResultError } from "@/type/result";

export async function promptProjectSaveFilePath(
context: ActionContext,
): Promise<string | undefined> {
const defaultPath = `${context.getters.DEFAULT_PROJECT_FILE_BASE_NAME}.vvproj`;

return await window.backend.showSaveFileDialog({
title: "プロジェクトファイルの保存",
name: "VOICEVOX Project file",
extensions: ["vvproj"],
defaultPath,
});
}

/**
* @throws ファイルの保存に失敗した場合
*/
export async function writeProjectFile(
context: ActionContext,
filePath: string,
) {
const appVersion = getAppInfos().version;
const {
audioItems,
audioKeys,
tpqn,
tempos,
timeSignatures,
tracks,
trackOrder,
} = context.state;
const projectData: LatestProjectType = {
appVersion,
talk: {
audioKeys,
audioItems,
},
song: {
tpqn,
tempos,
timeSignatures,
tracks: Object.fromEntries(tracks),
trackOrder,
},
};

const buf = new TextEncoder().encode(JSON.stringify(projectData)).buffer;
const result = await window.backend.writeFile({
filePath,
buffer: new Uint8Array(buf),
});
if (!result.ok) {
throw new DisplayableError("ファイルの保存に失敗しました。", {
cause: new ResultError(result),
});
}
}

export async function executeWritePromiseOrDialog(
savePromise: Promise<void>,
): Promise<boolean> {
try {
await savePromise;
return true;
} catch (e) {
window.backend.logError(e);
await showErrorDialog("プロジェクトファイルの保存に失敗しました", e);
return false;
}
}

export async function markCurrentProjectAsSaved(
context: ActionContext,
filePath: string,
) {
await context.actions.APPEND_RECENTLY_USED_PROJECT({
filePath,
});
context.mutations.SET_SAVED_LAST_COMMAND_IDS(
context.getters.LAST_COMMAND_IDS,
);
}
Loading
Loading