From 33167d88db48b96c089ec4dff7a0acfc3e69a2e8 Mon Sep 17 00:00:00 2001 From: Nanashi <sevenc7c@sevenc7c.com> Date: Wed, 14 Feb 2024 03:23:32 +0900 Subject: [PATCH] =?UTF-8?q?[=E3=82=BD=E3=83=B3=E3=82=B0]=20Improve:=20?= =?UTF-8?q?=E3=82=A8=E3=83=B3=E3=82=B8=E3=83=B3=E8=B5=B7=E5=8B=95=E5=89=8D?= =?UTF-8?q?=E3=81=AE=E7=94=BB=E9=9D=A2=E3=82=92=E8=89=AF=E3=81=84=E6=84=9F?= =?UTF-8?q?=E3=81=98=E3=81=AB=E3=81=99=E3=82=8B=20(#1790)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add: エンジン起動中のポップアップを追加 * Refactor: 共通化 * Change: slotを使わないように * Update: スタイルを移動 * Add: スケルトンを追加 * Fix: 細かいところを修正 * Fix: スタイルが消えてたので修正 * Change: EngineStartupPopup -> EngineStartupOverlay * Change: コンポーネントを分割 * Change: ToolBarは無効化で表現するように * Fix: スクロールバーが消えてたのを修正 * Delete: 未使用のスタイルを削除 * Delete: 未使用のスタイルを削除 --------- Co-authored-by: Hiroshiba <hihokaruta@gmail.com> --- src/components/EngineStartupOverlay.vue | 126 +++++++++++++++++ .../MenuButton.vue} | 38 +++-- .../CharacterMenuButton/SelectedCharacter.vue | 132 ++++++++++++++++++ src/components/Sing/SingEditor.vue | 3 + src/components/Sing/ToolBar.vue | 100 +------------ src/components/Talk/TalkEditor.vue | 117 +--------------- 6 files changed, 286 insertions(+), 230 deletions(-) create mode 100644 src/components/EngineStartupOverlay.vue rename src/components/Sing/{CharacterMenuButton.vue => CharacterMenuButton/MenuButton.vue} (93%) create mode 100644 src/components/Sing/CharacterMenuButton/SelectedCharacter.vue diff --git a/src/components/EngineStartupOverlay.vue b/src/components/EngineStartupOverlay.vue new file mode 100644 index 0000000000..ba78ca8048 --- /dev/null +++ b/src/components/EngineStartupOverlay.vue @@ -0,0 +1,126 @@ +<template> + <!-- TODO: 複数エンジン対応 --> + <!-- TODO: allEngineStateが "ERROR" のときエラーになったエンジンを探してトーストで案内 --> + <div v-if="allEngineState === 'FAILED_STARTING'" class="waiting-engine"> + <div>エンジンの起動に失敗しました。エンジンの再起動をお試しください。</div> + </div> + <div + v-else-if=" + !props.isCompletedInitialStartup || allEngineState === 'STARTING' + " + class="waiting-engine" + > + <div> + <q-spinner color="primary" size="2.5rem" /> + <div class="q-mt-xs"> + {{ + allEngineState === "STARTING" + ? "エンジン起動中・・・" + : "データ準備中・・・" + }} + </div> + + <template v-if="isEngineWaitingLong"> + <q-separator spaced /> + エンジン起動に時間がかかっています。<br /> + <q-btn + v-if="isMultipleEngine" + outline + :disable="reloadingLocked" + @click="reloadAppWithMultiEngineOffMode" + > + マルチエンジンをオフにして再読み込みする</q-btn + > + <q-btn v-else outline @click="openQa">Q&Aを見る</q-btn> + </template> + </div> + </div> +</template> +<script setup lang="ts"> +import { computed, ref, watch } from "vue"; +import { useStore } from "@/store"; +import { EngineState } from "@/store/type"; + +const store = useStore(); +const props = + defineProps<{ + isCompletedInitialStartup: boolean; + }>(); + +const reloadingLocked = computed(() => store.state.reloadingLock); +const isMultipleEngine = computed(() => store.state.engineIds.length > 1); + +// エンジン待機 +// TODO: 個別のエンジンの状態をUIで確認できるようにする +const allEngineState = computed(() => { + const engineStates = store.state.engineStates; + + let lastEngineState: EngineState | undefined = undefined; + + // 登録されているすべてのエンジンについて状態を確認する + for (const engineId of store.state.engineIds) { + const engineState: EngineState | undefined = engineStates[engineId]; + if (engineState == undefined) + throw new Error(`No such engineState set: engineId == ${engineId}`); + + // FIXME: 1つでも接続テストに成功していないエンジンがあれば、暫定的に起動中とする + if (engineState === "STARTING") { + return engineState; + } + + lastEngineState = engineState; + } + + return lastEngineState; // FIXME: 暫定的に1つのエンジンの状態を返す +}); + +const isEngineWaitingLong = ref<boolean>(false); +let engineTimer: number | undefined = undefined; +watch(allEngineState, (newEngineState) => { + if (engineTimer != undefined) { + clearTimeout(engineTimer); + engineTimer = undefined; + } + if (newEngineState === "STARTING") { + isEngineWaitingLong.value = false; + engineTimer = window.setTimeout(() => { + isEngineWaitingLong.value = true; + }, 30000); + } else { + isEngineWaitingLong.value = false; + } +}); + +const reloadAppWithMultiEngineOffMode = () => { + store.dispatch("CHECK_EDITED_AND_NOT_SAVE", { + closeOrReload: "reload", + isMultiEngineOffMode: true, + }); +}; + +const openQa = () => { + window.open("https://voicevox.hiroshiba.jp/qa/", "_blank"); +}; +</script> + +<style scoped lang="scss"> +@use '@/styles/colors' as colors; +@use '@/styles/variables' as vars; + +.waiting-engine { + background-color: rgba(colors.$display-rgb, 0.15); + position: absolute; + inset: 0; + z-index: 10; + display: flex; + text-align: center; + align-items: center; + justify-content: center; + > div { + color: colors.$display; + background: colors.$surface; + border-radius: 6px; + padding: 14px; + } +} +</style> diff --git a/src/components/Sing/CharacterMenuButton.vue b/src/components/Sing/CharacterMenuButton/MenuButton.vue similarity index 93% rename from src/components/Sing/CharacterMenuButton.vue rename to src/components/Sing/CharacterMenuButton/MenuButton.vue index a9e8b32598..0c9ff8165d 100644 --- a/src/components/Sing/CharacterMenuButton.vue +++ b/src/components/Sing/CharacterMenuButton/MenuButton.vue @@ -1,6 +1,10 @@ <template> - <q-btn flat class="q-pa-none"> - <slot></slot> + <q-btn flat class="q-pa-none" :disable="uiLocked"> + <selected-character + :show-skeleton="showSkeleton" + :selected-character-info="selectedCharacterInfo" + :selected-singer="selectedSinger" + /> <q-menu class="character-menu" transition-show="none" @@ -134,15 +138,22 @@ </q-btn> </template> +<script lang="ts"> +export default { + name: "CharacterMenuButton", +}; +</script> <script setup lang="ts"> import { computed, ref } from "vue"; import { debounce } from "quasar"; +import SelectedCharacter from "./SelectedCharacter.vue"; import { useStore } from "@/store"; import { base64ImageToUri } from "@/helpers/imageHelper"; import { SpeakerId, StyleId } from "@/type/preload"; import { getStyleDescription } from "@/sing/viewHelper"; const store = useStore(); +const uiLocked = computed(() => store.getters.UI_LOCKED); const userOrderedCharacterInfos = computed(() => { return store.getters.USER_ORDERED_CHARACTER_INFOS("singerLike"); @@ -160,6 +171,7 @@ const reassignSubMenuOpen = debounce((idx: number) => { arr[idx] = true; subMenuOpenFlags.value = arr; }, 100); +const showSkeleton = computed(() => selectedCharacterInfo.value == undefined); const changeStyleId = (speakerUuid: SpeakerId, styleId: StyleId) => { const engineId = store.state.engineIds.find((_engineId) => @@ -205,6 +217,10 @@ const selectedCharacterInfo = computed(() => { return store.getters.CHARACTER_INFO(singer.engineId, singer.styleId); }); +const selectedSinger = computed(() => { + return store.getters.SELECTED_TRACK.singer; +}); + const selectedSpeakerUuid = computed(() => { return selectedCharacterInfo.value?.metas.speakerUuid; }); @@ -235,20 +251,6 @@ const engineIcons = computed(() => @use '@/styles/variables' as vars; @use '@/styles/colors' as colors; -.character-name { - position: absolute; - top: 0px; - left: 0px; - padding: 1px 24px 1px 8px; - background-image: linear-gradient( - 90deg, - rgba(colors.$background-rgb, 0.5) 0%, - rgba(colors.$background-rgb, 0.5) 75%, - transparent 100% - ); - overflow-wrap: anywhere; -} - .character-menu { .q-item { color: colors.$display; @@ -261,10 +263,6 @@ const engineIcons = computed(() => background-color: rgba(colors.$primary-rgb, 0.1); } } - .selected-character-item, - .opened-character-item { - background-color: rgba(colors.$primary-rgb, 0.2); - } .engine-icon { position: absolute; width: 13px; diff --git a/src/components/Sing/CharacterMenuButton/SelectedCharacter.vue b/src/components/Sing/CharacterMenuButton/SelectedCharacter.vue new file mode 100644 index 0000000000..ec16378ab5 --- /dev/null +++ b/src/components/Sing/CharacterMenuButton/SelectedCharacter.vue @@ -0,0 +1,132 @@ +<template> + <div v-if="props.showSkeleton" class="selected-character"> + <q-skeleton class="character-avatar" type="QAvatar" size="52px" /> + <div class="character-info"> + <q-skeleton + class="character-name skeleton" + type="rect" + width="65px" + height="15px" + /> + <q-skeleton + class="character-style" + type="rect" + width="110px" + height="12px" + /> + </div> + </div> + <div v-else class="selected-character"> + <q-avatar + v-if="selectedStyleIconPath" + class="character-avatar" + size="3.5rem" + > + <img :src="selectedStyleIconPath" class="character-avatar-icon" /> + </q-avatar> + <div class="character-info"> + <div class="character-name"> + {{ selectedCharacterName }} + </div> + <div class="character-style"> + {{ selectedCharacterStyleDescription }} + </div> + </div> + <q-icon + name="arrow_drop_down" + size="sm" + class="character-menu-dropdown-icon" + /> + </div> +</template> + +<script setup lang="ts"> +import { computed } from "vue"; +import { Singer } from "@/store/type"; +import { CharacterInfo } from "@/type/preload"; +import { getStyleDescription } from "@/sing/viewHelper"; + +const props = + defineProps<{ + showSkeleton: boolean; + selectedCharacterInfo: CharacterInfo | undefined; + selectedSinger: Singer | undefined; + }>(); + +const selectedCharacterName = computed(() => { + return props.selectedCharacterInfo?.metas.speakerName; +}); +const selectedCharacterStyleDescription = computed(() => { + const style = props.selectedCharacterInfo?.metas.styles.find((style) => { + return ( + style.styleId === props.selectedSinger?.styleId && + style.engineId === props.selectedSinger?.engineId + ); + }); + return style != undefined ? getStyleDescription(style) : ""; +}); +const selectedStyleIconPath = computed(() => { + if (!props.selectedCharacterInfo || !props.selectedSinger) { + return; + } + const styles = props.selectedCharacterInfo.metas.styles; + return styles?.find((style) => { + return ( + style.styleId === props.selectedSinger?.styleId && + style.engineId === props.selectedSinger?.engineId + ); + })?.iconPath; +}); +</script> + +<style scoped lang="scss"> +@use '@/styles/variables' as vars; +@use '@/styles/colors' as colors; + +.selected-character { + align-items: center; + display: flex; + padding: 0.25rem 0.5rem 0.25rem 0.25rem; + position: relative; + + .character-avatar-icon { + display: block; + height: 100%; + object-fit: cover; + width: 100%; + } + + .character-info { + align-items: start; + display: flex; + flex-direction: column; + margin-left: 0.5rem; + text-align: left; + justify-content: center; + white-space: nowrap; + } + .character-name { + font-size: 0.875rem; + font-weight: bold; + line-height: 1rem; + padding-top: 0.5rem; + + &.skeleton { + margin-top: 0.4rem; + margin-bottom: 0.2rem; + } + } + + .character-style { + color: rgba(colors.$display-rgb, 0.6); + font-size: 0.75rem; + font-weight: bold; + line-height: 1rem; + } + + .character-menu-dropdown-icon { + color: rgba(colors.$display-rgb, 0.8); + margin-left: 0.25rem; + } +} +</style> diff --git a/src/components/Sing/SingEditor.vue b/src/components/Sing/SingEditor.vue index e6df509cc2..59f45a43ec 100644 --- a/src/components/Sing/SingEditor.vue +++ b/src/components/Sing/SingEditor.vue @@ -2,6 +2,7 @@ <menu-bar /> <tool-bar /> <div class="sing-main"> + <engine-startup-overlay :is-completed-initial-startup="isEnginesReady" /> <div v-if="nowAudioExporting" class="exporting-dialog"> <div> <q-spinner color="primary" size="2.5rem" /> @@ -34,6 +35,7 @@ import { DEFAULT_BPM, DEFAULT_TPQN, } from "@/sing/storeHelper"; +import EngineStartupOverlay from "@/components/EngineStartupOverlay.vue"; import { useStore } from "@/store"; const props = withDefaults( @@ -121,6 +123,7 @@ const unwatchIsEnginesReady = watch( .sing-main { display: flex; overflow: hidden; + position: relative; } .exporting-dialog { diff --git a/src/components/Sing/ToolBar.vue b/src/components/Sing/ToolBar.vue index b548367df9..f09484274f 100644 --- a/src/components/Sing/ToolBar.vue +++ b/src/components/Sing/ToolBar.vue @@ -2,30 +2,7 @@ <q-toolbar class="sing-toolbar"> <!-- configs for entire song --> <div class="sing-configs"> - <character-menu-button class="q-mr-sm"> - <div class="character-menu-toggle"> - <q-avatar - v-if="selectedStyleIconPath" - class="character-avatar" - size="48px" - > - <img :src="selectedStyleIconPath" class="character-avatar-icon" /> - </q-avatar> - <div class="character-info"> - <div class="character-name"> - {{ selectedCharacterName }} - </div> - <div class="character-style"> - {{ selectedCharacterStyleDescription }} - </div> - </div> - <q-icon - name="arrow_drop_down" - size="sm" - class="character-menu-dropdown-icon" - /> - </div> - </character-menu-button> + <character-menu-button /> <q-input type="number" :model-value="keyShiftInputBuffer" @@ -137,43 +114,10 @@ import { isValidBpm, isValidVoiceKeyShift, } from "@/sing/domain"; -import CharacterMenuButton from "@/components/Sing/CharacterMenuButton.vue"; -import { getStyleDescription } from "@/sing/viewHelper"; +import CharacterMenuButton from "@/components/Sing/CharacterMenuButton/MenuButton.vue"; const store = useStore(); -const userOrderedCharacterInfos = computed(() => - store.getters.USER_ORDERED_CHARACTER_INFOS("singerLike") -); -const selectedCharacterInfo = computed(() => { - const singer = store.getters.SELECTED_TRACK.singer; - if (!userOrderedCharacterInfos.value || !singer) { - return undefined; - } - return store.getters.CHARACTER_INFO(singer.engineId, singer.styleId); -}); -const selectedCharacterName = computed(() => { - return selectedCharacterInfo.value?.metas.speakerName; -}); -const selectedCharacterStyleDescription = computed(() => { - const style = selectedCharacterInfo.value?.metas.styles.find((style) => { - const singer = store.getters.SELECTED_TRACK.singer; - return ( - style.styleId === singer?.styleId && style.engineId === singer?.engineId - ); - }); - return style != undefined ? getStyleDescription(style) : ""; -}); -const selectedStyleIconPath = computed(() => { - const styles = selectedCharacterInfo.value?.metas.styles; - const singer = store.getters.SELECTED_TRACK.singer; - return styles?.find((style) => { - return ( - style.styleId === singer?.styleId && style.engineId === singer?.engineId - ); - })?.iconPath; -}); - const tempos = computed(() => store.state.tempos); const timeSignatures = computed(() => store.state.timeSignatures); const keyShift = computed(() => store.getters.SELECTED_TRACK.voiceKeyShift); @@ -377,46 +321,6 @@ onUnmounted(() => { } } -.character-menu-toggle { - align-items: center; - display: flex; - padding: 4px 8px 8px 8px; - position: relative; -} -.character-avatar-icon { - display: block; - height: 100%; - object-fit: cover; - width: 100%; -} - -.character-info { - align-items: start; - display: flex; - flex-direction: column; - margin-left: 0.5rem; - text-align: left; - justify-content: center; - white-space: nowrap; -} -.character-name { - font-size: 0.875rem; - font-weight: bold; - line-height: 1rem; - padding-top: 4px; -} - -.character-style { - color: rgba(colors.$display-rgb, 0.73); - font-size: 11px; - line-height: 1rem; - vertical-align: text-bottom; -} - -.character-menu-dropdown-icon { - color: rgba(colors.$display-rgb, 0.73); - margin-left: 0.25rem; -} .sing-toolbar { background: colors.$sing-toolbar; align-items: center; diff --git a/src/components/Talk/TalkEditor.vue b/src/components/Talk/TalkEditor.vue index 54e996276d..2a1289343f 100644 --- a/src/components/Talk/TalkEditor.vue +++ b/src/components/Talk/TalkEditor.vue @@ -7,45 +7,10 @@ <q-page-container> <q-page class="main-row-panes"> <progress-view /> + <engine-startup-overlay + :is-completed-initial-startup="isCompletedInitialStartup" + /> - <!-- TODO: 複数エンジン対応 --> - <!-- TODO: allEngineStateが "ERROR" のときエラーになったエンジンを探してトーストで案内 --> - <div v-if="allEngineState === 'FAILED_STARTING'" class="waiting-engine"> - <div> - エンジンの起動に失敗しました。エンジンの再起動をお試しください。 - </div> - </div> - <div - v-else-if=" - !isCompletedInitialStartup || allEngineState === 'STARTING' - " - class="waiting-engine" - > - <div> - <q-spinner color="primary" size="2.5rem" /> - <div class="q-mt-xs"> - {{ - allEngineState === "STARTING" - ? "エンジン起動中・・・" - : "データ準備中・・・" - }} - </div> - - <template v-if="isEngineWaitingLong"> - <q-separator spaced /> - エンジン起動に時間がかかっています。<br /> - <q-btn - v-if="isMultipleEngine" - outline - :disable="reloadingLocked" - @click="reloadAppWithMultiEngineOffMode" - > - マルチエンジンをオフにして再読み込みする</q-btn - > - <q-btn v-else outline @click="openQa">Q&Aを見る</q-btn> - </template> - </div> - </div> <q-splitter horizontal reverse @@ -207,7 +172,8 @@ import DictionaryManageDialog from "@/components/Dialog/DictionaryManageDialog.v import EngineManageDialog from "@/components/Dialog/EngineManageDialog.vue"; import ProgressView from "@/components/ProgressView.vue"; import UpdateNotificationDialogContainer from "@/components/Dialog/UpdateNotificationDialog/Container.vue"; -import { AudioItem, EngineState } from "@/store/type"; +import EngineStartupOverlay from "@/components/EngineStartupOverlay.vue"; +import { AudioItem } from "@/store/type"; import { AudioKey, HotkeyActionType, @@ -229,9 +195,6 @@ const store = useStore(); const audioKeys = computed(() => store.state.audioKeys); const uiLocked = computed(() => store.getters.UI_LOCKED); -const reloadingLocked = computed(() => store.state.reloadingLock); - -const isMultipleEngine = computed(() => store.state.engineIds.length > 1); // hotkeys handled by Mousetrap const hotkeyMap = new Map<HotkeyActionType, () => HotkeyReturnType>([ @@ -594,47 +557,6 @@ const unwatchIsEnginesReady = watch( } ); -// エンジン待機 -// TODO: 個別のエンジンの状態をUIで確認できるようにする -const allEngineState = computed(() => { - const engineStates = store.state.engineStates; - - let lastEngineState: EngineState | undefined = undefined; - - // 登録されているすべてのエンジンについて状態を確認する - for (const engineId of store.state.engineIds) { - const engineState: EngineState | undefined = engineStates[engineId]; - if (engineState == undefined) - throw new Error(`No such engineState set: engineId == ${engineId}`); - - // FIXME: 1つでも接続テストに成功していないエンジンがあれば、暫定的に起動中とする - if (engineState === "STARTING") { - return engineState; - } - - lastEngineState = engineState; - } - - return lastEngineState; // FIXME: 暫定的に1つのエンジンの状態を返す -}); - -const isEngineWaitingLong = ref<boolean>(false); -let engineTimer: number | undefined = undefined; -watch(allEngineState, (newEngineState) => { - if (engineTimer != undefined) { - clearTimeout(engineTimer); - engineTimer = undefined; - } - if (newEngineState === "STARTING") { - isEngineWaitingLong.value = false; - engineTimer = window.setTimeout(() => { - isEngineWaitingLong.value = true; - }, 30000); - } else { - isEngineWaitingLong.value = false; - } -}); - // 代替ポート情報の変更を監視 watch( () => [store.state.altPortInfos, store.state.isVuexReady], @@ -660,17 +582,6 @@ watch( } ); -const reloadAppWithMultiEngineOffMode = () => { - store.dispatch("CHECK_EDITED_AND_NOT_SAVE", { - closeOrReload: "reload", - isMultiEngineOffMode: true, - }); -}; - -const openQa = () => { - window.open("https://voicevox.hiroshiba.jp/qa/", "_blank"); -}; - // ライセンス表示 const isHelpDialogOpenComputed = computed({ get: () => store.state.isHelpDialogOpen, @@ -864,24 +775,6 @@ const onAudioCellPaneClick = () => { } } -.waiting-engine { - background-color: rgba(colors.$display-rgb, 0.15); - position: absolute; - inset: 0; - z-index: 10; - display: flex; - text-align: center; - align-items: center; - justify-content: center; - - > div { - color: colors.$display; - background: colors.$surface; - border-radius: 6px; - padding: 14px; - } -} - .main-row-panes { flex-grow: 1; flex-shrink: 1;