Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
18 changes: 18 additions & 0 deletions src/components/Menu/MenuBar/MenuBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ const saveProjectAs = async () => {
}
};

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

const importProject = () => {
if (!uiLocked.value) {
void store.actions.LOAD_PROJECT_FILE({ type: "dialog" });
Expand Down Expand Up @@ -338,6 +344,14 @@ const menudata = computed<MenuItemData[]>(() => [
},
disableWhenUiLocked: true,
},
{
type: "button",
label: "プロジェクトを複製として保存",
onClick: async () => {
await saveProjectCopy();
},
disableWhenUiLocked: true,
},
{
type: "button",
label: "プロジェクトを読み込む",
Expand Down Expand Up @@ -585,6 +599,10 @@ registerHotkeyForAllEditors({
callback: saveProjectAs,
name: "プロジェクトを名前を付けて保存",
});
registerHotkeyForAllEditors({
callback: saveProjectCopy,
name: "プロジェクトを複製として保存",
});
registerHotkeyForAllEditors({
callback: importProject,
name: "プロジェクトを読み込む",
Expand Down
11 changes: 8 additions & 3 deletions src/domain/hotkeyAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ export const hotkeyActionNameSchema = z.enum([
"元に戻す",
"やり直す",
"新規プロジェクト",
"プロジェクトを名前を付けて保存",
"プロジェクトを上書き保存",
"プロジェクトを名前を付けて保存",
"プロジェクトを複製として保存",
"プロジェクトを読み込む",
"テキストを読み込む",
"全体のイントネーションをリセット",
Expand Down Expand Up @@ -146,13 +147,17 @@ export function getDefaultHotkeySettings({
action: "全画面表示を切り替え",
combination: HotkeyCombination(!isMac ? "F11" : "Ctrl Meta F"),
},
{
action: "プロジェクトを上書き保存",
combination: HotkeyCombination(!isMac ? "Ctrl S" : "Meta S"),
},
{
action: "プロジェクトを名前を付けて保存",
combination: HotkeyCombination(!isMac ? "Ctrl Shift S" : "Shift Meta S"),
},
{
action: "プロジェクトを上書き保存",
combination: HotkeyCombination(!isMac ? "Ctrl S" : "Meta S"),
action: "プロジェクトを複製として保存",
combination: HotkeyCombination(!isMac ? "Ctrl Alt S" : "Alt Meta S"),
},
{
action: "プロジェクトを読み込む",
Expand Down
199 changes: 114 additions & 85 deletions src/store/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
} 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 @@ -246,108 +247,136 @@ export const projectStore = createPartialStore<ProjectStoreTypes>({
),
},

SAVE_PROJECT_FILE: {
SAVE_PROJECT_FILE_AS_COPY: {
/**
* プロジェクトファイルを保存する。保存の成否が返る。
* プロジェクトファイルを複製として保存する。保存の成否が返る。
* エラー発生時はダイアログが表示される。
*/
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;
}
}

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

SAVE_PROJECT_FILE: {
/**
* プロジェクトファイルを保存し、現在のプロジェクトファイルのパスを更新する。保存の成否が返る。
*/
action: createUILockAction(
async (context, { overwrite }: { overwrite?: boolean }) => {
let filePath = context.state.projectFilePath;
try {
if (!overwrite || !filePath) {
let defaultPath: string;

if (!filePath) {
// 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 = filePath;
}

// Write the current status to a project file.
const ret = await window.backend.showSaveFileDialog({
title: "プロジェクトファイルの保存",
name: "VOICEVOX Project file",
extensions: ["vvproj"],
defaultPath,
});
if (ret == undefined) {
return false;
}
filePath = ret;
}
if (
context.state.projectFilePath &&
context.state.projectFilePath != filePath
) {
await showMessageDialog({
type: "info",
title: "保存",
message: `編集中のプロジェクトが ${filePath} に切り替わりました。`,
});
if (!overwrite || !filePath) {
filePath = await context.actions.PROMPT_PROJECT_SAVE_FILE_PATH({
defaultFilePath: filePath,
});
if (!filePath) {
return false;
}

await context.actions.APPEND_RECENTLY_USED_PROJECT({
filePath,
}
if (
context.state.projectFilePath &&
context.state.projectFilePath != filePath
) {
await showMessageDialog({
type: "info",
title: "保存",
message: `編集中のプロジェクトが ${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: buf,
})
.then(getValueOrThrow);
}

await context.actions.APPEND_RECENTLY_USED_PROJECT({
filePath,
});
const result = await context.actions.SAVE_PROJECT_FILE_AS_COPY({
filePath,
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SAVE_PROJECT_FILEの中でSAVE_PROJECT_FILE_AS_COPYを呼んでますが、実はこれ危なかったりします。
これらの関数は今偶然コードが一致しているだけで、役割が異なるはずなためです。

例として、「複製を保存」のときだけ特別な処理、例えば「複製を{path}に保存しました!」というダイアログを表示する変更を後で加えたくなったとします。
この処理は関数SAVE_PROJECT_FILE_AS_COPYにとって不自然ではないので、後から実装する人は問題ないと思って実装します。
でも実はこの関数はSAVE_PROJECT_FILEから依存されているので、例えば上書き保存したときも「複製を保存しました!」というダイアログが表示されてしまうバグに繋がるんですね。

もう少し抽象的な見方をすると、はSOLID原則のO、オープン・クローズドの原則(コードは拡張に開いていて、修正に閉じるべき)に反します。
「拡張に開く」は「コード変更は簡単にできるよう作るべき」の意味で、
「修正に閉じる」は「コード変更したときに他のものに影響を与えないべき」の意味です。
SAVE_PROJECT_FILE_AS_COPYのコードはクローズド原則に反して、この関数には問題ない変更をした場合もSAVE_PROJECT_FILEをバグらせてしまうんですね。

この原則を意識する方法は簡単で、関数を名前(=目的)に背いた使い方をしなければOKです。
「SAVE_PROJECT_FILE_AS_COPY」は「複製を保存する」関数で、SAVE_PROJECT_FILE複製を保存したいわけじゃないはず。
なのでたとえば今のSAVE_PROJECT_FILE_AS_COPYと全く同じ処理をする関数を切り出し、「プロジェクトファイル未指定ならファイル選択ダイアログを開き、指定したファイルに書き込んで、エラーが起きたらダイアログを表示する」ような名前を付ければ共通化してOKとなります。

ちなみにこの関数の名付けは難しいんですよね。
こういうときは大抵より良い共通化ができたりします。
マージ後にPR送るので見ていただければ!!

とりあえず一旦FIXMEコメントを書くのでどうでしょうか。

Suggested change
const result = await context.actions.SAVE_PROJECT_FILE_AS_COPY({
filePath,
});
// FIXME: SAVE_PROJECT_FILE_AS_COPYへの依存をやめる
const result = await context.actions.SAVE_PROJECT_FILE_AS_COPY({
filePath,
});

if (result) {
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);
const message = (() => {
if (typeof err === "string") return err;
if (!(err instanceof Error)) return "エラーが発生しました。";
return err.message;
})();
await showAlertDialog({
title: "エラー",
message: `プロジェクトファイルの保存に失敗しました。\n${message}`,
});
return false;
}

return result;
},
),
},

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;
}

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

WRITE_PROJECT_FILE: {
action: createUILockAction(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: buf,
})
.then(getValueOrThrow);
}),
},

/**
* プロジェクトファイルを保存するか破棄するかキャンセルするかのダイアログを出して、保存する場合は保存する。
* 何を選択したかが返る。
Expand Down
12 changes: 12 additions & 0 deletions src/store/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1853,6 +1853,18 @@ export type ProjectStoreTypes = {
action(payload: { overwrite?: boolean }): boolean;
};

SAVE_PROJECT_FILE_AS_COPY: {
action(payload: { filePath?: string }): boolean;
};

PROMPT_PROJECT_SAVE_FILE_PATH: {
action(payload: { defaultFilePath?: string }): Promise<string | undefined>;
};

WRITE_PROJECT_FILE: {
action(payload: { filePath: string }): Promise<void>;
};

SAVE_OR_DISCARD_PROJECT_FILE: {
action(palyoad: {
additionalMessage?: string;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading