[Update] Migrate to tauri-app(Web UI)

This commit is contained in:
Sakamoto Shiina
2024-07-25 01:01:22 +09:00
parent 25899b63da
commit ebd1a8d70d
342 changed files with 14616 additions and 13428 deletions

View File

@@ -0,0 +1,52 @@
import { useEffect, useRef } from "react";
import styles from "./MainWindow.module.scss";
import { SidebarSection } from "./sidebar_section/SidebarSection";
import { MainSection } from "./main_section/MainSection";
import { useStartPython } from "@logics/useStartPython";
export const MainWindow = () => {
const { asyncStartPython } = useStartPython();
const hasRunRef = useRef(false);
useEffect(() => {
if (!hasRunRef.current) {
asyncStartPython();
}
return () => hasRunRef.current = true;
}, []);
return (
<div className={styles.container}>
<SidebarSection />
<MainSection />
<MainWindowCover />
</div>
);
};
import { useTranslation } from "react-i18next";
import { useIsOpenedConfigWindow } from "@store";
import { useWindow } from "@utils/useWindow";
export const MainWindowCover = () => {
const { t } = useTranslation();
const { currentIsOpenedConfigWindow } = useIsOpenedConfigWindow();
const { closeConfigWindow } = useWindow();
// console.log(currentIsOpenedConfigWindow);
if ( currentIsOpenedConfigWindow === false) return null;
const closeSettingsWindow = () => closeConfigWindow();
return (
<div className={styles.main_window_cover}>
<p className={styles.cover_message}>{t("main_window.cover_message")}</p>
<button
className={styles.close_settings_window_button}
onClick={closeSettingsWindow}
>
{t("main_window.close_settings_window")}
</button>
</div>
);
};

View File

@@ -0,0 +1,45 @@
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
background-color: var(--dark_888_color);
position: relative;
}
.main_window_cover {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: (#000000cc);
display: flex;
flex-direction: column;
gap: 2rem;
justify-content: center;
align-items: center;
backdrop-filter: blur(0.8rem);
color: var(--dark_basic_text_color);
}
.cover_message {
font-size: 2rem;
font-weight: 300;
}
.close_settings_window_button {
font-size: 1.6rem;
font-weight: 300;
padding: 1rem 1.4rem;
border-radius: 0.4rem;
background-color: var(--dark_888_color);
cursor: pointer;
&:hover {
background-color: var(--dark_825_color);
}
&:active {
background-color: var(--dark_925_color);
}
}

View File

@@ -0,0 +1,11 @@
import React from "react";
import ReactDOM from "react-dom/client";
import "../../../locales/config.js";
import "@utils/root.css";
import { MainWindow } from "./MainWindow";
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<MainWindow />
</React.StrictMode>,
);

View File

@@ -0,0 +1,30 @@
import styles from "./MainSection.module.scss";
import { TopBar } from "./top_bar/TopBar";
import { MessageContainer } from "./message_container/MessageContainer";
import { LanguageSelector } from "./language_selector/LanguageSelector";
import { useIsOpenedLanguageSelector } from "@store";
export const MainSection = () => {
return (
<div className={styles.container}>
<TopBar />
<MessageContainer />
<HandleLanguageSelector />
</div>
);
};
const HandleLanguageSelector = () => {
const { currentIsOpenedLanguageSelector } = useIsOpenedLanguageSelector();
if (currentIsOpenedLanguageSelector.your_language === true) {
return <LanguageSelector id="your_language"/>;
} else if (currentIsOpenedLanguageSelector.target_language === true) {
return <LanguageSelector id="target_language"/>;
} else {
return null;
}
};

View File

@@ -0,0 +1,13 @@
.container {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.language_selector_container {
width: 100%;
height: 100%;
}

View File

@@ -0,0 +1,61 @@
import { useTranslation } from "react-i18next";
import { language_list } from "@data";
import styles from "./LanguageSelector.module.scss";
import { LanguageSelectorTopBar } from "./language_selector_top_bar/LanguageSelectorTopBar";
export const LanguageSelector = ({ id }) => {
const { t } = useTranslation();
const languageTitles = {
"your_language": t("selectable_language_window.title_your_language"),
"target_language": t("selectable_language_window.title_target_language")
};
const language_selector_title = languageTitles[id] || "";
const groupLanguagesByFirstLetter = (languages) => {
return languages.reduce((acc, { language, country }) => {
const firstLetter = language[0].toUpperCase();
if (!acc[firstLetter]) {
acc[firstLetter] = [];
}
acc[firstLetter].push({ language, country });
return acc;
}, {});
};
const groupedLanguages = groupLanguagesByFirstLetter(language_list);
return (
<div className={styles.container}>
<LanguageSelectorTopBar title={language_selector_title}/>
<div className={styles.language_list_scroll_wrapper}>
<div className={styles.language_list}>
{Object.entries(groupedLanguages).map(([letter, languages]) => (
<LanguageGroup key={letter} letter={letter} languages={languages} />
))}
</div>
</div>
</div>
);
};
const LanguageGroup = ({ letter, languages }) => {
return (
<div className={styles.language_each_letter_box}>
<p className={styles.language_latter}>{letter}</p>
{languages.map((languageData, index) => (
<LanguageButton key={index} languageData={languageData} />
))}
</div>
);
};
const LanguageButton = ({ languageData }) => {
return (
<div className={styles.language_button}>
<p className={styles.language_label}>{languageData.language} ({languageData.country})</p>
</div>
);
};

View File

@@ -0,0 +1,50 @@
.container {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: var(--dark_875_color);
flex: 1;
overflow-y: hidden;
}
.language_list_scroll_wrapper {
height: 100%;
overflow-y: auto;
padding: 1.4rem 1rem 8rem 1rem;
}
.language_list {
column-count: auto;
column-width: 16rem;
}
.language_each_letter_box {
break-inside: avoid-column;
margin-bottom: 1.4rem;
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.language_latter {
font-size: 1.4rem;
color: var(--dark_500_color);
}
.language_button {
padding: 0.8rem 0.6rem;
cursor: pointer;
&:hover{
background-color: var(--dark_825_color);
}
&:active{
background-color: var(--dark_888_color);
}
}
.language_label {
font-size: 1.4rem;
color: var(--dark_basic_text_color);
}

View File

@@ -0,0 +1,23 @@
import styles from "./LanguageSelectorTopBar.module.scss";
import { useIsOpenedLanguageSelector } from "@store";
export const LanguageSelectorTopBar = (props) => {
const { updateIsOpenedLanguageSelector } = useIsOpenedLanguageSelector();
const closeLanguageSelector = () => {
updateIsOpenedLanguageSelector({
your_language: false,
target_language: false,
});
};
return (
<div className={styles.container}>
<div className={styles.go_back_button_wrapper} onClick={closeLanguageSelector}>
<p className={styles.go_back_button_label}>Go Back</p>
</div>
<p className={styles.title}>{props.title}</p>
</div>
);
};

View File

@@ -0,0 +1,32 @@
.container {
height: var(--main_window_topbar_height);
background-color: var(--dark_850_color);
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.title {
font-size: 2rem;
color: var(--dark_400_color);
}
.go_back_button_wrapper {
position: absolute;
left: 0;
background-color: var(--dark_800_color);
padding: 1.2rem;
cursor: pointer;
&:hover{
background-color: var(--dark_750_color);
}
&:active{
background-color: var(--dark_875_color);
}
}
.go_back_button_label {
font-size: 1.4rem;
color: var(--dark_400_color);
}

View File

@@ -0,0 +1,34 @@
import { useResizable } from "react-resizable-layout";
import styles from "./MessageContainer.module.scss";
import { LogBox } from "./log_box/LogBox";
import { MessageInputBox } from "./message_input_box/MessageInputBox";
export const MessageContainer = () => {
const { position, separatorProps } = useResizable({
axis: "y",
reverse: true
});
return (
<div className={styles.container}>
<LogBox />
<Separator
dir={"horizontal"}
{...separatorProps}
/>
<div className={styles.message_box_resize_wrapper} style={ { height: `${(position / 10) - 1.5 }rem` } }>
<MessageInputBox />
</div>
</div>
);
};
const Separator = ({ ...props }) => {
return (
<div tabIndex={0} className={styles.separator} {...props}>
<span className={styles.separator_line}></span>
</div>
);
};

View File

@@ -0,0 +1,36 @@
.container {
height: 0%;
display: flex;
flex-direction: column;
flex: 1;
padding: 0 1.6rem 1rem 1.6rem;
}
.separator {
position: relative;
width: 100%;
height: 0.8rem;
cursor: row-resize;
flex-shrink: 0;
&:hover {
& .separator_line {
background-color: var(--primary_300_color);
}
}
}
.separator_line {
position: absolute;
bottom: 0%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
height: 50%;
width: 99%;
transition: background-color .15s ease-out;
}
.message_box_resize_wrapper {
height: 10rem;
min-height: 3.8rem;
max-height: 80%;
}

View File

@@ -0,0 +1,47 @@
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import styles from "./LogBox.module.scss";
import { useMessageLogs, store } from "@store";
import { MessageContainer } from "./message_container/MessageContainer";
import { scrollToBottom } from "@logics/scrollToBottom";
export const LogBox = () => {
const { currentMessageLogs } = useMessageLogs();
const log_container_ref = useRef(null);
const [is_scrolling, setIsScrolling] = useState(false);
useLayoutEffect(() => {
store.log_box_ref = log_container_ref;
if (!is_scrolling) {
scrollToBottom(store.log_box_ref, true);
}
}, [currentMessageLogs]);
useEffect(() => {
const handleScroll = () => {
const element = log_container_ref.current;
const currentScrollTop = element.scrollTop;
const at_bottom = element.scrollHeight - currentScrollTop === element.clientHeight;
if (at_bottom) {
setIsScrolling(false);
} else {
setIsScrolling(true);
}
};
const element = log_container_ref.current;
element.addEventListener("scroll", handleScroll);
return () => {
element.removeEventListener("scroll", handleScroll);
};
}, []);
return (
<div id="log_container" className={styles.container} ref={log_container_ref}>
{currentMessageLogs.map(message_data => (
<MessageContainer key={message_data.id} {...message_data} />
))}
</div>
);
};

View File

@@ -0,0 +1,13 @@
.container {
height: 100%;
width: 100%;
flex: 1;
background-color: var(--dark_900_color);
overflow: auto;
border-radius: 0.8rem;
padding: 1rem;
}
.text {
overflow-wrap: break-word;
}

View File

@@ -0,0 +1,44 @@
import { useTranslation } from "react-i18next";
import clsx from "clsx";
import styles from "./MessageContainer.module.scss";
export const MessageContainer = ({ messages, status, category, created_at }) => {
const { t } = useTranslation();
const is_translated_exist = messages.translated.length >= 1;
const is_pending = status === "pending";
const is_sent_message = category === "sent";
const category_text = is_sent_message ? t("main_window.textbox_tab_sent") : t("main_window.textbox_tab_received");
const message_type_class_name = clsx({
[styles.sent_message]: is_sent_message,
[styles.received_message]: !is_sent_message,
});
return (
<div className={clsx(styles.container, message_type_class_name)}>
<div className={clsx(styles.info_box, message_type_class_name)}>
<p className={styles.time}>{created_at}</p>
<p className={clsx(styles.category, message_type_class_name)}>{category_text}</p>
{is_sent_message && is_pending && <span className={styles.loader}></span>}
</div>
<div className={clsx(styles.message_box, message_type_class_name)}>
{is_translated_exist
? <WithTranslatedMessages messages={messages} />
: <p className={styles.message_main}>{messages.original}</p>
}
</div>
</div>
);
};
const WithTranslatedMessages = ({ messages }) => {
return (
<>
<p className={styles.message_second}>{messages.original}</p>
{messages.translated.map((message, index) => (
<p key={index} className={styles.message_main}>{message}</p>
))}
</>
);
};

View File

@@ -0,0 +1,70 @@
@import "@scss_mixins";
.container {
margin-bottom: 1rem;
width: 100%;
flex-direction: column;
display: flex;
justify-content: center;
&.sent_message {
align-items: end;
}
&.received_message {
align-items: start;
}
}
.info_box {
position: relative;
display: flex;
gap: 0.8rem;
justify-content: center;
&.sent_message {
align-items: end;
}
&.received_message {
flex-flow: row-reverse;
align-items: start;
}
}
.loader {
@include loader(0.8rem, 0.1rem, left, -1rem);
}
.time {
font-size: 1rem;
color: var(--dark_600_color);
}
.category {
font-size: 1rem;
&.sent_message {
color: var(--sent_400_color);
}
&.received_message {
align-items: start;
color: var(--received_300_color);
}
}
.message_box {
display: flex;
flex-direction: column;
&.sent_message {
align-items: end;
}
&.received_message {
align-items: start;
}
}
.message_main {
color: var(--dark_basic_text_color);
font-size: 1.4rem;
}
.message_second {
color: var(--dark_450_color);
font-size: 1rem;
}

View File

@@ -0,0 +1,45 @@
import { useState } from "react";
import styles from "./MessageInputBox.module.scss";
import SendMessageSvg from "@images/send_message.svg?react";
import { useMessage } from "@logics/useMessage";
import { store } from "@store";
import { scrollToBottom } from "@logics/scrollToBottom";
export const MessageInputBox = () => {
const [inputValue, setInputValue] = useState("");
const { sendMessage } = useMessage();
const onSubmitFunction = (e) => {
e.preventDefault();
sendMessage(inputValue);
setTimeout(() => {
scrollToBottom(store.log_box_ref);
}, 10);
};
const onChangeFunction = (e) => {
setInputValue(e.currentTarget.value);
};
return (
<div className={styles.container}>
<div className={styles.message_box_wrapper}>
<textarea
className={styles.message_box_input_area}
onChange={onChangeFunction}
placeholder="Input Textfield"
/>
</div>
<button
className={styles.message_send_button}
type="button"
onClick={onSubmitFunction}
>
<SendMessageSvg className={styles.message_send_icon} />
</button>
</div>
);
};

View File

@@ -0,0 +1,45 @@
.container {
height: 100%;
display: flex;
flex-direction: row;
}
.message_box_wrapper {
width: 100%;
height: 100%;
margin-right: 1rem;
padding: 0.8rem;
background-color: var(--dark_875_color);
border: 0.1rem solid var(--dark_750_color);
border-radius: 0.4rem;
}
.message_box_input_area {
width: 100%;
height: 100%;
font-size: 1.6rem;
resize: none;
}
.message_send_button {
display: flex;
justify-content: center;
align-items: center;
max-width: 10rem;
height: 100%;
font-size: 1.2rem;
background-color: var(--dark_850_color);
border-radius: 0.4rem;
aspect-ratio: 1 / 1;
&:hover {
background-color: var(--dark_825_color);
}
&:active {
background-color: var(--dark_900_color);
}
}
.message_send_icon {
width: 2rem;
color: var(--dark_400_color);
}

View File

@@ -0,0 +1,13 @@
import styles from "./TopBar.module.scss";
import { SidebarCompactModeButton } from "./sidebar_compact_mode_button/SidebarCompactModeButton";
import { RightSideComponents } from "./right_side_components/RightSideComponents";
export const TopBar = () => {
return (
<div className={styles.container}>
<SidebarCompactModeButton />
<RightSideComponents />
</div>
);
};

View File

@@ -0,0 +1,7 @@
.container {
height: var(--main_window_topbar_height);
display: flex;
justify-content: space-between;
align-items: center;
margin-right: 1.6rem;
}

View File

@@ -0,0 +1,20 @@
import styles from "./RightSideComponents.module.scss";
import HelpSvg from "@images/help.svg?react";
export const RightSideComponents = () => {
return (
<div className={styles.container}>
<p>VRC mic mute sync</p>
<p>Overlay(VR)</p>
<a
className={styles.help_and_info_button}
href="https://mzsoftware.notion.site/VRCT-Documents-be79b7a165f64442ad8f326d86c22246"
target="_blank"
rel="noreferrer"
>
<HelpSvg className={styles.help_svg} />
</a>
</div>
);
};

View File

@@ -0,0 +1,23 @@
.container {
display: flex;
flex-direction: row;
align-items: center;
gap: 1rem;
}
.help_and_info_button {
padding: 0.6rem;
border-radius: 0.6rem;
cursor: pointer;
&:hover {
background-color: var(--dark_800_color);
}
&:active {
background-color: var(--dark_900_color);
}
}
.help_svg {
width: 2.4rem;
color: var(--dark_400_color);
}

View File

@@ -0,0 +1,23 @@
import clsx from "clsx";
import styles from "./SidebarCompactModeButton.module.scss";
import { useIsCompactMode } from "@store";
import ArrowLeftSvg from "@images/arrow_left.svg?react";
export const SidebarCompactModeButton = () => {
const { updateIsCompactMode, currentIsCompactMode } = useIsCompactMode();
const toggleCompactMode = () => {
updateIsCompactMode(!currentIsCompactMode);
};
const class_names = clsx(styles["arrow_left_svg"], {
[styles["reverse"]]: currentIsCompactMode
});
return (
<div className={styles.container} onClick={toggleCompactMode}>
<ArrowLeftSvg className={class_names} preserveAspectRatio="none" />
</div>
);
};

View File

@@ -0,0 +1,22 @@
.container {
height: 100%;
width: 2.2rem;
background-color: var(--dark_850_color);
cursor: pointer;
&:hover {
background-color: var(--dark_800_color);
}
&:active {
background-color: var(--dark_900_color);
}
}
.arrow_left_svg {
height: 100%;
width: 100%;
padding: 1.1rem 0rem;
color: var(--dark_400_color);
&.reverse {
transform: rotate(180deg);
}
}

View File

@@ -0,0 +1,25 @@
import clsx from "clsx";
import styles from "./SidebarSection.module.scss";
import { useIsCompactMode } from "@store";
import { Logo } from "./logo/Logo";
import { MainFunctionSwitch } from "./main_function_switch/MainFunctionSwitch";
import { LanguageSettings } from "./language_settings/LanguageSettings";
import { OpenSettings } from "./open_settings/OpenSettings";
export const SidebarSection = () => {
const { currentIsCompactMode } = useIsCompactMode();
const container_class_name = clsx(styles["container"], {
[styles["is_compact_mode"]]: currentIsCompactMode
});
return (
<div className={container_class_name}>
<Logo />
<MainFunctionSwitch />
{!currentIsCompactMode && <LanguageSettings />}
<OpenSettings />
</div>
);
};

View File

@@ -0,0 +1,11 @@
.container {
position: relative;
min-width: 23rem;
height: 100%;
display: flex;
flex-direction: column;
background-color: var(--dark_850_color);
&.is_compact_mode {
min-width: auto;
}
}

View File

@@ -0,0 +1,101 @@
import { useTranslation } from "react-i18next";
import styles from "./LanguageSettings.module.scss";
import { PresetSelectTabs } from "./preset_select_tabs/PresetSelectTabs";
import { LanguageSelectorOpenButton } from "./language_selector_open_button/LanguageSelectorOpenButton";
import { LanguageSwapButton } from "./language_swap_button/LanguageSwapButton";
import { TranslatorSelectorOpenButton } from "./translator_selector_open_button/TranslatorSelectorOpenButton";
import { useOpenedTranslatorSelector } from "@store";
export const LanguageSettings = () => {
const { updateOpenedTranslatorSelector} = useOpenedTranslatorSelector();
const closeTranslatorSelector = () => updateOpenedTranslatorSelector(false);
return (
<div className={styles.container} onMouseLeave={closeTranslatorSelector} >
<p className={styles.title}>Language Settings</p>
<PresetSelectTabs />
<PresetContainer />
</div>
);
};
import MicSvg from "@images/mic.svg?react";
import HeadphonesSvg from "@images/headphones.svg?react";
import { useIsOpenedLanguageSelector } from "@store";
import { useMainFunction } from "@logics/useMainFunction";
const PresetContainer = () => {
const { t } = useTranslation();
const { updateIsOpenedLanguageSelector, currentIsOpenedLanguageSelector } = useIsOpenedLanguageSelector();
const {
currentState_TranscriptionSend,
currentState_TranscriptionReceive,
} = useMainFunction();
const closeLanguageSelector = () => {
updateIsOpenedLanguageSelector({
your_language: false,
target_language: false,
});
};
const toggleYourLanguageSelector = () => {
if (currentIsOpenedLanguageSelector.your_language === true) {
closeLanguageSelector();
} else {
updateIsOpenedLanguageSelector({
your_language: true,
target_language: false,
});
}
};
const toggleTargetLanguageSelector = () => {
if (currentIsOpenedLanguageSelector.target_language === true) {
closeLanguageSelector();
} else {
updateIsOpenedLanguageSelector({
your_language: false,
target_language: true,
});
}
};
const handleLanguageSelectorClick = (selector) => {
if (selector === "your_language") {
toggleYourLanguageSelector();
} else if (selector === "target_language") {
toggleTargetLanguageSelector();
}
};
const your_language_settings = {
title: t("main_window.your_language"),
is_opened: currentIsOpenedLanguageSelector.your_language,
onClickFunction: () => handleLanguageSelectorClick("your_language"),
TurnedOnSvgComponent: <MicSvg />,
is_turned_on: currentState_TranscriptionSend.data,
};
const target_language_settings = {
title: t("main_window.target_language"),
is_opened: currentIsOpenedLanguageSelector.target_language,
onClickFunction: () => handleLanguageSelectorClick("target_language"),
TurnedOnSvgComponent: <HeadphonesSvg />,
is_turned_on: currentState_TranscriptionReceive.data,
};
return (
<div className={styles.preset_container}>
<LanguageSelectorOpenButton {...your_language_settings} />
<LanguageSwapButton />
<LanguageSelectorOpenButton {...target_language_settings} />
<TranslatorSelectorOpenButton />
</div>
);
};

View File

@@ -0,0 +1,21 @@
.container {
display: flex;
flex-direction: column;
align-items: center;
}
.title {
font-size: 1.4rem;
padding-top: 1rem;
padding-bottom: 0.8rem;
color: var(--dark_400_color);
}
.preset_container {
width: 100%;
padding-top: 0.8rem;
background-color: var(--dark_800_color);
display: flex;
flex-direction: column;
align-items: center;
}

View File

@@ -0,0 +1,34 @@
import clsx from "clsx";
import styles from "./LanguageSelectorOpenButton.module.scss";
import ArrowLeftSvg from "@images/arrow_left.svg?react";
import { useSvg } from "@utils/useSvg";
export const LanguageSelectorOpenButton = (props) => {
const toggleLanguageSelector = () => {
props.onClickFunction();
};
const class_names = clsx(styles["arrow_left_svg"], {
[styles["reverse"]]: props.is_opened
});
const SvgComponent = useSvg(props.TurnedOnSvgComponent,
{className: clsx(styles["category_svg"], {
[styles["is_turned_on"]]: props.is_turned_on
})}
);
return (
<div className={styles.container}>
<div className={styles.title_container}>
{SvgComponent}
<p className={styles.title}>{props.title}</p>
</div>
<div className={styles.dropdown_menu_container} onClick={toggleLanguageSelector}>
<p className={styles.selected_language}>Japanese</p>
<p className={styles.selected_language}>(Japan)</p>
<ArrowLeftSvg className={class_names} />
</div>
</div>
);
};

View File

@@ -0,0 +1,73 @@
.container {
width: 100%;
background-color: var(--dark_825_color);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 0.8rem;
gap: 0.8rem;
}
.title_container {
position: relative;
width: 100%;
display: flex;
justify-content: center;
text-align: center;
}
.category_svg {
position: absolute;
top: 50%;
left: 1.2rem;
transform: translate(-50%, -50%);
width: 1.4rem;
color: var(--dark_400_color);
display: none;
&.is_turned_on {
display: block;
}
}
.title {
font-size: 1.6rem;
color: var(--dark_400_color);
}
.dropdown_menu_container {
position: relative;
background-color: var(--dark_888_color);
width: 100%;
padding: 0.2rem 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border-radius: 0.4rem;
gap: 0.2rem;
cursor: pointer;
&:hover {
background-color: var(--dark_875_color);
}
&:active {
background-color: var(--dark_900_color);
}
}
.selected_language {
font-size: 1.2rem;
color: var(--dark_basic_text_color);
}
.arrow_left_svg {
position: absolute;
right: 0;
margin: 0 0.2rem;
transform: rotate(180deg);
width: 1.6rem;
color: var(--dark_basic_text_color);
&.reverse {
transform: rotate(0deg);
}
}

View File

@@ -0,0 +1,37 @@
import { useState } from "react";
import clsx from "clsx";
import { useTranslation } from "react-i18next";
import styles from "./LanguageSwapButton.module.scss";
import NarrowArrowDownSvg from "@images/narrow_arrow_down.svg?react";
export const LanguageSwapButton = () => {
const [isHovered, setIsHovered] = useState(false);
const { t } = useTranslation();
const label = isHovered
? t("main_window.swap_button_label")
: t("main_window.translate_each_other_label");
const labelClassName = clsx(styles["label"], {
[styles["is_hovered"]]: isHovered
});
const handleMouseEnter = () => setIsHovered(true);
const handleMouseLeave = () => setIsHovered(false);
return (
<div className={styles.container}>
<div
className={styles.swap_button_wrapper}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<NarrowArrowDownSvg className={clsx(styles.narrow_arrow_down_svg, styles.reverse)} />
<p className={labelClassName}>{label}</p>
<NarrowArrowDownSvg className={styles.narrow_arrow_down_svg} />
</div>
</div>
);
};

View File

@@ -0,0 +1,36 @@
.container {
width: 100%;
}
.swap_button_wrapper {
width: auto;
display: flex;
justify-content: space-between;
align-items: center;
margin: 0.8rem 2rem;
padding: 0.4rem 0.8rem;
border-radius: 0.6rem;
cursor: pointer;
&:hover {
background-color: var(--dark_750_color);
}
&:active {
background-color: var(--dark_850_color);
}
}
.narrow_arrow_down_svg {
width: 1.6rem;
color: var(--dark_500_color);
&.reverse {
transform: rotate(180deg);
}
}
.label {
font-size: 1.2rem;
color: var(--dark_500_color);
&.is_hovered {
color: var(--dark_basic_text_color);
}
}

View File

@@ -0,0 +1,32 @@
import styles from "./PresetSelectTabs.module.scss";
export const PresetSelectTabs = () => {
return (
<div className={styles.container}>
<Tab preset_number={1} />
<Tab preset_number={2} />
<Tab preset_number={3} />
</div>
);
};
import clsx from "clsx";
import { useSelectedTab } from "@store";
const Tab = (props) => {
const { updateSelectedTab, currentSelectedTab } = useSelectedTab();
const onclickFunction = () => {
updateSelectedTab(props.preset_number);
};
const class_names = clsx(styles["tab_container"], {
[styles["is_selected"]]: (currentSelectedTab === props.preset_number) ? true : false
});
return (
<div className={class_names} onClick={onclickFunction}>
<p className={styles.tab_number}>{props.preset_number}</p>
</div>
);
};

View File

@@ -0,0 +1,32 @@
.container {
width: 100%;
display: flex;
justify-content: space-between;
}
.tab_container {
height: 3rem;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
border-radius: 0.6rem 0.6rem 0 0;
color: var(--dark_600_color);
cursor: pointer;
&:hover {
background-color: var(--dark_825_color);
}
&:active {
background-color: var(--dark_875_color);
}
&.is_selected {
background-color: var(--dark_800_color);
color: var(--dark_basic_text_color);
cursor: default;
pointer-events: none;
}
}
.tab_number {
font-size: 1.6rem;
}

View File

@@ -0,0 +1,28 @@
import { useTranslation } from "react-i18next";
import styles from "./TranslatorSelectorOpenButton.module.scss";
import { TranslatorSelector } from "./translator_selector/TranslatorSelector";
import { useTranslatorList, useSelectedTranslator, useOpenedTranslatorSelector } from "@store";
export const TranslatorSelectorOpenButton = () => {
const { t } = useTranslation();
const { currentSelectedTranslator } = useSelectedTranslator();
const { currentTranslatorList } = useTranslatorList();
const currentTranslator = currentTranslatorList.find(
translator_data => translator_data.translator_key === currentSelectedTranslator
);
const { currentOpenedTranslatorSelector, updateOpenedTranslatorSelector} = useOpenedTranslatorSelector();
const openTranslatorSelector = () => updateOpenedTranslatorSelector(!currentOpenedTranslatorSelector);
return (
<div className={styles.container}>
<div className={styles.translator_selector_button} onClick={openTranslatorSelector}>
<p className={styles.label}>{t("main_window.translator")}</p>
<p className={styles.label}>{currentTranslator?.translator_name}</p>
</div>
{currentOpenedTranslatorSelector && <TranslatorSelector />}
</div>
);
};

View File

@@ -0,0 +1,27 @@
.container {
position: relative;
width: 100%;
}
.translator_selector_button {
width: auto;
display: flex;
justify-content: center;
align-items: center;
gap: 0.2rem;
margin: 0.4rem;
padding: 0.6rem 0;
border-radius: 0.6rem;
cursor: pointer;
&:hover {
background-color: var(--dark_875_color);
}
&:active {
background-color: var(--dark_900_color);
}
}
.label {
font-size: 1.2rem;
color: var(--dark_basic_text_color);
}

View File

@@ -0,0 +1,49 @@
import styles from "./TranslatorSelector.module.scss";
import { chunkArray } from "@utils/chunkArray";
import { useTranslatorList, useSelectedTranslator, useOpenedTranslatorSelector } from "@store";
export const TranslatorSelector = () => {
const { currentTranslatorList } = useTranslatorList();
const columns = chunkArray(currentTranslatorList, 2);
return (
<div className={styles.container}>
<div className={styles.wrapper}>
{columns.map((column, column_index) => (
<div className={styles.column_wrapper} key={`column_${column_index}`}>
{column.map(({ translator_key, translator_name, is_available }) => (
<TranslatorBox
key={translator_key}
translator_id={translator_key}
translator_name={translator_name}
is_available={is_available}
/>
))}
</div>
))}
</div>
</div>
);
};
import clsx from "clsx";
const TranslatorBox = (props) => {
const { currentSelectedTranslator, updateSelectedTranslator} = useSelectedTranslator();
const { updateOpenedTranslatorSelector} = useOpenedTranslatorSelector();
const box_class_name = clsx(
styles.box,
{ [styles["is_selected"]]: (currentSelectedTranslator === props.translator_id) ? true : false },
{ [styles["is_available"]]: (props.is_available === true) ? true : false }
);
const selectTranslator = () => {
updateSelectedTranslator(props.translator_id);
updateOpenedTranslatorSelector(false);
};
return (
<div className={box_class_name} onClick={selectTranslator}>
<p className={styles.translator_name}>{props.translator_name}</p>
</div>
);
};

View File

@@ -0,0 +1,63 @@
.container {
position: absolute;
bottom: 100%;
width: 100%;
height: 26rem;
padding: 1rem;
background-color: (#000000dd);
// background-color: (var(--dark_875_color) + 80);
backdrop-filter: blur(0.1rem);
display: flex;
justify-content: center;
align-items: center;
}
.wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1rem;
}
.column_wrapper {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
}
$box_size: 6.8rem;
.box {
width: $box_size;
height: $box_size;
background-color: var(--dark_875_color);
display: flex;
justify-content: center;
align-items: center;
white-space: pre-wrap;
text-align: center;
border-radius: 0.6rem;
cursor: pointer;
&:hover {
background-color: var(--dark_825_color);
}
&:active {
background-color: var(--dark_900_color);
border: 0.1rem solid var(--primary_300_color);
}
&.is_selected {
border: 0.2rem solid var(--primary_300_color);
}
&:not(.is_available) {
pointer-events: none;
background-color: var(--dark_950_color);
& .translator_name {
color: var(--dark_600_color);
}
}
}
.translator_name {
font-size: 1.4rem;
}

View File

@@ -0,0 +1,23 @@
import styles from "./Logo.module.scss";
export const Logo = () => {
return (
<div className={styles.container}>
<LogoBox />
</div>
);
};
import vrct_logo from "@images/vrct_logo_for_dark_mode.png";
import chato_img from "@images/chato_white.png";
import { useIsCompactMode } from "@store";
export const LogoBox = () => {
const { currentIsCompactMode } = useIsCompactMode();
if (currentIsCompactMode === true) {
return <img src={chato_img} className={styles.logo_chato} alt="VRCT logo chato" />;
} else {
return <img src={vrct_logo} className={styles.logo} alt="VRCT logo" />;
}
};

View File

@@ -0,0 +1,18 @@
.container {
height: var(--main_window_topbar_height);
display: flex;
justify-content: center;
align-items: center;
}
.logo {
width: 12rem;
height: auto;
margin: auto;
}
.logo_chato {
width: 2rem;
height: auto;
margin: auto;
}

View File

@@ -0,0 +1,117 @@
import { useTranslation } from "react-i18next";
import clsx from "clsx";
import styles from "./MainFunctionSwitch.module.scss";
import TranslationSvg from "@images/translation.svg?react";
import MicSvg from "@images/mic.svg?react";
import HeadphonesSvg from "@images/headphones.svg?react";
import ForegroundSvg from "@images/foreground.svg?react";
import { useIsCompactMode } from "@store";
import { useMainFunction } from "@logics/useMainFunction";
export const MainFunctionSwitch = () => {
const { t } = useTranslation();
const {
toggleTranslation, currentState_Translation,
toggleTranscriptionSend, currentState_TranscriptionSend,
toggleTranscriptionReceive, currentState_TranscriptionReceive,
toggleForeground, currentState_Foreground,
} = useMainFunction();
const switch_items = [
{
switch_id: "translation",
label: t("main_window.translation"),
SvgComponent: TranslationSvg,
currentState: currentState_Translation,
toggleFunction: toggleTranslation,
},
{
switch_id: "transcription_send",
label: t("main_window.transcription_send"),
SvgComponent: MicSvg,
currentState: currentState_TranscriptionSend,
toggleFunction: toggleTranscriptionSend,
},
{
switch_id: "transcription_receive",
label: t("main_window.transcription_receive"),
SvgComponent: HeadphonesSvg,
currentState: currentState_TranscriptionReceive,
toggleFunction: toggleTranscriptionReceive,
},
{
switch_id: "foreground",
label: t("main_window.foreground"),
SvgComponent: ForegroundSvg,
currentState: currentState_Foreground,
toggleFunction: toggleForeground,
},
];
return (
<div className={styles.container}>
{switch_items.map(item => (
<SwitchContainer
key={item.switch_id}
switch_id={item.switch_id}
switchLabel={item.label}
currentState={item.currentState}
toggleFunction={item.toggleFunction}
SvgComponent={item.SvgComponent}
>
</SwitchContainer>
))}
</div>
);
};
import { useState } from "react";
export const SwitchContainer = ({ switchLabel, switch_id, children, currentState, toggleFunction, SvgComponent }) => {
const [is_hovered, setIsHovered] = useState(false);
const [is_mouse_down, setIsMouseDown] = useState(false);
const { currentIsCompactMode } = useIsCompactMode();
const getClassNames = (baseClass) => clsx(baseClass, {
[styles.is_compact_mode]: currentIsCompactMode,
[styles.is_active]: (currentState.data === true),
[styles.is_loading]: (currentState.state === "loading"),
[styles.is_hovered]: is_hovered,
[styles.is_mouse_down]: is_mouse_down,
});
const onMouseEnter = () => setIsHovered(true);
const onMouseLeave = () => setIsHovered(false);
const onMouseDown = () => setIsMouseDown(true);
const onMouseUp = () => setIsMouseDown(false);
return (
<div className={getClassNames(styles.switch_container)}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onClick={toggleFunction}
>
<div className={styles.label_wrapper}>
<SvgComponent className={getClassNames(styles.switch_svg)} />
<p className={getClassNames(styles.switch_label)}>{switchLabel}</p>
{children}
</div>
<div className={getClassNames(styles.toggle_control)}>
<span className={getClassNames(styles.control)}></span>
</div>
<div className={getClassNames(styles.switch_indicator)}></div>
{(currentState.state === "loading")
? <span className={styles.loader}></span>
: null
}
</div>
);
};

View File

@@ -0,0 +1,88 @@
@import "@scss_mixins";
.container {
display: flex;
flex-direction: column;
justify-content: center;
gap: 0.1rem;
}
.switch_container {
position: relative;
display: flex;
justify-content: center;
align-items: center;
padding: 1.6rem 1.4rem;
background-color: var(--dark_825_color);
cursor: pointer;
&:hover {
background-color: var(--dark_800_color);
}
&:active {
background-color: var(--dark_875_color);
}
&.is_compact_mode {
padding: 1.5rem;
}
&.is_loading {
pointer-events: none;
}
}
.label_wrapper {
width: 100%;
display: flex;
justify-content: left;
align-items: center;
gap: 0.8rem;
}
$basic_label_color: var(--dark_basic_text_color);
$loading_label_color: var(--dark_500_color);
.switch_label {
font-size: 1.4rem;
color: $basic_label_color;
&.is_compact_mode {
display: none;
}
&.is_loading {
color: $loading_label_color;
}
}
.switch_svg {
width: 1.8rem;
color: $basic_label_color;
&.is_loading {
color: $loading_label_color;
}
&:not(.is_compact_mode) {
width: 1.6rem;
color: var(--dark_350_color);
}
}
.switch_indicator {
position: absolute;
top: 50%;
right: 0.4rem;
transform: translate(-50%, -50%);
width: 0.2rem;
height: 2.6rem;
border-radius: 0.1rem;
background-color: var(--primary_300_color);
display: none;
&.is_compact_mode.is_active {
display: block;
}
}
.loader {
@include loader(2rem, 0.2rem, left, 50%);
}
.toggle_control {
@include toggle_control_styles;
&.is_compact_mode {
display: none;
}
}

View File

@@ -0,0 +1,20 @@
import styles from "./OpenSettings.module.scss";
import ConfigurationSvg from "@images/configuration.svg?react";
import { useWindow } from "@utils/useWindow";
export const OpenSettings = () => {
const { createConfigWindow } = useWindow();
const openConfigWindow = () => {
createConfigWindow();
};
return (
<div className={styles.container}>
<div className={styles.open_config_window_button} onClick={openConfigWindow}>
<ConfigurationSvg className={styles.configuration_svg} />
</div>
</div>
);
};

View File

@@ -0,0 +1,28 @@
.container {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
background-color: var(--dark_850_color);
}
.open_config_window_button {
width: auto;
margin: 1rem;
padding: 0.8rem 0;
display: flex;
justify-content: center;
border-radius: 0.6rem;
cursor: pointer;
&:hover {
background-color: var(--dark_800_color);
}
&:active {
background-color: var(--dark_875_color);
}
}
.configuration_svg {
width: 2rem;
color: var(--dark_500_color);
}