Skip to content

Commit

Permalink
Add: BPM・拍子変更機能を追加 (VOICEVOX#2303)
Browse files Browse the repository at this point in the history
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Sig <[email protected]>
Co-authored-by: Hiroshiba <[email protected]>
  • Loading branch information
5 people authored Dec 6, 2024
1 parent e829c02 commit 34c670c
Show file tree
Hide file tree
Showing 43 changed files with 2,512 additions and 716 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ jobs:
# git diff に表示されるようにする
git add --intent-to-add --all tests/
git diff --binary --cached tests/ > patch-${{ matrix.os }}.diff
git diff --binary HEAD tests/ > patch-${{ matrix.os }}.diff
- name: Upload patch to artifact
if: needs.config.outputs.shouldUpdateSnapshots == 'true'
Expand Down
1,871 changes: 1,292 additions & 579 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@
"vite-plugin-electron": "0.29.0",
"vite-tsconfig-paths": "5.1.2",
"vitest": "2.1.2",
"vue-component-type-helpers": "2.1.6",
"vue-tsc": "2.1.10",
"yargs": "17.2.1"
}
Expand Down
3 changes: 3 additions & 0 deletions src/components/Menu/ContextMenu/Presentation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ defineProps<{
uiLocked?: boolean;
}>();
defineExpose({
show: (event?: MouseEvent | undefined) => {
contextMenu.value?.show(event);
},
hide: () => {
contextMenu.value?.hide();
},
Expand Down
91 changes: 91 additions & 0 deletions src/components/Sing/ChangeValueDialog/CommonDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<!--
テンポや拍子などを変更・追加するためのダイアログ
-->
<template>
<QDialog ref="dialogRef" v-model="modelValue">
<QCard class="q-py-sm q-px-md dialog-card">
<QCardSection>
<div class="text-h5">
{{ props.title }}
</div>
</QCardSection>

<QSeparator />

<QCardSection>
<QCardActions>
<div>{{ props.name }}</div>
<QSpace />
<slot />
</QCardActions>
</QCardSection>

<QSeparator />

<QCardActions>
<QSpace />
<QBtn
unelevated
label="キャンセル"
color="surface"
textColor="display"
class="text-no-wrap text-bold q-mr-sm"
@click="handleCancel"
/>
<QBtn
unelevated
:label="okText"
color="primary"
textColor="display-on-primary"
class="text-no-wrap text-bold q-mr-sm"
@click="handleOk"
/>
</QCardActions>
</QCard>
</QDialog>
</template>

<script setup lang="ts">
import { useDialogPluginComponent } from "quasar";
import { computed } from "vue";

const { dialogRef, onDialogOK, onDialogCancel } = useDialogPluginComponent();

const modelValue = defineModel<boolean>();
const props = defineProps<{
title: string;
mode: "add" | "edit";
name: string;
}>();
defineSlots<{
default(props: Record<string, never>): void;
}>();

defineEmits({
...useDialogPluginComponent.emitsObject,
});

const okText = computed(() =>
props.mode === "edit" ? "変更する" : "追加する",
);

const handleOk = () => {
onDialogOK();
};

const handleCancel = () => {
onDialogCancel();
modelValue.value = false;
};
</script>

<style scoped lang="scss">
.dialog-card {
width: 400px;
max-width: 40vw;
}

.value-input {
width: 60px;
}
</style>
103 changes: 103 additions & 0 deletions src/components/Sing/ChangeValueDialog/TempoChangeDialog.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { userEvent, within, expect, fn } from "@storybook/test";
import { Meta, StoryObj } from "@storybook/vue3";
import TempoChangeDialog from "./TempoChangeDialog.vue";
import { DEFAULT_BPM } from "@/sing/domain";

const meta: Meta<typeof TempoChangeDialog> = {
component: TempoChangeDialog,
args: {
modelValue: true,
tempoChange: {
bpm: DEFAULT_BPM,
},
mode: "add",

onOk: fn(),
onHide: fn(),
},
tags: ["!autodocs"], // ダイアログ系はautodocsのプレビューが正しく表示されないので無効化
};

export default meta;
type Story = StoryObj<typeof meta>;

export const CreateOpened: Story = {
name: "開いている:追加",
args: {
modelValue: true,
mode: "add",
},
};
export const ChangeOpened: Story = {
name: "開いている:変更",
args: {
modelValue: true,
tempoChange: {
bpm: 120,
},
mode: "edit",
},
};

export const ClickOk: Story = {
name: "OKボタンを押す:追加",
args: { ...CreateOpened.args },
play: async ({ args }) => {
const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う

const input = canvas.getByLabelText("テンポ");
await userEvent.clear(input);
await userEvent.type(input, "100");

const button = canvas.getByRole("button", { name: /追加する/ });
await userEvent.click(button);

await expect(args["onOk"]).toBeCalledWith({
tempoChange: {
bpm: 100,
},
});
},
};

export const ClickDelete: Story = {
name: "OKボタンを押す:編集",
args: { ...ChangeOpened.args },
play: async ({ args }) => {
const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う

const input = canvas.getByLabelText("テンポ");
await userEvent.clear(input);
await userEvent.type(input, "100");

const button = canvas.getByRole("button", { name: /変更する/ });
await userEvent.click(button);

await expect(args["onOk"]).toBeCalledWith({
tempoChange: {
bpm: 100,
},
});
},
};

export const CancelClose: Story = {
name: "キャンセルボタンを押す",
args: { ...ChangeOpened.args },
play: async ({ args }) => {
const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う

const button = canvas.getByRole("button", { name: /キャンセル/ });
await userEvent.click(button);

await expect(args["onOk"]).not.toBeCalled();
},
};

export const Closed: Story = {
name: "閉じている",
tags: ["skip-screenshot"],
args: {
modelValue: false,
},
};
44 changes: 44 additions & 0 deletions src/components/Sing/ChangeValueDialog/TempoChangeDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<template>
<CommonDialog
v-model="modelValue"
:title="props.mode === 'add' ? 'テンポの追加' : 'テンポの編集'"
name="テンポ"
:mode="props.mode"
@ok="() => $emit('ok', { tempoChange })"
@hide="() => $emit('hide')"
>
<QInput
v-model.number="tempoChange.bpm"
type="number"
dense
hideBottomSpace
class="value-input"
aria-label="テンポ"
/>
</CommonDialog>
</template>

<script setup lang="ts">
import { QInput, useDialogPluginComponent } from "quasar";
import { ref } from "vue";
import CommonDialog from "./CommonDialog.vue";
import { Tempo } from "@/store/type";
import { cloneWithUnwrapProxy } from "@/helpers/cloneWithUnwrapProxy";
const modelValue = defineModel<boolean>();
const props = defineProps<{
tempoChange: Omit<Tempo, "position">;
mode: "add" | "edit";
}>();
defineEmits({
...useDialogPluginComponent.emitsObject,
});
const tempoChange = ref(cloneWithUnwrapProxy(props.tempoChange));
</script>

<style scoped lang="scss">
.value-input {
width: 60px;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { userEvent, within, expect, fn } from "@storybook/test";
import { Meta, StoryObj } from "@storybook/vue3";
import TimeSignatureChangeDialog from "./TimeSignatureChangeDialog.vue";
import { DEFAULT_BEAT_TYPE, DEFAULT_BEATS } from "@/sing/domain";

const meta: Meta<typeof TimeSignatureChangeDialog> = {
component: TimeSignatureChangeDialog,
args: {
modelValue: true,
timeSignatureChange: {
beats: DEFAULT_BEATS,
beatType: DEFAULT_BEAT_TYPE,
},
mode: "add",

onOk: fn(),
onHide: fn(),
},
tags: ["!autodocs"], // ダイアログ系はautodocsのプレビューが正しく表示されないので無効化
};

export default meta;
type Story = StoryObj<typeof meta>;

export const CreateOpened: Story = {
name: "開いている:追加",
args: {
modelValue: true,
mode: "add",
},
};
export const ChangeOpened: Story = {
name: "開いている:変更",
args: {
modelValue: true,
timeSignatureChange: {
beats: 4,
beatType: 4,
},
mode: "edit",
},
};

export const ClickOk: Story = {
name: "OKボタンを押す:追加",
args: { ...CreateOpened.args },
play: async ({ args }) => {
const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う

const input = canvas.getByLabelText("拍子の分子");
await userEvent.clear(input);
await userEvent.type(input, "3");

const button = canvas.getByRole("button", { name: /追加する/ });
await userEvent.click(button);

await expect(args["onOk"]).toBeCalledWith({
timeSignatureChange: {
beats: 3,
beatType: 4,
},
});
},
};

export const ClickDelete: Story = {
name: "OKボタンを押す:編集",
args: { ...ChangeOpened.args },
play: async ({ args }) => {
const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う

const input = canvas.getByLabelText("拍子の分子");
await userEvent.clear(input);
await userEvent.type(input, "6");

const button = canvas.getByRole("button", { name: /変更する/ });
await userEvent.click(button);

await expect(args["onOk"]).toBeCalledWith({
timeSignatureChange: {
beats: 6,
beatType: 4,
},
});
},
};

export const CancelClose: Story = {
name: "キャンセルボタンを押す",
args: { ...ChangeOpened.args },
play: async ({ args }) => {
const canvas = within(document.body); // ダイアログなので例外的にdocument.bodyを使う

const button = canvas.getByRole("button", { name: /キャンセル/ });
await userEvent.click(button);

await expect(args["onOk"]).not.toBeCalled();
},
};

export const Closed: Story = {
name: "閉じている",
tags: ["skip-screenshot"],
args: {
modelValue: false,
},
};
Loading

0 comments on commit 34c670c

Please sign in to comment.