[Refactor] Move to src-ui/views and src-ui/logics structure.

This commit is contained in:
Sakamoto Shiina
2025-11-05 11:49:48 +09:00
parent 62f7c6d534
commit db820375f1
339 changed files with 19 additions and 19 deletions

View File

@@ -0,0 +1,88 @@
import { useState } from "react";
import { ErrorBoundary } from "react-error-boundary";
import XMarkSvg from "@images/cancel.svg?react";
import CopySvg from "@images/copy.svg?react";
import CheckMarkSvg from "@images/check_mark.svg?react";
import { ContactsContainer } from "./contacts_container/ContactsContainer";
import { useWindow } from "@logics_common";
import styles from "./AppErrorBoundary.module.scss";
export const AppErrorBoundary = ({children}) => {
return (
<ErrorBoundary
fallbackRender={({ error }) => (
<ErrorContainer error={error} />
)
}>
{children}
</ErrorBoundary>
);
};
const ErrorContainer = ({error}) => {
const [is_copied, setIsCopied] = useState(false);
const formatted_stack = error ? formatStackTrace(error.stack) : "Unknown error";
const copyToClipboard = async () => {
if (is_copied) return;
await navigator.clipboard.writeText(formatted_stack);
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
}, 1000);
};
return (
<div className={styles.container}>
<CloseButtonContainer />
<div className={styles.wrapper}>
<p className={styles.error_message}>An error occurred. Please restart VRCT or contact the developers.</p>
{error ?
<div className={styles.error_detail_container}>
<div className={styles.error_stack_container}>
<p className={styles.error_stack}>
{formatted_stack}
</p>
</div>
<button className={styles.copy_error_message_button} onClick={copyToClipboard}>
<p className={styles.copy_text}>Copy</p>
{is_copied
? <CheckMarkSvg className={styles.check_mark_svg}/>
: <CopySvg className={styles.copy_svg}/>
}
</button>
</div>
: null}
<ContactsContainer />
</div>
</div>
);
};
// Duplicated
const CloseButtonContainer = () => {
const { asyncCloseApp } = useWindow();
return (
<button className={styles.close_button_wrapper} onClick={asyncCloseApp}>
<div className={styles.close_button}>
<XMarkSvg className={styles.x_mark_svg}/>
</div>
</button>
);
};
const formatStackTrace = (stack) => {
if (!stack) return "";
// フルパスの除去(例として window.location.origin や絶対パス部分を削除)
// ※必要に応じて正規表現を調整してください
const formatted = stack.replace(new RegExp(window.location.origin, "g"), "");
return formatted;
};

View File

@@ -0,0 +1,111 @@
.container {
width: 100%;
height: 100%;
position: relative;
}
.wrapper {
width: 100%;
height: 100vh;
display: flex;
justify-content: safe center;
align-items: center;
flex-direction: column;
padding: 2rem;
overflow-y: auto;
}
.error_message {
font-size: 2rem;
text-align: center;
user-select: text;
margin-bottom: 3.2rem;
}
.error_detail_container {
display: flex;
flex-direction: column;
align-items: end;
gap: 1rem;
}
.error_stack_container {
max-height: 10rem;
width: 100%;
overflow-y: scroll;
padding: 1rem;
background-color: var(--dark_950_color);
border-radius: 0.4rem;
}
.error_stack {
font-size: 1rem;
user-select: text;
}
.copy_error_message_button {
// background-color: var(--dark_800_color);
padding: 0.8rem 1rem;
font-size: 1.4rem;
display: flex;
gap: 1rem;
justify-content: center;
align-items: center;
border-radius: 0.4rem;
background-color: var(--dark_825_color);
&:hover {
background-color: var(--dark_800_color);
}
&:active {
background-color: var(--dark_850_color);
}
}
.copy_svg {
width: 1.4rem;
color: var(--dark_500_color);
}
.check_mark_svg {
width: 1.4rem;
color: var(--primary_300_color);
}
// Duplicated
.close_button_wrapper {
position: absolute;
top: 0;
left: 100%;
transform: translate(-50%, -50%) rotate(45deg);
display: flex;
justify-content: center;
align-items: end;
width: 68px;
aspect-ratio: 1 / 1;
background-color: var(--error_bc_color);
& .x_mark_svg {
color: var(--dark_200_color);
}
&:hover {
& .x_mark_svg {
transform: rotate(45deg);
}
}
&:active {
background-color: var(--error_bc_active_color);
}
transition: all 0.1s ease;
}
.close_button {
// width: 100%;
// height: 100%;
}
.x_mark_svg {
width: 24px;
transform: rotate(-45deg);
color: var(--dark_700_color);
transition: transform 0.3s ease;
}

View File

@@ -0,0 +1,29 @@
import styles from "./ContactsContainer.module.scss";
export const ContactsContainer = () => {
return (
<div className={styles.container}>
<OpenLinkContainer className={styles.github_issues} href_id="github_issues" text="Github Issues"/>
<OpenLinkContainer className={styles.google_forms} href_id="google_forms" text="Google Forms"/>
</div>
);
};
import dev_github_icon from "@images/about_vrct/dev_github_icon.png";
import document from "@images/document.png";
const contacts_links = {
github_issues: { img: dev_github_icon, href: "https://github.com/misyaguziya/VRCT/issues" },
google_forms: { img: document, href: "https://docs.google.com/forms/d/e/1FAIpQLSei-xoydOY60ivXqhOjaTzNN8PiBQIDcNhzfy6cw2sjYkcg_g/viewform" },
};
const OpenLinkContainer = ({className, href_id, text}) => {
const href = contacts_links[href_id].href;
const img = contacts_links[href_id].img;
return (
<a className={className} href={href} target="_blank" rel="noreferrer" >
<img className={styles.contact_button_icon} src={img} />
<p className={styles.contact_button_label}>{text}</p>
</a>
);
};

View File

@@ -0,0 +1,28 @@
.container {
display: flex;
gap: 3.2rem;
}
.github_issues, .google_forms {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 100%;
padding: 1rem;
border-radius: 0.4rem;
gap: 1rem;
&:hover {
background-color: var(--dark_800_color);
}
&:active {
background-color: var(--dark_900_color)
}
}
.contact_button_icon {
width: 5.2rem;
}
.contact_button_label {
font-size: 1.4rem;
white-space: nowrap;
}

View File

@@ -0,0 +1,6 @@
export { WindowTitleBar } from "./window_title_bar/WindowTitleBar.jsx";
export { SplashComponent } from "./splash_component/SplashComponent.jsx";
export { UpdatingComponent } from "./updating_component/UpdatingComponent.jsx";
export { ModalController } from "./modal_controller/ModalController.jsx";
export { SnackbarController } from "./snackbar_controller/SnackbarController.jsx";
export { AppErrorBoundary } from "./error_boundary/AppErrorBoundary.jsx";

View File

@@ -0,0 +1,34 @@
import styles from "./ModalController.module.scss";
import { useStore_OpenedQuickSetting } from "@store";
import { Vr, VrcMicMuteSyncContainer, Plugins } from "@setting_box";
import { UpdateModal } from "./update_modal/UpdateModal";
export const ModalController = () => {
const { currentOpenedQuickSetting, updateOpenedQuickSetting } = useStore_OpenedQuickSetting();
if (currentOpenedQuickSetting.data === "") return null;
return (
<div className={styles.container}>
<div className={styles.bg_onclick_close_area} onClick={() => updateOpenedQuickSetting("")}></div>
<div className={styles.wrapper}>
<QuickSettingsController />
</div>
</div>
);
};
const QuickSettingsController = () => {
const { currentOpenedQuickSetting, updateOpenedQuickSetting } = useStore_OpenedQuickSetting();
switch (currentOpenedQuickSetting.data) {
case "plugins":
return <Plugins />;
case "vrc_mic_mute_sync":
return <VrcMicMuteSyncContainer />;
case "overlay":
return <Vr />;
case "update_software":
return <UpdateModal />;
default:
return null;
}
};

View File

@@ -0,0 +1,29 @@
.container {
position: relative;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.bg_onclick_close_area {
position: absolute;
width: 100%;
height: 100%;
background-color: var(--dark_550_color_22);
backdrop-filter: blur(0.2rem);
}
.wrapper {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
overflow-y: auto;
background-color: var(--dark_900_color);
width: 80%;
height: 96%;
padding: 2rem;
border-radius: 0.6rem;
}

View File

@@ -0,0 +1,100 @@
import clsx from "clsx";
import styles from "./UpdateModal.module.scss";
import { useI18n } from "@useI18n";
import { useStore_OpenedQuickSetting } from "@store";
import { usePlugins } from "@logics_configs";
import {
useComputeMode,
useUpdateSoftware,
useIsSoftwareUpdating,
useSoftwareVersion,
} from "@logics_common";
import { PluginCompatibilityList } from "./plugins_compatibility_list/PluginCompatibilityList";
export const UpdateModal = () => {
const { t } = useI18n();
const { updateOpenedQuickSetting } = useStore_OpenedQuickSetting();
const { updateSoftware, updateSoftware_CUDA } = useUpdateSoftware();
const { updateIsSoftwareUpdating } = useIsSoftwareUpdating();
const { currentComputeMode } = useComputeMode();
const { currentLatestSoftwareVersionInfo } = useSoftwareVersion();
const { isAnyPluginEnabled } = usePlugins();
const is_latest_version_already = currentLatestSoftwareVersionInfo.data.is_update_available === false;
const is_cpu_version = currentComputeMode.data === "cpu";
const onClickUpdateSoftware = () => {
updateIsSoftwareUpdating(true);
updateSoftware();
}
const onClickUpdateSoftware_CUDA = () => {
updateIsSoftwareUpdating(true);
updateSoftware_CUDA();
}
const cpu_accept_button_class_name = clsx(styles.accept_button, {
[styles.current_compute_version]: is_cpu_version,
[styles.is_latest_version_already]: is_latest_version_already,
})
const cuda_accept_button_class_name = clsx(styles.accept_button, {
[styles.current_compute_version]: !is_cpu_version,
[styles.is_latest_version_already]: is_latest_version_already,
})
return (
<div className={styles.container}>
<div className={styles.wrapper}>
<div className={styles.update_section_wrapper}>
{isAnyPluginEnabled() && <PluginCompatibilityList />}
<div className={styles.update_section}>
<div className={styles.cpu_section}>
<div className={styles.button_wrapper}>
<button className={cpu_accept_button_class_name} onClick={onClickUpdateSoftware}>CPU</button>
{is_cpu_version ? <CurrentVersionLabel is_latest_version_already={is_latest_version_already} /> : null}
</div>
<div className={styles.version_desc_container}>
<VersionDescComponent desc={t("update_modal.cpu_desc")} />
</div>
</div>
<div className={styles.cuda_section}>
<div className={styles.button_wrapper}>
<button className={cuda_accept_button_class_name} onClick={onClickUpdateSoftware_CUDA}>CUDA (CPU/GPU)</button>
{!is_cpu_version ? <CurrentVersionLabel is_latest_version_already={is_latest_version_already} is_cuda={true}/> : null}
</div>
<div className={styles.version_desc_container}>
<VersionDescComponent desc={t("update_modal.cuda_desc")} />
<VersionDescComponent desc={t("update_modal.cuda_compare_cpu_desc")} />
<VersionDescComponent desc={t("update_modal.cuda_disk_space_desc", {size: "5GB"})} />
</div>
</div>
<p className={styles.update_desc}>{t("update_modal.download_latest_and_restart")}</p>
</div>
</div>
<div className={styles.button_wrapper}>
<button className={styles.deny_button} onClick={() => updateOpenedQuickSetting("")} >{t("update_modal.close_modal")}</button>
</div>
</div>
</div>
);
};
const VersionDescComponent = (props) => {
return (
<div className={styles.version_desc_wrapper}>
<div className={styles.version_desc_point}></div>
<p className={styles.version_desc}>{`- ${props.desc}`}</p>
</div>
);
};
const CurrentVersionLabel = (props) => {
const { t } = useI18n();
if (props.is_latest_version_already) {
return <p className={clsx(styles.current_version_label, {[styles.is_cuda]: props.is_cuda})}>{t("update_modal.is_latest_version_already")}</p>;
}
return <p className={clsx(styles.current_version_label, {[styles.is_cuda]: props.is_cuda})}>{t("update_modal.is_current_compute_device")}</p>;
};

View File

@@ -0,0 +1,156 @@
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: safe center;
align-items: center;
gap: 2.4rem;
}
.wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 8rem;
}
.update_section_wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 4rem;
}
.update_section {
border: 0.1rem solid var(--dark_600_color);
border-radius: 0.4rem;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
gap: 4rem;
padding: 6rem 2rem;
position: relative;
}
.update_desc {
position: absolute;
top: 100%;
transform: translateY(-50%);
font-size: 1.4rem;
background-color: var(--dark_900_color);
padding: 0 2.8rem;
text-align: center;
font-weight: 400;
}
.cpu_section, .cuda_section {
width: 100%;
display: flex;
justify-content: start;
align-items: center;
gap: 3rem;
}
.button_wrapper {
display: flex;
width: 16rem;
justify-content: space-between;
position: relative;
flex-shrink: 0;
}
.deny_button, .accept_button {
font-size: 1.6rem;
padding: 1rem;
min-width: 10rem;
flex: 1;
max-width: 20rem;
text-align: center;
border-radius: 0.4rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.accept_button {
background-color: var(--primary_700_color);
position: relative;
overflow: visible;
&:hover {
background-color: var(--primary_450_color);
}
&:active {
background-color: var(--primary_600_color);
}
&.current_compute_version {
&::after {
content: "";
position: absolute;
top: -1rem;
right: -1rem;
bottom: -1rem;
left: -1rem;
border: 0.1rem solid var(--primary_400_color);
border-radius: 0.4rem;
}
&.is_latest_version_already {
background-color: var(--dark_825_color);
pointer-events: none;
}
}
}
.deny_button {
background-color: var(--dark_825_color);
&:hover {
background-color: var(--dark_800_color);
}
&:active {
background-color: var(--dark_850_color);
}
}
.current_version_label {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
margin: 0 0 0.4rem 0;
font-size: 1.2rem;
width: max-content;
height: max-content;
background-color: var(--dark_900_color);
padding: 0 1rem;
font-weight: 300;
&.is_cuda {
top: 100%;
margin: 0.4rem 0 0 0;
}
}
.version_desc_container {
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.version_desc_wrapper {
display: flex;
align-items: center;
gap: 0.6rem;
}
.version_desc_point {
width: 0.3rem;
border-radius: 50%;
aspect-ratio: 1 / 1;
}
.version_desc {
font-size: 1.4rem;
max-width: 48rem;
// text-align: center;
font-weight: 300;
}

View File

@@ -0,0 +1,78 @@
import { useEffect } from "react";
import styles from "./PluginCompatibilityList.module.scss";
import { usePlugins } from "@logics_configs";
import CheckMarkSvg from "@images/check_mark.svg?react";
import XSvg from "@images/x_mark.svg?react";
import WarningSvg from "@images/warning.svg?react";
export const PluginCompatibilityList = () => {
const {
enabledPluginsList,
asyncFetchPluginsInfo,
currentFetchedPluginsInfo,
} = usePlugins();
useEffect(() => {
asyncFetchPluginsInfo();
}, []);
// ダウンロード済みのもの
const downloaded_plugin = enabledPluginsList().filter(p => p.is_downloaded);
const compatible_plugins_list = [];
const incompatible_plugins_list = [];
for (const p of downloaded_plugin) {
if (!p.downloaded_plugin_info?.is_plugin_supported_latest_vrct && !p.latest_plugin_info?.is_plugin_supported_latest_vrct) {
// プラグイン最新版でも、VRCT最新版VRCTアプデ後に非対応のもの
incompatible_plugins_list.push(p);
} else {
// 現プラグイン or 最新版が、VRCT最新版VRCTアプデ後に対応しているもの
compatible_plugins_list.push(p);
}
}
const is_any_incompatible_plugin = incompatible_plugins_list.length > 0;
const is_any_compatible_plugin = compatible_plugins_list.length > 0;
if (!is_any_incompatible_plugin && !is_any_compatible_plugin) return null; // This is just for safety.
// Duplicate
const is_failed_to_fetch = currentFetchedPluginsInfo.state === "error";
const is_fetching = currentFetchedPluginsInfo.state === "pending";
return (
<div className={styles.container}>
<p className={styles.title}>使用中プラグインの互換性チェック</p>
{is_failed_to_fetch && <p>Failed to fetch plugins data</p>}
{is_fetching && <p>Fetching plugins data...</p>}
<div className={styles.plugins_compatibility_container}>
{incompatible_plugins_list.map(plugin => {
const target_data = plugin.downloaded_plugin_info;
return <PluginContainer key={target_data.plugin_id} target_data={target_data} is_compatible={false}/>;
})}
{compatible_plugins_list.map(plugin => {
const target_data = plugin.downloaded_plugin_info;
return <PluginContainer key={target_data.plugin_id} target_data={target_data} is_compatible={true} />;
})}
</div>
{is_any_incompatible_plugin &&
<div className={styles.warning_container}>
<WarningSvg className={styles.warning_svg}/>
<p className={styles.warning_text}>VRCT最新バージョンで互換性のないプラグインはアップデート後に無効化されます引き続き使用したい場合は各プラグインの更新を待ってください</p>
</div>
}
</div>
);
};
const PluginContainer = ({ target_data, is_compatible }) => {
return (
<div className={styles.plugin_box}>
<p className={clsx(styles.plugin_label, {[styles.is_compatible]: is_compatible})} >{target_data.title}</p>
{is_compatible
? <CheckMarkSvg className={styles.check_mark_svg}/>
: <XSvg className={styles.x_svg}/>
}
</div>
);
};

View File

@@ -0,0 +1,63 @@
.container {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 1rem;
}
.title {
font-size: 1.6rem;
}
.plugins_compatibility_container {
display: flex;
justify-content: center;
align-items: center;
gap: 0.2rem 1rem;
flex-wrap: wrap;
}
.plugin_box {
display: flex;
justify-content: center;
align-items: center;
padding: 0.4rem 0.6rem;
gap: 0.6rem;
}
.plugin_label {
font-size: 1.4rem;
color: var(--error_bc_color);
&.is_compatible {
color: var(--primary_300_color);
}
}
.check_mark_svg {
width: 1.8rem;
color: var(--primary_300_color);
}
.x_svg {
width: 1.8rem;
color: var(--error_bc_color);
}
.warning_container {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
}
.warning_svg {
padding-bottom: 0.4rem;
width: 2.4rem;
color: var(--warning_color);
flex-shrink: 0;
}
.warning_text {
font-size: 1.2rem;
}

View File

@@ -0,0 +1,144 @@
:root {
--toastify-color-light: #fff;
--toastify-color-dark: var(--dark_950_color);
--toastify-color-info: var(--sent_400_color);
--toastify-color-success: var(--primary_400_color);
--toastify-color-warning: var(--warning_bc_color);
--toastify-color-error: var(--error_bc_color);
--toastify-color-transparent: rgba(255, 255, 255, 0.7);
--toastify-icon-color-info: var(--toastify-color-info);
--toastify-icon-color-success: var(--toastify-color-success);
--toastify-icon-color-warning: var(--toastify-color-warning);
--toastify-icon-color-error: var(--toastify-color-error);
--toastify-container-width: fit-content;
--toastify-toast-width: 32rem;
--toastify-toast-offset: 1.6rem;
--toastify-toast-top: max(var(--toastify-toast-offset), env(safe-area-inset-top));
--toastify-toast-right: max(var(--toastify-toast-offset), env(safe-area-inset-right));
--toastify-toast-left: max(var(--toastify-toast-offset), env(safe-area-inset-left));
--toastify-toast-bottom: max(var(--toastify-toast-offset), env(safe-area-inset-bottom));
--toastify-toast-background: #fff;
--toastify-toast-padding: 1.4rem;
--toastify-toast-min-height: 6.4rem;
--toastify-toast-max-height: 80rem;
--toastify-toast-bd-radius: 0.6rem;
--toastify-toast-shadow: .0 0.4rem 1.2rem rgba(0, 0, 0, 0.1);
--toastify-font-family: var(--font_family);
--toastify-z-index: 9999;
--toastify-text-color-light: #757575;
--toastify-text-color-dark: var(--dark_basic_text_color);
/* Used only for colored theme */
--toastify-text-color-info: var(--dark_basic_text_color);
--toastify-text-color-success: var(--dark_basic_text_color);
--toastify-text-color-warning: var(--dark_basic_text_color);
--toastify-text-color-error: var(--dark_basic_text_color);
--toastify-spinner-color: #616161;
--toastify-spinner-color-empty-area: #e0e0e0;
--toastify-color-progress-light: linear-gradient(to right, #4cd964, #5ac8fa, #007aff, #34aadc, #5856d6, #ff2d55);
--toastify-color-progress-dark: #bb86fc;
--toastify-color-progress-info: var(--toastify-color-info);
--toastify-color-progress-success: var(--toastify-color-success);
--toastify-color-progress-warning: var(--toastify-color-warning);
--toastify-color-progress-error: var(--toastify-color-error);
/* used to control the opacity of the progress trail */
--toastify-color-progress-bgo: 0.2;
}
.Toastify__toast {
// --------------------------------------------------------
// Default Settings
// --------------------------------------------------------
position: relative;
touch-action: none;
// width: var(--toastify-toast-width);
min-height: var(--toastify-toast-min-height);
box-sizing: border-box;
margin-bottom: 1rem;
// padding: var(--toastify-toast-padding);
border-radius: 0.6rem;
box-shadow: none;
max-height: var(--toastify-toast-max-height);
// font-family: "Yu Gothic UI";
// font-family: var(--toastify-font-family);
// z-index: 0;
// display: flex;
// flex: 1 auto;
// align-items: center;
word-break: break-word;
// --------------------------------------------------------
// --------------------------------------------------------
// Comment out above and override. Commented out is just for memorization.
overflow: hidden;
display: flex;
justify-content: start;
align-items: center;
font-size: 1.4rem;
width: fit-content;
max-width: 70vw;
padding-right: 4rem;
background-color: var(--dark_950_color);
gap: 0.6rem;
white-space: pre-wrap;
}
.Toastify__progress-bar--wrp {
height: 0.4rem;
}
.Toastify__progress-bar--success {
background: var(--success_bc_color);
}
.Toastify__progress-bar--warning {
background: var(--warning_bc_color);
}
.Toastify__progress-bar--error {
background: var(--error_bc_color);
}
.Toastify__toast-icon {
width: fit-content;
max-width: 2.8rem;
min-width: 2.8rem;
justify-content: center;
align-items: center;
}
@keyframes fade_in {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes fade_out {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.fade_in {
opacity: 0;
animation-name: fade_in;
animation-duration: 0.1s;
animation-timing-function: ease-out;
animation-fill-mode: forwards;
}
.fade_out {
opacity: 1;
animation-name: fade_out;
animation-duration: 0.1s;
animation-timing-function: ease-in;
animation-fill-mode: forwards;
}

View File

@@ -0,0 +1,115 @@
import React, { useEffect, useState, isValidElement } from "react";
import { ToastContainer, toast, cssTransition } from "react-toastify";
import clsx from "clsx";
import "./ReactToastifyOverrideClass.scss";
import styles from "./SnackbarController.module.scss";
import XMarkSvg from "@images/cancel.svg?react";
import WarningSvg from "@images/warning.svg?react";
import MegaphoneSvg from "@images/megaphone.svg?react";
import CheckMarkSvg from "@images/check_mark.svg?react";
import ErrorSvg from "@images/error.svg?react";
import { useNotificationStatus } from "@logics_common";
const CustomTransition = cssTransition({
enter: "fade_in",
exit: "fade_out",
collapse: false,
});
export const SnackbarController = () => {
const { currentNotificationStatus, closeNotification } = useNotificationStatus();
const [containerKey, setContainerKey] = useState(0);
const settings = currentNotificationStatus.data;
const snackbar_classname = clsx(
styles.snackbar_content,
{
[styles.is_success]: settings.status === "success",
[styles.is_warning]: settings.status === "warning",
[styles.is_error]: settings.status === "error",
}
);
let hide_duration = 5000;
if (settings.options?.hide_duration === null) {
hide_duration = false;
} else if (Number(settings.options?.hide_duration)) {
hide_duration = Number(settings.options?.hide_duration);
}
useEffect(() => {
if (!settings.is_open) return;
const message_text = settings.message;
const category_id = settings.category_id ? settings.category_id : message_text;
const to_hide_progress_bar = (settings.options?.to_hide_progress_bar === true) ? true : false;
const asyncShowNotification = async () => {
setTimeout(() => {
toast(message_text, {
toastId: category_id,
type: settings.status,
autoClose: hide_duration,
transition: CustomTransition,
toastClassName: snackbar_classname,
hideProgressBar: to_hide_progress_bar,
progressClassName: styles.toast_progress,
closeButton: <CloseButtonContainer />,
onClose: () => {
closeNotification();
},
});
}, 100);
};
// setContainerKey(prevKey => prevKey + 1);
asyncShowNotification();
}, [settings]);
return (
<ToastContainer
// key={containerKey}
position="bottom-left"
transition={CustomTransition}
hideProgressBar={false}
newestOnTop={false}
closeOnClick={false}
pauseOnFocusLoss={false}
draggable={false}
pauseOnHover={true}
theme="dark"
icon={({ type }) => {
switch (type) {
case "info":
return <MegaphoneSvg className={styles.megaphone_svg} />;
case "error":
return <ErrorSvg className={styles.error_svg} />;
case "success":
return <CheckMarkSvg className={styles.check_mark_svg} />;
case "warning":
return <WarningSvg className={styles.warning_svg} />;
default:
return null;
}
}}
/>
);
};
const CloseButtonContainer = ({ closeToast }) => {
return (
<button className={styles.close_button_wrapper} onClick={closeToast}>
<div className={styles.close_button}>
<XMarkSvg className={styles.x_mark_svg} />
</div>
</button>
);
};

View File

@@ -0,0 +1,75 @@
.snackbar_content {
position: relative;
padding: 1.2rem 1.6rem;
border-radius: 0.8rem;
font-size: 1.4rem;
box-shadow: 0 0.2rem 0.8rem rgba(0, 0, 0, 0.5);
}
// .is_success {
// background-color: var(--success_bc_color);
// }
// .is_warning {
// background-color: var(--warning_bc_color);
// }
// .is_error {
// background-color: var(--error_bc_color);
// }
.megaphone_svg {
color: var(--dark_200_color);
width: 2.4rem;
}
.error_svg {
color: var(--error_bc_color);
width: 2.4rem;
}
.check_mark_svg {
color: var(--primary_400_color);
width: 2rem;
}
.warning_svg {
color: var(--warning_color);
width: 2.4rem;
}
// Duplicated (Customized)
.close_button_wrapper {
position: absolute;
top: 0;
left: 100%;
transform: translate(-50%, -50%) rotate(45deg);
display: flex;
justify-content: center;
align-items: end;
width: 5.6rem;
aspect-ratio: 1 / 1;
&:hover {
background-color: var(--error_bc_color);
& .x_mark_svg {
color: var(--dark_200_color);
transform: rotate(45deg);
}
}
&:active {
background-color: var(--error_bc_active_color);
}
transition: all 0.1s ease;
}
.close_button {
// width: 100%;
// height: 100%;
}
.x_mark_svg {
width: 2rem;
transform: rotate(-45deg);
color: var(--dark_700_color);
transition: transform 0.3s ease;
}

View File

@@ -0,0 +1,85 @@
import { useState, useEffect } from "react";
import styles from "./SplashComponent.module.scss";
import { StartUpProgressContainer } from "./start_up_progress_container/StartUpProgressContainer";
import { DownloadModelsContainer } from "./download_models_container/DownloadModelsContainer";
import MegaphoneSvg from "@images/megaphone.svg?react";
import XMarkSvg from "@images/cancel.svg?react";
import { useWindow } from "@logics_common";
import clsx from "clsx";
export const SplashComponent = () => {
return (
<div className={styles.container}>
<StartUpProgressContainer />
<DownloadModelsContainer />
<AnnouncementsContainer />
<CloseButtonContainer />
</div>
);
};
const SHOW_MEGAPHONE_TIME = 500;
const AnnouncementsContainer = () => {
const labels = ["Check the Latest Status", "最新の状況を確認"];
const [is_shown, setIsShown] = useState(0);
const [currentLabelIndex, setCurrentLabelIndex] = useState(0);
const [is_labels_active, setIsLabelsActive] = useState(false);
useEffect(() => {
const showTimeout = setTimeout(() => {
setIsShown(true);
}, SHOW_MEGAPHONE_TIME);
const labelsTimeout = setTimeout(() => {
setIsLabelsActive(true);
}, SHOW_MEGAPHONE_TIME + 15000);
let labelInterval;
if (is_labels_active) {
labelInterval = setInterval(() => {
setCurrentLabelIndex((prevIndex) => (prevIndex + 1) % labels.length);
}, 4000);
}
return () => {
clearTimeout(showTimeout);
clearTimeout(labelsTimeout);
if (labelInterval) clearInterval(labelInterval);
};
}, [is_labels_active, labels.length]);
return (
<a
className={clsx(styles.announcements_button_wrapper, {
[styles.is_shown]: is_shown,
[styles.is_labels_active]: is_labels_active,
})}
href="https://docs.google.com/spreadsheets/d/1_L5i-1U6PB1dnaPPTE_5uKMfqOpkLziPyRkiMLi4mqU/edit?usp=sharing"
target="_blank"
rel="noreferrer"
>
<button className={styles.announcements_button}>
<MegaphoneSvg className={styles.announcements_link_svg} />
<p className={styles.announcements_label}>
{labels[currentLabelIndex]}
</p>
</button>
</a>
);
};
// Duplicated
const CloseButtonContainer = () => {
const { asyncCloseApp } = useWindow();
return (
<button className={styles.close_button_wrapper} onClick={asyncCloseApp}>
<div className={styles.close_button}>
<XMarkSvg className={styles.x_mark_svg}/>
</div>
</button>
);
};

View File

@@ -0,0 +1,104 @@
.container {
width: 100%;
height: 100%;
overflow: hidden;
position: relative;
}
.announcements_button_wrapper {
position: absolute;
top: 10px;
left: 10px;
opacity: 0;
transition: opacity 0.3s ease, border 0.3s ease;
padding: 4px 8px;
border-radius: 4px;
&.is_shown {
opacity: 1;
}
&.is_labels_active {
& .announcements_label {
display: block;
animation: appear .3s ease;
}
& .announcements_link_svg {
color: var(--dark_200_color);
}
}
&:hover {
background-color: var(--dark_825_color);
& .announcements_label {
color: var(--dark_200_color);
}
& .announcements_link_svg {
color: var(--primary_300_color);
}
}
&:active {
background-color: var(--dark_850_color);
}
}
@keyframes appear {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.announcements_button {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
transition: all 0.1s ease;
}
.announcements_label {
font-size: 12px;
color: var(--dark_400_color);
display: none;
transition: all 0.3s ease;
}
.announcements_link_svg {
width: 20px;
color: var(--dark_600_color);
}
// Duplicated
.close_button_wrapper {
position: absolute;
top: 0;
left: 100%;
transform: translate(-50%, -50%) rotate(45deg);
display: flex;
justify-content: center;
align-items: end;
width: 68px;
aspect-ratio: 1 / 1;
&:hover {
background-color: var(--error_bc_color);
& .x_mark_svg {
color: var(--dark_200_color);
transform: rotate(45deg);
}
}
&:active {
background-color: var(--error_bc_active_color);
}
transition: all 0.1s ease;
}
.close_button {
// width: 100%;
// height: 100%;
}
.x_mark_svg {
width: 24px;
transform: rotate(-45deg);
color: var(--dark_700_color);
transition: transform 0.3s ease;
}

View File

@@ -0,0 +1,69 @@
import styles from "./DownloadModelsContainer.module.scss";
import vrct_logo_for_dark_mode from "@images/vrct_logo_for_dark_mode.png";
import vrct_now_downloading from "@images/VRCT_now_downloading.png";
import {
useTranslation,
useTranscription,
} from "@logics_configs";
export const DownloadModelsContainer = () => {
const { currentCTranslate2WeightTypeStatus } = useTranslation();
const { currentWhisperWeightTypeStatus } = useTranscription();
const c_translate_2 = currentCTranslate2WeightTypeStatus.data.find(d => d.id === "m2m100_418M-ct2-int8");
const whisper = currentWhisperWeightTypeStatus.data.find(d => d.id === "base");
if (c_translate_2.progress === null && whisper.progress === null) return null;
return (
<div className={styles.container}>
<div className={styles.progress_container}>
<DownloadModelsProgress progress={c_translate_2.progress} type_label="Translation Model"/>
<DownloadModelsProgress progress={whisper.progress} type_label="Transcription Model"/>
</div>
<div className={styles.labels_wrapper}>
<img src={vrct_logo_for_dark_mode} className={styles.logo_img}/>
<img src={vrct_now_downloading} className={styles.vrct_now_downloading_img}/>
</div>
</div>
);
};
const DownloadModelsProgress = (props) => {
if (props.progress === null) return null;
const circular_progress = Math.floor(props.progress / 5) * 5;
const progress_color = generateGradientColor({
value: circular_progress,
colorStart: [242, 242, 242], // #f2f2f2
colorEnd: [72, 164, 149], // #48a495
});
return(
<div className={styles.progress_bar_container}>
<div className={styles.progress_bar_wrapper}>
<div
className={styles.progress_bar}
style={{
width: `${props.progress}%`,
backgroundColor: progress_color,
}}
/>
</div>
<p className={styles.progress_label}>{`${props.type_label}: ${Math.round(props.progress)}%`}</p>
</div>
);
};
const generateGradientColor = ({ value, colorStart, colorEnd }) => {
const normalizedValue = Math.max(0, Math.min(100, value)) / 100;
const interpolatedColor = colorStart.map((start, i) => {
const end = colorEnd[i];
return Math.round(start + (end - start) * normalizedValue);
});
const hexColor = `#${interpolatedColor.map(val => val.toString(16).padStart(2, '0')).join('')}`;
return hexColor;
};

View File

@@ -0,0 +1,64 @@
.container {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: hidden;
background-color: var(--dark_888_color);
}
.progress_container {
position: absolute;
top: 77%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
gap: 6px;
display: flex;
flex-direction: column;
}
.labels_wrapper {
position: relative;
width: 100%;
height: 100%;
}
.logo_img {
position: absolute;
top: 42%;
left: 50%;
width: 280px;
transform: translate(-50%, -50%);
}
.vrct_now_downloading_img {
position: absolute;
bottom: 2px;
right: 10px;
width: 300px;
}
.progress_bar_container {
display: flex;
flex-direction: column;
gap: 2px;
justify-content: center;
}
.progress_bar_wrapper {
background-color: var(--dark_800_color);
}
$progress_ease: cubic-bezier(0, 1, 0.75, 1);
// Duplicated
.progress_bar {
height: 8px;
transition: width 0.3s $progress_ease;
}
.progress_label {
text-align: end;
font-size: 12px;
color: var(--dark_400_color);
}

View File

@@ -0,0 +1,39 @@
import clsx from "clsx";
import styles from "./StartUpProgressContainer.module.scss";
import { useInitProgress } from "@logics_common";
import chat_white_square from "@images/chato_white_square.png";
import vrct_explanation from "@images/vrchat_chatbox_trasnlator_transcription.png";
import vrct_starting_up from "@images/vrct_starting_up.png";
export const StartUpProgressContainer = () => {
const { currentInitProgress } = useInitProgress();
const progress = currentInitProgress.data;
return (
<div className={styles.container}>
<div className={styles.progress_bar_wrapper}>
{[...Array(4)].map((_, index) => (
<div
key={index}
className={clsx(styles.progress_bar, {
[styles.progressed]: index < progress && progress !== 0,
})}
>
{index === 3
?
<div className={styles.chato_box}>
<img src={chat_white_square} className={styles.chato_img}/>
</div>
: null
}
</div>
))}
</div>
<div className={styles.labels_wrapper}>
<img src={vrct_starting_up} className={styles.vrct_starting_up_img}/>
<img src={vrct_explanation} className={styles.vrct_explanation_img}/>
</div>
</div>
);
};

View File

@@ -0,0 +1,101 @@
$progress_ease: cubic-bezier(0, 1, 0.75, 1);
// Duplicated
.container {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: hidden;
}
.progress_bar_wrapper {
position: absolute;
top: 48%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
gap: 36px;
}
.progress_bar {
width: 60px;
height: 2px;
position: relative;
}
.progress_bar::before {
content: "";
position: absolute;
left: 0;
top: 0;
width: 0;
height: 100%;
background-color: var(--dark_200_color);
transition: width 0.3s $progress_ease;
}
.progress_bar.progressed::before {
width: 100%;
}
.progress_bar:last-child::before {
background-color: var(--primary_400_color);
}
.chato_box {
position: relative;
top: 0;
transform: translateY(-100%);
width: 100%;
height: 8rem;
overflow: hidden;
}
.chato_img {
position: absolute;
top: 150%;
left: 51%;
transform: translate(-50%, -50%) rotate(-90deg);
width: 2.8rem;
transition: all 0.3s $progress_ease 0.2s;
}
.progress_bar.progressed .chato_img {
top: 50%;
transform: translate(-50%, -50%) rotate(30deg);
animation: infinite-rotation 20s linear infinite 0.5s;
}
@keyframes infinite-rotation {
from {
transform: translate(-50%, -50%) rotate(30deg);
}
to {
transform: translate(-50%, -50%) rotate(390deg);
}
}
.labels_wrapper {
position: relative;
width: 100%;
height: 100%;
}
.vrct_starting_up_img {
position: absolute;
top: 68%;
left: 50%;
transform: translate(-50%, -50%);
width: 100px;
// transform: translate(-50%, 50%);
}
.vrct_explanation_img {
position: absolute;
bottom: 4px;
right: 10px;
width: 280px;
// transform: translate(-50%, 50%);
}

View File

@@ -0,0 +1,22 @@
import styles from "./UpdatingComponent.module.scss";
import { useI18n } from "@useI18n";
import CircularProgress from "@mui/material/CircularProgress";
import chat_white_square from "@images/chato_white_square.png";
export const UpdatingComponent = () => {
const { t } = useI18n();
return (
<div className={styles.container}>
<div className={styles.chato_box}>
<img src={chat_white_square} className={styles.chato_img}/>
</div>
<div className={styles.circular_box}>
<CircularProgress size="20rem" sx={{
color: "var(--primary_300_color)",
}}/>
</div>
<p className={styles.label}>{t("main_page.updating")}</p>
</div>
);
};

View File

@@ -0,0 +1,52 @@
.container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 4rem;
position: relative;
}
.label {
position: absolute;
top: 60%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 2rem;
}
.circular_box {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.chato_box {
position: relative;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
overflow: hidden;
}
.chato_img {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 4.8rem;
animation: infinite-rotation 20s linear infinite 0.5s;
}
@keyframes infinite-rotation {
from {
transform: translate(-50%, -50%) rotate(0deg);
}
to {
transform: translate(-50%, -50%) rotate(360deg);
}
}

View File

@@ -0,0 +1,33 @@
import { useWindow } from "@logics_common";
// import clsx from "clsx";
import styles from "./WindowTitleBar.module.scss";
import XMarkSvg from "@images/cancel.svg?react";
import SquareSvg from "@images/square.svg?react";
import LineSvg from "@images/line.svg?react";
import VrctSvg from "@images/vrct.svg?react";
export const WindowTitleBar = () => {
const { asyncCloseApp, asyncToggleMaximizeApp, asyncMinimizeApp} = useWindow();
return (
<div className={styles.container}>
<div className={styles.wrapper} data-tauri-drag-region>
<div className={styles.title_wrapper}>
<VrctSvg className={styles.title_svg}/>
</div>
<div className={styles.window_control_wrapper}>
<div className={styles.minimize_button} onClick={asyncMinimizeApp}>
<LineSvg className={styles.line_svg}/>
</div>
<div className={styles.maximize_button} onClick={asyncToggleMaximizeApp}>
<SquareSvg className={styles.square_svg}/>
</div>
<div className={styles.close_button} onClick={asyncCloseApp}>
<XMarkSvg className={styles.x_mark_svg}/>
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,84 @@
.container {
width: 100%;
background-color: var(--dark_900_color);
flex-shrink: 0;
display: flex;
}
.wrapper {
flex-shrink: 0;
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
height: var(--title_bar_height);
margin-top: 0.4rem;
}
.title_wrapper {
padding-left: 1rem;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.title_svg {
color: var(--dark_800_color);
width: 4rem;
height: 100%;
}
.window_control_wrapper {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.minimize_button, .maximize_button, .close_button {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 2.8rem;
margin-bottom: 0.4rem;
cursor: pointer;
&:hover {
.x_mark_svg, .square_svg, .line_svg {
color: var(--dark_100_color);
}
}
}
.minimize_button, .maximize_button {
&:hover {
background-color: var(--dark_800_color);
}
&:active {
background-color: var(--dark_950_color);
}
}
.close_button {
&:hover {
background-color: var(--error_bc_color);
}
&:active {
background-color: var(--error_bc_active_color);
}
}
.x_mark_svg, .square_svg, .line_svg {
color: var(--dark_450_color);
height: 100%;
}
.x_mark_svg {
width: 1.8rem;
}
.square_svg {
width: 1.2rem;
}
.line_svg {
padding-top: 0.1rem;
width: 1.8rem;
}