Skip to content
Merged
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
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
158 changes: 98 additions & 60 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,38 +247,52 @@ 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;
}
}
await context.actions.WRITE_PROJECT_FILE({ filePath });

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,
filePath = await context.actions.PROMPT_PROJECT_SAVE_FILE_PATH({
defaultFilePath: filePath,
});
if (ret == undefined) {
if (!filePath) {
return false;
}
filePath = ret;
}

if (
context.state.projectFilePath &&
context.state.projectFilePath != filePath
Expand All @@ -292,62 +307,85 @@ export const projectStore = createPartialStore<ProjectStoreTypes>({
await context.actions.APPEND_RECENTLY_USED_PROJECT({
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.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);
const message = (() => {
if (typeof err === "string") return err;
if (!(err instanceof Error)) return "エラーが発生しました。";
return err.message;
})();
await showAlertDialog({
title: "エラー",
message: `プロジェクトファイルの保存に失敗しました。\n${message}`,
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;
}

// 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: 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