[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,21 @@
import clsx from "clsx";
import styles from "./MainPage.module.scss";
import { SidebarSection } from "./sidebar_section/SidebarSection";
import { MainSection } from "./main_section/MainSection";
import { useIsOpenedConfigPage } from "@logics_common";
export const MainPage = () => {
const { currentIsOpenedConfigPage } = useIsOpenedConfigPage();
return (
<div className={clsx(styles.page, styles.main_page, {
[styles.show_config]: currentIsOpenedConfigPage.data,
[styles.show_main]: !currentIsOpenedConfigPage.data
})}>
<div className={styles.container}>
<SidebarSection />
<MainSection />
</div>
</div>
);
};

View File

@@ -0,0 +1,28 @@
.page {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
overflow: hidden;
transition: transform 0.5s ease;
}
.show_config.main_page {
transform: translateY(-100%);
}
.show_main.main_page {
transform: translateY(0%);
}
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
background-color: var(--dark_888_color);
position: relative;
}

View File

@@ -0,0 +1,102 @@
import { useI18n } from "@useI18n";
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 { useStore_IsOpenedLanguageSelector } from "@store";
import { useLanguageSettings } from "@logics_main";
import { useEffect } from "react";
import { PluginHost } from "./PluginHost";
import { usePlugins } from "@logics_configs";
export const MainSection = () => {
const { currentPluginsData } = usePlugins();
const render_plugins = currentPluginsData.data.filter((plugin) => (
plugin.is_downloaded &&
plugin.is_enabled &&
plugin.downloaded_plugin_info.is_plugin_supported &&
plugin.downloaded_plugin_info.location === "main_section"
));
return (
<div className={styles.container}>
<TopBar />
{render_plugins.length
? <PluginHost render_components={render_plugins}/>
: <MessageContainer />
}
<HandleLanguageSelector />
</div>
);
};
const HandleLanguageSelector = () => {
const { t } = useI18n();
const { currentIsOpenedLanguageSelector, updateIsOpenedLanguageSelector } = useStore_IsOpenedLanguageSelector();
const {
currentSelectedPresetTabNumber,
currentSelectedYourLanguages,
setSelectedYourLanguages,
currentSelectedTargetLanguages,
setSelectedTargetLanguages,
} = useLanguageSettings();
useEffect(() => {
updateIsOpenedLanguageSelector({
your_language: false,
target_language: false,
target_key: "1"
});
}, [currentSelectedPresetTabNumber.data, currentSelectedYourLanguages.data, currentSelectedTargetLanguages.data]);
const getTitle = (target_selector_key) => {
if (target_selector_key === "your_language") return t("main_page.language_selector.title_your_language");
if (target_selector_key === "target_language") {
if (currentSelectedTargetLanguages.data[currentSelectedPresetTabNumber.data]["2"].enable === false) return t("main_page.language_selector.title_target_language");
return `${t("main_page.language_selector.title_target_language")} (${currentIsOpenedLanguageSelector.data.target_key})`;
}
};
if (currentIsOpenedLanguageSelector.data.your_language === true) {
const onclickFunction_YourLanguage = (payload) => {
updateIsOpenedLanguageSelector({ your_language: false, target_language: false, target_key: currentIsOpenedLanguageSelector.data.target_key });
setSelectedYourLanguages({
...payload,
target_key: currentIsOpenedLanguageSelector.data.target_key,
});
};
const title = getTitle("your_language");
return (
<LanguageSelector
title={title}
onClickFunction={onclickFunction_YourLanguage}
/>
);
} else if (currentIsOpenedLanguageSelector.data.target_language === true) {
const onclickFunction_TargetLanguage = (payload) => {
updateIsOpenedLanguageSelector({ your_language: false, target_language: false, target_key: currentIsOpenedLanguageSelector.data.target_key });
setSelectedTargetLanguages({
...payload,
target_key: currentIsOpenedLanguageSelector.data.target_key,
});
};
const title = getTitle("target_language");
return (
<LanguageSelector
title={title}
onClickFunction={onclickFunction_TargetLanguage}
/>
);
} else {
return null;
}
};

View File

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

View File

@@ -0,0 +1,28 @@
import { usePlugins } from "@logics_configs";
import { ErrorBoundary } from "react-error-boundary";
export const PluginHost = ({ render_components }) => {
const { setErrorPlugin } = usePlugins();
return (
<>
{render_components.map((plugin, index) => {
const PluginComponent = plugin.component;
const plugin_id = plugin.plugin_id;
return PluginComponent ? (
<ErrorBoundary
key={plugin_id || index}
fallbackRender={() => null}
onError={(_error, _info) => {
// Disable the plugin on error
setErrorPlugin(plugin_id, "disabled_due_to_an_error");
}}
>
<PluginComponent />
</ErrorBoundary>
) : null;
})}
</>
);
};

View File

@@ -0,0 +1,63 @@
import { useI18n } from "@useI18n";
import { useLanguageSettings } from "@logics_main";
import styles from "./LanguageSelector.module.scss";
import { LanguageSelectorTopBar } from "./language_selector_top_bar/LanguageSelectorTopBar";
export const LanguageSelector = ({ title, onClickFunction }) => {
const { t } = useI18n();
const { currentSelectableLanguageList } = useLanguageSettings();
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(currentSelectableLanguageList.data);
return (
<div className={styles.container}>
<LanguageSelectorTopBar title={title}/>
<div className={styles.language_list_scroll_wrapper}>
<div className={styles.language_list}>
{Object.entries(groupedLanguages).map(([letter, languages]) => (
<LanguageGroup key={letter} onClickFunction={onClickFunction} letter={letter} languages={languages} />
))}
</div>
</div>
</div>
);
};
const LanguageGroup = ({ onClickFunction, letter, languages }) => {
return (
<div className={styles.language_each_letter_box}>
<p className={styles.language_latter}>{letter}</p>
{languages.map((language_data, index) => (
<LanguageButton key={index} onClickFunction={onClickFunction} language_data={language_data} />
))}
</div>
);
};
const LanguageButton = ({ onClickFunction, language_data }) => {
const adjustedOnClickFunction = () => {
onClickFunction({
language: language_data.language,
country: language_data.country,
});
};
return (
<div className={styles.language_button} onClick={adjustedOnClickFunction}>
<p className={styles.language_label}>{language_data.language} ({language_data.country})</p>
</div>
);
};

View File

@@ -0,0 +1,49 @@
.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: 1rem 1rem 8rem 1.6rem;
}
.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;
}

View File

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

View File

@@ -0,0 +1,37 @@
.container {
height: var(--main_page_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: 0 2rem 0 1.6rem;
height: 100%;
min-width: 8rem;
display: flex;
align-items: center;
justify-content: center;
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,88 @@
import { useResizable } from "react-resizable-layout";
import { useRef, useEffect, useState, forwardRef } from "react";
import styles from "./MessageContainer.module.scss";
import { LogBox } from "./log_box/LogBox";
import { MessageLogSettingsContainer } from "./message_log_settings_container/MessageLogSettingsContainer";
import { MessageInputBox } from "./message_input_box/MessageInputBox";
import { useMessageInputBoxRatio } from "@logics_main";
export const MessageContainer = () => {
const { currentMessageInputBoxRatio, asyncSetMessageInputBoxRatio } = useMessageInputBoxRatio();
const [ui_message_box_ratio, setUiMessageBoxRatio] = useState(false);
const [is_hovered, setIsHovered] = useState(false);
const container_ref = useRef(null);
const separator_ref = useRef(null);
const log_box_ref = useRef(null);
const message_input_box_wrapper_ref = useRef(null);
const calculateMessageInputBoxRatio = (position) => {
if (log_box_ref.current && message_input_box_wrapper_ref.current && separator_ref.current && container_ref.current) {
const container_padding_bottom = parseFloat(
window.getComputedStyle(container_ref.current).paddingBottom
);
const total_height =
log_box_ref.current.offsetHeight +
separator_ref.current.offsetHeight * 2 +
message_input_box_wrapper_ref.current.offsetHeight;
const adjusted_position = position - container_padding_bottom;
const message_box_ratio = (adjusted_position / total_height) * 100;
return message_box_ratio;
}
console.warn("References not ready for calculation");
return 10; // Default initial height percentage
};
const asyncSaveRatio = (position) => {
if (position > 0) {
asyncSetMessageInputBoxRatio(calculateMessageInputBoxRatio(position));
}
};
const { position, separatorProps } = useResizable({
axis: "y",
reverse: true,
});
useEffect(() => {
if (position > 0) {
setUiMessageBoxRatio(calculateMessageInputBoxRatio(position));
const timeout = setTimeout(() => {
asyncSaveRatio(position);
}, 200);
return () => clearTimeout(timeout);
}
}, [position]);
useEffect(() => {
setUiMessageBoxRatio(currentMessageInputBoxRatio.data);
}, [currentMessageInputBoxRatio]);
return (
<div className={styles.container} ref={container_ref}>
<div
className={styles.log_box_resize_wrapper}
ref={log_box_ref}
onMouseOver={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<LogBox />
<MessageLogSettingsContainer to_visible_toggle_bar={is_hovered} />
</div>
<Separator {...separatorProps} ref={separator_ref} />
<div
className={styles.message_box_resize_wrapper}
ref={message_input_box_wrapper_ref}
style={{ height: `${ui_message_box_ratio}%` }}
>
<MessageInputBox />
</div>
</div>
);
};
const Separator = forwardRef((props, ref) => (
<div tabIndex={0} className={styles.separator} ref={ref} {...props}>
<span className={styles.separator_line}></span>
</div>
));

View File

@@ -0,0 +1,42 @@
.container {
height: 0%;
display: flex;
flex-direction: column;
flex: 1;
padding: 0 1.6rem 1rem 1.6rem;
}
.log_box_resize_wrapper {
flex: 1;
overflow: auto;
position: relative;
}
.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: 10%;
min-height: 3.8rem;
max-height: 90%;
}

View File

@@ -0,0 +1,41 @@
import React, { useRef, useLayoutEffect, useEffect } from "react";
import styles from "./LogBox.module.scss";
import { MessageContainer } from "./message_container/MessageContainer";
import { useMessage } from "@logics_common";
import { useMessageLogScroll } from "@logics_main";
import { store } from "@store";
export const LogBox = () => {
const { currentMessageLogs } = useMessage();
const { scrollToBottom, isScrolling } = useMessageLogScroll();
const logContainerRef = useRef(null);
useLayoutEffect(() => {
store.log_box_ref = logContainerRef;
if (!isScrolling) {
scrollToBottom();
}
}, [currentMessageLogs.data, isScrolling]);
return (
<div id="log_container" className={styles.container} ref={logContainerRef}>
<MessageLogUiSizeController />
{currentMessageLogs.data.map((message_data) => (
<MessageContainer key={message_data.id} {...message_data} />
))}
</div>
);
};
import { useAppearance } from "@logics_configs";
const MessageLogUiSizeController = () => {
const { currentMessageLogUiScaling } = useAppearance();
const font_size = currentMessageLogUiScaling.data / 100;
useEffect(() => {
const log_container_el = document.getElementById("log_container");
log_container_el.style.setProperty("font-size", `${font_size}rem`);
}, [currentMessageLogUiScaling.data]);
return null;
};

View File

@@ -0,0 +1,10 @@
.container {
height: 100%;
width: 100%;
flex: 1;
background-color: var(--dark_900_color);
overflow: auto;
border-radius: 0.8rem;
padding: 1rem;
font-size: 1rem; // This is the standard font size for message logs, and is controlled by JavaScript. All child elements this are generally expressed in "em" and are affected by the font size setting here.
}

View File

@@ -0,0 +1,167 @@
import { useState } from "react";
import { useI18n } from "@useI18n";
import clsx from "clsx";
import styles from "./MessageContainer.module.scss";
import { MessageSubMenuContainer } from "./message_sub_menu_container/MessageSubMenuContainer";
import { useMessage } from "@logics_common";
import { useAppearance } from "@logics_configs";
export const MessageContainer = ({ messages, status, category, created_at }) => {
const { t } = useI18n();
const {
sendMessage,
updateMessageInputValue,
} = useMessage();
const { currentShowResendButton } = useAppearance();
const [is_hovered, setIsHovered] = useState(false);
const [is_locked, setIsLocked] = useState(false);
const resendFunction = () => {
sendMessage(messages.original.message);
};
const editFunction = () => {
updateMessageInputValue(messages.original.message);
};
const handleMouseEnter = () => {
if (!is_locked) {
setIsHovered(true);
}
};
const handleMouseLeave = () => {
setIsHovered(false);
setIsLocked(false);
};
const lockHoverState = () => {
setIsHovered(false);
setIsLocked(true);
};
const is_translation_exist = messages.translations?.length > 0;
const is_pending = status === "pending";
const is_sent_message = category === "sent";
const is_system_message = category === "system";
const category_text = is_sent_message
? t("main_page.message_log.sent")
: is_system_message
? t("main_page.message_log.system")
: t("main_page.message_log.received");
const message_type_class_name = clsx({
[styles.sent_message]: is_sent_message,
[styles.received_message]: !is_sent_message && !is_system_message,
[styles.system_message]: is_system_message,
});
return (
<div
className={clsx(styles.container, message_type_class_name)}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div className={clsx(styles.message_wrapper, 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_system_message ? (
<p className={styles.message_main_system}>{messages.original.message}</p>
) : is_translation_exist ? (
<WithTranslatedMessages messages={messages} />
) : (
<OriginalMessage messages={messages} />
)}
</div>
</div>
{currentShowResendButton.data && is_sent_message && is_hovered ? (
<MessageSubMenuContainer
setIsHovered={lockHoverState}
resendFunction={resendFunction}
editFunction={editFunction}
/>
) : null}
</div>
);
};
const MessageWithTransliteration = ({ item }) => {
const renderTokenNode = (token, key) => {
const orig = token.orig ?? "";
const hira = token.hira ?? "";
const hepburn = token.hepburn ?? "";
// Only hovered romaji if it exists. (No ruby cuz 'orig' and 'hira' are same.)
if (hira && hira === orig && hepburn) {
return (
<span key={key} title={hepburn} className={styles.with_hepburn}>
{orig}
</span>
);
}
// Ruby hiragana and hovered romaji.
if (hira && hepburn) {
return (
<ruby key={key} title={hepburn} className={styles.with_hepburn}>
{orig}
<rt>{hira}</rt>
</ruby>
);
}
// Ruby romaji or hiragana.
if (hepburn || hira) {
const ruby = hepburn ? hepburn : hira;
if (ruby !== orig) {
return (
<ruby key={key} className={styles.ruby}>
{orig}
<rt>{ruby}</rt>
</ruby>
);
};
}
// Nothing. Original only.
return (
<span key={key} className={styles.original_only}>
{orig}
</span>
);
};
if (!item.transliteration.length) {
return <p className={styles.message_main}>{item.message}</p>;
}
return (
<p className={styles.message_main}>
{item.transliteration.map((token, idx) => renderTokenNode(token, idx))}
</p>
);
};
const OriginalMessage = ({ messages }) => {
return (
<>
<MessageWithTransliteration item={messages.original} />
</>
);
};
const WithTranslatedMessages = ({ messages }) => {
return (
<>
<p className={styles.message_second}>{messages.original.message}</p>
{messages.translations.map((item, idx) => (
<div key={idx}>
<MessageWithTransliteration item={item} />
</div>
))}
</>
);
};

View File

@@ -0,0 +1,122 @@
@import "@scss_mixins";
// ******************* *******************
// ******************* Express in "em" not "rem" *******************
// ******************* *******************
.container {
width: 100%;
display: flex;
user-select: text;
gap: 0.6rem;
&.sent_message.is_shown_resend_button:hover {
background-color: var(--dark_950_color);
}
}
.message_wrapper {
display: flex;
width: 100%;
flex-direction: column;
justify-content: center;
align-items: end;
user-select: text;
padding: 0.6em 0;
&.sent_message {
align-items: end;
}
&.received_message {
align-items: start;
}
&.system_message {
flex-direction: row;
align-items: center;
gap: 0.6rem;
}
}
.info_box {
position: relative;
display: flex;
gap: 0.8em;
justify-content: center;
&.sent_message {
align-items: end;
}
&.received_message {
flex-flow: row-reverse;
align-items: start;
}
}
.loader {
@include loader(0.8em, 0.1em, left, -1em);
}
.time {
font-size: 1em;
color: var(--dark_600_color);
}
.category {
font-size: 1em;
&.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 {
width: 100%;
align-items: end;
text-align: end;
}
&.received_message {
width: 100%;
align-items: start;
}
}
.message_main {
user-select: text;
font-size: 1.4em;
overflow-wrap: break-word;
max-width: 100%;
}
.message_second {
color: var(--dark_450_color);
user-select: text;
font-size: 1em;
}
.system_message {
justify-content: center;
text-align: center;
.category {
color: var(--primary_300_color);
}
.message_box {
align-items: center;
}
.message_main_system {
font-size: 1.2rem;
color: var(--dark_500_color);
}
}
// For ruby
.with_hepburn {
&:hover {
color: var(--dark_500_color);
}
}

View File

@@ -0,0 +1,73 @@
import React, { useState, useRef } from "react";
import { useI18n } from "@useI18n";
import Tooltip, { tooltipClasses } from '@mui/material/Tooltip';
import styles from "./MessageSubMenuContainer.module.scss";
import SendMessageSvg from "@images/send_message.svg?react";
import RefreshSvg from "@images/refresh_2.svg?react";
export const MessageSubMenuContainer = (props) => {
const [is_holding, setIsHolding] = useState(false);
const progressRef = useRef(null);
const holdTimeout = useRef(null);
const startHold = () => {
setIsHolding(true);
if (progressRef.current) {
progressRef.current.style.transition = "width 500ms linear";
progressRef.current.style.width = "100%";
}
holdTimeout.current = setTimeout(() => {
props.resendFunction();
props.setIsHovered(false);
}, 500);
};
const cancelHold = () => {
setIsHolding(false);
if (progressRef.current) {
progressRef.current.style.transition = "none";
progressRef.current.style.width = "0%";
}
clearTimeout(holdTimeout.current);
};
const onClickFunction = () => {
props.editFunction();
};
const offset = {
popper: {
sx: {
[`&.${tooltipClasses.popper}[data-popper-placement*="top"] .${tooltipClasses.tooltip}`]: { marginBottom: "0.2em" },
}
}
};
return (
<div className={styles.container}>
<Tooltip
title={<Title_p />}
placement="top"
slotProps={offset}
>
<button
className={styles.resend_button}
onMouseDown={startHold}
onMouseUp={cancelHold}
onMouseLeave={cancelHold}
onClick={onClickFunction}
>
<SendMessageSvg className={styles.send_message_svg} />
<RefreshSvg className={styles.refresh_svg} />
<div ref={progressRef} className={styles.hold_progress_bar}></div>
</button>
</Tooltip>
</div>
);
};
const Title_p = () => {
const { t } = useI18n();
return <p className={styles.tooltip_title}>{t("main_page.message_log.resend_button_on_hover_desc")}</p>;
};

View File

@@ -0,0 +1,45 @@
// ******************* *******************
// ******************* Express in "em" not "rem" *******************
// ******************* *******************
.container {
}
.resend_button {
background-color: var(--dark_825_color);
position: relative;
height: 100%;
width: 3.8em;
}
.send_message_svg {
position: absolute;
top: 58%;
left: 50%;
transform: translate(-50%, -50%);
width: 2.2em;
color: var(--dark_400_color);
}
.refresh_svg {
position: absolute;
top: 36%;
left: 42%;
transform: translate(-50%, -50%);
width: 1.8em;
color: var(--sent_400_color);
filter: drop-shadow(0.2em 0.2em 0 var(--dark_825_color));
}
.tooltip_title {
font-size: 1.2rem;
color: var(--dark_200_color);
}
.hold_progress_bar {
position: absolute;
top: 10%;
left: 50%;
transform: translate(-50%, -50%);
width: 0%;
height: 0.4em;
background-color: var(--sent_400_color);
transition: none;
}

View File

@@ -0,0 +1,126 @@
import { useState, useEffect, useLayoutEffect, useRef } from "react";
import styles from "./MessageInputBox.module.scss";
import SendMessageSvg from "@images/send_message.svg?react";
import { useMessage } from "@logics_common";
import { useAppearance, useOthers } from "@logics_configs";
import { useMessageLogScroll } from "@logics_main";
import { store } from "@store";
export const MessageInputBox = () => {
const [message_history, setMessageHistory] = useState([]);
const [history_index, setHistoryIndex] = useState(-1);
const {
sendMessage,
currentMessageLogs,
currentMessageInputValue,
updateMessageInputValue,
startTyping,
stopTyping,
} = useMessage();
const { currentEnableAutoClearMessageInputBox } = useOthers();
const { currentSendMessageButtonType } = useAppearance();
const { scrollToBottom } = useMessageLogScroll();
const log_box_ref = useRef(null);
useLayoutEffect(() => {
store.text_area_ref = log_box_ref;
}, []);
useEffect(() => {
if (currentMessageLogs.data) {
const sentMessages = currentMessageLogs.data
.filter(log => log.category === "sent")
.map(log => log.messages.original.message);
setMessageHistory(sentMessages);
}
}, [currentMessageLogs.data]);
const onSubmitFunction = (e) => {
e.preventDefault();
if (!currentMessageInputValue.data.trim()) return updateMessageInputValue("");
sendMessage(currentMessageInputValue.data);
if (currentEnableAutoClearMessageInputBox.data) updateMessageInputValue("");
setTimeout(() => {
scrollToBottom();
}, 10);
setHistoryIndex(-1);
};
const onChangeFunction = (e) => {
const value = e.currentTarget.value;
updateMessageInputValue(value);
value.trim() ? startTyping() : stopTyping();
};
const onKeyDownFunction = (e) => {
if (e.key === "ArrowUp" && e.shiftKey) {
e.preventDefault();
if (history_index + 1 < message_history.length) {
const new_index = history_index + 1;
setHistoryIndex(new_index);
updateMessageInputValue(message_history[message_history.length - 1 - new_index]);
}
}
if (e.key === "ArrowDown" && e.shiftKey) {
e.preventDefault();
if (history_index > -1) {
const new_index = history_index - 1;
setHistoryIndex(new_index);
updateMessageInputValue(
new_index >= 0
? message_history[message_history.length - 1 - new_index]
: ""
);
}
}
if (currentSendMessageButtonType.data === "show_and_disable_enter_key") return;
if (e.keyCode === 13 && !e.shiftKey) {
onSubmitFunction(e);
}
};
return (
<div className={styles.container}>
<div className={styles.message_box_wrapper}>
<textarea
ref={log_box_ref}
className={styles.message_box_input_area}
onChange={onChangeFunction}
onBlur={stopTyping}
// placeholder="Input Textfield"
value={currentMessageInputValue.data}
onKeyDown={onKeyDownFunction}
/>
</div>
{currentSendMessageButtonType.data !== "hide" && (
<SendMessageButton onSubmitFunction={onSubmitFunction} />
)}
</div>
);
};
const SendMessageButton = ({ onSubmitFunction }) => {
return (
<button
className={styles.message_send_button}
type="button"
onClick={onSubmitFunction}
>
<SendMessageSvg className={styles.message_send_icon} />
</button>
);
};

View File

@@ -0,0 +1,44 @@
.container {
height: 100%;
display: flex;
gap: 1rem;
}
.message_box_wrapper {
width: 100%;
height: 100%;
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,36 @@
import { useState } from "react";
import styles from "./MessageLogSettingsContainer.module.scss";
import clsx from "clsx";
import { useI18n } from "@useI18n";
import { MessageLogUiScalingContainer } from "@setting_box";
import ConfigSvg from "@images/configuration.svg?react";
export const MessageLogSettingsContainer = (props) => {
const { t } = useI18n();
const [is_opened, setIsOpened] = useState(false);
const [is_hovered, setIsHovered] = useState(false);
const container_class_name = clsx(styles.container, {
[styles.to_visible_toggle_bar]: props.to_visible_toggle_bar,
[styles.is_hovered]: is_hovered,
[styles.is_opened]: is_opened
});
return (
<div className={container_class_name}
onMouseOver={() => setIsHovered(true)}
onMouseLeave={() => {setIsHovered(false); setIsOpened(false);}}
onClick={() => setIsOpened(true)}
>
<div className={styles.container_relative_wrapper}>
<div className={styles.config_svg_wrapper}>
<ConfigSvg className={styles.config_svg}/>
</div>
</div>
<MessageLogUiScalingContainer />
<div className={styles.others_wrapper}>
</div>
</div>
);
};

View File

@@ -0,0 +1,85 @@
$container_height: 18rem;
$toggle_config_size: 3rem;
.container {
position: absolute;
top: calc(-#{$container_height} + -#{$toggle_config_size});
left: 0;
height: $container_height;
width: 100%;
background-color: var(--dark_825_color);
transition: top 0.3s ease;
padding: 0.6rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
&.to_visible_toggle_bar {
top: calc(-#{$container_height} + 1rem);
& .config_svg_wrapper {
transform: translate(-50%, -50%) rotate(180deg);
}
}
&.is_hovered {
top: calc(-#{$container_height} + 2rem);
}
&.is_opened {
top: 0;
background-color: var(--dark_825_color_cc);
backdrop-filter: blur(0.6rem);
& .config_svg_wrapper {
width: 0%;
bottom: 0;
}
}
&:not(.is_opened) {
cursor: pointer;
}
}
.container_relative_wrapper {
position: absolute;
width: 100%;
height: 100%;
}
.config_svg_wrapper {
position: absolute;
left: 50%;
bottom: -#{$toggle_config_size};
transform: translate(-50%, -50%) rotate(0deg);
background-color: var(--dark_825_color);
width: $toggle_config_size;
border-radius: 50%;
aspect-ratio: 1 / 1;
display: flex;
justify-content: center;
align-items: center;
transition: all 0.3s ease, transform 0.6s ease;
}
.config_svg {
width: calc($toggle_config_size / 1.6);
color: var(--dark_500_color);
}
.others_wrapper {
width: 100%;
display: flex;
}
.resend_checkbox_toggle {
display: flex;
justify-content: center;
align-items: center;
padding: 0.6rem 1.2rem;
gap: 1rem;
cursor: pointer;
&:hover {
background-color: var(--dark_850_color);
}
}
.resend_checkbox_label {
font-size: 1.4rem;
}

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_page_topbar_height);
display: flex;
justify-content: space-between;
align-items: center;
margin-right: 1.6rem;
}

View File

@@ -0,0 +1,101 @@
import { useI18n } from "@useI18n";
import styles from "./RightSideComponents.module.scss";
import RefreshSvg from "@images/refresh.svg?react";
import HelpSvg from "@images/help.svg?react";
import { useStore_OpenedQuickSetting } from "@store";
import { useSoftwareVersion } from "@logics_common";
import { useVr, useOthers } from "@logics_configs";
import { OpenQuickSettingButton } from "./_buttons/OpenQuickSettingButton";
export const RightSideComponents = () => {
return (
<div className={styles.container}>
<PluginsQuickSetting />
<OpenVrcMicMuteSyncQuickSetting />
<OpenOverlayQuickSetting />
<SoftwareUpdateAvailableButton />
<a
className={styles.help_and_info_button}
href="https://docs.google.com/spreadsheets/d/1_L5i-1U6PB1dnaPPTE_5uKMfqOpkLziPyRkiMLi4mqU/edit?usp=sharing"
target="_blank"
rel="noreferrer"
>
<HelpSvg className={styles.help_svg} />
</a>
</div>
);
};
const OpenOverlayQuickSetting = () => {
// const { t } = useI18n();
const { updateOpenedQuickSetting } = useStore_OpenedQuickSetting();
const {
currentIsEnabledOverlaySmallLog,
currentIsEnabledOverlayLargeLog,
} = useVr();
const onClickFunction = () => {
updateOpenedQuickSetting("overlay");
};
const is_enable = currentIsEnabledOverlaySmallLog.data === true || currentIsEnabledOverlayLargeLog.data === true;
return (
<OpenQuickSettingButton
label="Overlay(VR)"
variable={is_enable}
onClickFunction={onClickFunction}
/>
);
};
const PluginsQuickSetting = () => {
const { t } = useI18n();
const { updateOpenedQuickSetting } = useStore_OpenedQuickSetting();
const onClickFunction = () => {
updateOpenedQuickSetting("plugins");
};
return (
<OpenQuickSettingButton
label={t("config_page.side_menu_labels.plugins")}
onClickFunction={onClickFunction}
/>
);
};
const OpenVrcMicMuteSyncQuickSetting = () => {
const { t } = useI18n();
const { updateOpenedQuickSetting } = useStore_OpenedQuickSetting();
const { currentEnableVrcMicMuteSync } = useOthers();
const onClickFunction = () => {
updateOpenedQuickSetting("vrc_mic_mute_sync");
};
return (
<OpenQuickSettingButton
label={t("config_page.others.vrc_mic_mute_sync.label")}
variable={currentEnableVrcMicMuteSync.data.is_enabled}
onClickFunction={onClickFunction}
/>
);
};
const SoftwareUpdateAvailableButton = () => {
const { currentLatestSoftwareVersionInfo } = useSoftwareVersion();
const { t } = useI18n();
if (currentLatestSoftwareVersionInfo.data.is_update_available === false) return null;
const { updateOpenedQuickSetting } = useStore_OpenedQuickSetting();
return (
<button className={styles.software_update_button} onClick={()=>updateOpenedQuickSetting("update_software")}>
<RefreshSvg className={styles.refresh_svg}/>
<p className={styles.software_update_label}>{t("main_page.update_available")}</p>
</button>
);
};

View File

@@ -0,0 +1,49 @@
.container {
display: flex;
flex-direction: row;
align-items: center;
gap: 1rem;
height: 100%;
}
.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);
}
.software_update_button {
display: flex;
justify-content: center;
align-items: center;
gap: 0.4rem;
color: var(--primary_300_color);
padding: 1rem 0.4rem;
cursor: pointer;
&:hover {
background-color: var(--dark_800_color);
}
&:active {
background-color: var(--dark_900_color);
}
}
.refresh_svg {
width: 1.8rem;
transform: rotate(-30deg);
}
.software_update_label {
font-size: 1.2rem;
}

View File

@@ -0,0 +1,26 @@
import { useI18n } from "@useI18n";
import clsx from "clsx";
import styles from "./OpenQuickSettingButton.module.scss";
export const OpenQuickSettingButton = (props) => {
const { t } = useI18n();
const variable = (typeof props.variable === "boolean") ? props.variable : null;
return (
<div className={styles.container}>
<div className={styles.button_wrapper} onClick={props.onClickFunction}>
<p className={styles.button_label}>{props.label}</p>
{variable !== null && (
props.variable === true ? (
<p className={clsx(styles.button_indicator_label, styles.enabled)}>
{t("main_page.state_text_enabled")}
</p>
) : (
<p className={clsx(styles.button_indicator_label, styles.disabled)}>
{t("main_page.state_text_disabled")}
</p>
)
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,35 @@
.container {
height: 100%;
}
.button_wrapper {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 0.2rem;
padding: 0 0.8rem;
border-radius: 0.2rem;
cursor: pointer;
&:hover {
background-color: var(--dark_800_color);
}
&:active {
background-color: var(--dark_900_color);
}
}
.button_label {
font-size: 1.2rem;
}
.button_indicator_label {
font-size: 1rem;
&.disabled {
color: var(--dark_600_color);
}
&.enabled {
color: var(--primary_300_color);
}
}

View File

@@ -0,0 +1,19 @@
import clsx from "clsx";
import styles from "./SidebarCompactModeButton.module.scss";
import { useIsMainPageCompactMode } from "@logics_main";
import ArrowLeftSvg from "@images/arrow_left.svg?react";
export const SidebarCompactModeButton = () => {
const { toggleIsMainPageCompactMode, currentIsMainPageCompactMode } = useIsMainPageCompactMode();
const class_names = clsx(styles["arrow_left_svg"], {
[styles["reverse"]]: currentIsMainPageCompactMode.data
});
return (
<div className={styles.container} onClick={toggleIsMainPageCompactMode}>
<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,33 @@
import clsx from "clsx";
import styles from "./SidebarSection.module.scss";
import { useStore_IsOpenedLanguageSelector } from "@store";
import { useIsMainPageCompactMode } from "@logics_main";
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 { currentIsMainPageCompactMode } = useIsMainPageCompactMode();
const container_class_name = clsx(styles.container, {
[styles.is_compact_mode]: currentIsMainPageCompactMode.data
});
const { currentIsOpenedLanguageSelector } = useStore_IsOpenedLanguageSelector();
const scroll_container_class_names = clsx(styles.scroll_container, {
[styles.is_opened]: (currentIsOpenedLanguageSelector.data.your_language === true || currentIsOpenedLanguageSelector.data.target_language === true)
});
return (
<div className={container_class_name}>
<Logo />
<div className={scroll_container_class_names}>
<MainFunctionSwitch />
{!currentIsMainPageCompactMode.data && <LanguageSettings />}
</div>
<OpenSettings />
</div>
);
};

View File

@@ -0,0 +1,51 @@
.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;
.scroll_container {
// overflow-y: hidden;
// width: auto;
}
}
}
.scroll_container {
width: calc(100% + 0.8rem);
overflow-y: scroll;
overflow-x: hidden;
margin-bottom: calc(2rem + 1.6rem + 2rem); // config button's sizes (svg + padding + margin).
pointer-events: auto;
z-index: 1;
&::-webkit-scrollbar {
width: 0.8rem;
}
&::-webkit-scrollbar-track {
background-color: var(--dark_888_color);
border-radius: 0.4rem;
}
&::-webkit-scrollbar-thumb {
background-color: var(--dark_888_color);
border-radius: 0.4rem;
}
&.is_opened {
&::-webkit-scrollbar-track {
background-color: var(--dark_875_color);
}
&::-webkit-scrollbar-thumb {
background-color: var(--dark_875_color);
}
}
&:hover {
&::-webkit-scrollbar-thumb {
background-color: var(--dark_800_color);
}
}
}

View File

@@ -0,0 +1,55 @@
import { useI18n } from "@useI18n";
import styles from "./LanguageSettings.module.scss";
import { PresetTabSelector } from "./preset_tab_selector/PresetTabSelector";
import { LanguageSelectorOpenButton } from "./language_selector_open_button/LanguageSelectorOpenButton";
import { LanguageSwapButton } from "./language_swap_button/LanguageSwapButton";
import { TranslatorSelectorOpenButton } from "./translator_selector_open_button/TranslatorSelectorOpenButton";
import { AddRemoveTargetLanguageButtons } from "./add_remove_target_language_buttons/AddRemoveTargetLanguageButtons";
import { useStore_IsOpenedTranslatorSelector } from "@store";
export const LanguageSettings = () => {
const { t } = useI18n();
const { updateIsOpenedTranslatorSelector } = useStore_IsOpenedTranslatorSelector();
const closeTranslatorSelector = () => updateIsOpenedTranslatorSelector(false);
return (
<div className={styles.container} onMouseLeave={closeTranslatorSelector}>
<p className={styles.title}>{t("main_page.language_settings")}</p>
<PresetTabSelector />
<PresetContainer />
</div>
);
};
import MicSvg from "@images/mic.svg?react";
import HeadphonesSvg from "@images/headphones.svg?react";
import { useMainFunction } from "@logics_main";
const PresetContainer = () => {
const { t } = useI18n();
const { currentTranscriptionSendStatus, currentTranscriptionReceiveStatus } = useMainFunction();
const yourLanguageSettings = {
TurnedOnSvgComponent: MicSvg,
is_turned_on: currentTranscriptionSendStatus.data,
};
const targetLanguageSettings = {
TurnedOnSvgComponent: HeadphonesSvg,
is_turned_on: currentTranscriptionReceiveStatus.data,
};
return (
<div className={styles.preset_container}>
<LanguageSelectorOpenButton {...yourLanguageSettings} selector_key="your_language" target_key="1"/>
<LanguageSwapButton />
<div className={styles.target_language_containers}>
<LanguageSelectorOpenButton {...targetLanguageSettings} selector_key="target_language" target_key="1" />
<LanguageSelectorOpenButton {...targetLanguageSettings} selector_key="target_language" target_key="2" />
<LanguageSelectorOpenButton {...targetLanguageSettings} selector_key="target_language" target_key="3" />
</div>
<AddRemoveTargetLanguageButtons />
<TranslatorSelectorOpenButton />
</div>
);
};

View File

@@ -0,0 +1,29 @@
.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;
}
.target_language_containers {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.2rem;
}

View File

@@ -0,0 +1,34 @@
import clsx from "clsx";
import styles from "./AddRemoveTargetLanguageButtons.module.scss";
import RemoveSvg from "@images/remove.svg?react";
import AddSvg from "@images/add.svg?react";
import { useLanguageSettings } from "@logics_main";
export const AddRemoveTargetLanguageButtons = () => {
const {
currentSelectedPresetTabNumber,
// currentSelectedYourLanguages,
currentSelectedTargetLanguages,
removeTargetLanguage,
addTargetLanguage,
} = useLanguageSettings();
const remove_button_class = clsx(styles.remove_target_language_button, {
[styles.is_disabled]: !currentSelectedTargetLanguages.data[currentSelectedPresetTabNumber.data]["2"].enable,
});
const add_button_class = clsx(styles.add_target_language_button, {
[styles.is_disabled]: currentSelectedTargetLanguages.data[currentSelectedPresetTabNumber.data]["3"].enable,
});
return (
<div className={styles.add_remove_target_language_container}>
<div className={remove_button_class} onClick={removeTargetLanguage}>
<RemoveSvg className={styles.remove_svg} />
</div>
<div className={add_button_class} onClick={addTargetLanguage}>
<AddSvg className={styles.add_svg} />
</div>
</div>
);
};

View File

@@ -0,0 +1,40 @@
.add_remove_target_language_container {
width: 100%;
display: flex;
justify-content: end;
align-items: center;
}
.remove_target_language_button, .add_target_language_button {
display: flex;
justify-content: center;
align-items: center;
padding: 0.6rem 1rem;
cursor: pointer;
background-color: var(--dark_825_color);
&:hover {
background-color: var(--dark_875_color);
}
&:active {
background-color: var(--dark_900_color);
}
&.is_disabled {
pointer-events: none;
.remove_svg, .add_svg {
color: var(--dark_700_color);
}
}
}
.remove_target_language_button {
border-radius: 0 0 0 0.4rem;
}
.add_target_language_button {
border-radius: 0 0 0.4rem 0;
}
.remove_svg, .add_svg {
width: 0.8rem;
color: var(--dark_200_color);
}

View File

@@ -0,0 +1,73 @@
import { useI18n } from "@useI18n";
import clsx from "clsx";
import styles from "./LanguageSelectorOpenButton.module.scss";
import ArrowLeftSvg from "@images/arrow_left.svg?react";
import { useStore_IsOpenedLanguageSelector } from "@store";
import {
useLanguageSettings,
} from "@logics_main";
export const LanguageSelectorOpenButton = ({ TurnedOnSvgComponent, is_turned_on, selector_key, target_key }) => {
const { t } = useI18n();
const { updateIsOpenedLanguageSelector, currentIsOpenedLanguageSelector } = useStore_IsOpenedLanguageSelector();
const {
currentSelectedPresetTabNumber,
currentSelectedYourLanguages,
currentSelectedTargetLanguages,
} = useLanguageSettings();
const toggleSelector = () => {
if (currentIsOpenedLanguageSelector.data[selector_key] === true && currentIsOpenedLanguageSelector.data.target_key === target_key) { // Close Language Selector
updateIsOpenedLanguageSelector({ your_language: false, target_language: false, target_key: "1" });
} else { // Open Language Selector
updateIsOpenedLanguageSelector({
your_language: selector_key === "your_language",
target_language: selector_key === "target_language",
target_key: target_key,
});
}
};
const arrow_class_names = clsx(styles.arrow_left_svg, {
[styles.reverse]: (currentIsOpenedLanguageSelector.data[selector_key] === true && currentIsOpenedLanguageSelector.data.target_key === target_key),
});
const category_class_names = clsx(styles.category_svg, {
[styles.is_turned_on]: is_turned_on,
});
const getVariable = (target_selector_key) => {
if (target_selector_key === "your_language") return currentSelectedYourLanguages.data[currentSelectedPresetTabNumber.data];
if (target_selector_key === "target_language") return currentSelectedTargetLanguages.data[currentSelectedPresetTabNumber.data];
};
const getTitle = (target_selector_key) => {
if (target_selector_key === "your_language") return t("main_page.your_language");
if (target_selector_key === "target_language") {
if (currentSelectedTargetLanguages.data[currentSelectedPresetTabNumber.data]["2"].enable === false) return t("main_page.target_language");
return `${t("main_page.target_language")} ${target_key}`;
}
};
const title = getTitle(selector_key);
if (getVariable(selector_key)[target_key].enable === false) return null;
const language_text = getVariable(selector_key)[target_key].language ?? "Loading...";
const country_text = getVariable(selector_key)[target_key].country ?? "Loading...";
return (
<div className={styles.container}>
<div className={styles.title_container}>
<TurnedOnSvgComponent className={category_class_names} />
<p className={styles.title}>{title}</p>
</div>
<div className={styles.dropdown_menu_container} onClick={toggleSelector}>
<p className={styles.selected_language}>{language_text}</p>
<p className={styles.selected_language}>({country_text})</p>
<ArrowLeftSvg className={arrow_class_names} />
</div>
</div>
);
};

View File

@@ -0,0 +1,71 @@
.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.4rem 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;
}
.arrow_left_svg {
position: absolute;
right: 0;
margin: 0 0.2rem;
transform: rotate(180deg);
width: 1.6rem;
&.reverse {
transform: rotate(0deg);
}
}

View File

@@ -0,0 +1,40 @@
import { useState } from "react";
import clsx from "clsx";
import { useI18n } from "@useI18n";
import styles from "./LanguageSwapButton.module.scss";
import NarrowArrowDownSvg from "@images/narrow_arrow_down.svg?react";
import { useLanguageSettings } from "@logics_main";
export const LanguageSwapButton = () => {
const [isHovered, setIsHovered] = useState(false);
const { t } = useI18n();
const { swapSelectedLanguages } = useLanguageSettings();
const label = isHovered
? t("main_page.swap_button_label")
: t("main_page.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}
onClick={swapSelectedLanguages}
>
<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_200_color);
}
}

View File

@@ -0,0 +1,32 @@
import styles from "./PresetTabSelector.module.scss";
export const PresetTabSelector = () => {
return (
<div className={styles.container}>
<Tab preset_number={"1"} />
<Tab preset_number={"2"} />
<Tab preset_number={"3"} />
</div>
);
};
import clsx from "clsx";
import { useLanguageSettings } from "@logics_main";
const Tab = (props) => {
const { currentSelectedPresetTabNumber, setSelectedPresetTabNumber } = useLanguageSettings();
const onclickFunction = () => {
setSelectedPresetTabNumber(props.preset_number);
};
const class_names = clsx(styles.tab_container, {
[styles.is_selected]: (currentSelectedPresetTabNumber.data === 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_200_color);
cursor: default;
pointer-events: none;
}
}
.tab_number {
font-size: 1.6rem;
}

View File

@@ -0,0 +1,94 @@
import { useI18n } from "@useI18n";
import { updateLabelsById } from "@utils";
import styles from "./TranslatorSelectorOpenButton.module.scss";
import { TranslatorSelector } from "./translator_selector/TranslatorSelector";
import { useStore_IsOpenedTranslatorSelector } from "@store";
import { useLanguageSettings } from "@logics_main";
import WarningSvg from "@images/warning.svg?react";
export const TranslatorSelectorOpenButton = () => {
const { t } = useI18n();
const {
currentSelectedYourLanguages,
currentSelectedTargetLanguages,
currentSelectedPresetTabNumber,
currentTranslationEngines,
currentSelectedTranslationEngines,
} = useLanguageSettings();
// const new_labels = [
// {id: "CTranslate2", label: "AI\nCTranslate2"}
// ];
const translation_engines = currentTranslationEngines.data;
// const translation_engines = updateLabelsById(currentTranslationEngines.data, new_labels);
const selected_engine_id = currentSelectedTranslationEngines.data[currentSelectedPresetTabNumber.data];
const checkIsSelectedSameLanguage = () => {
const your_language_data = currentSelectedYourLanguages.data[currentSelectedPresetTabNumber.data];
const target_language_data = currentSelectedTargetLanguages.data[currentSelectedPresetTabNumber.data];
const yourLanguage = your_language_data["1"];
const yourLanguageName = yourLanguage.language;
const yourCountry = yourLanguage.country;
let is_selected_same_language = false;
for (const key in target_language_data) {
const targetLanguage = target_language_data[key];
if (targetLanguage.enable) {
const targetLanguageName = targetLanguage.language;
const targetCountry = targetLanguage.country;
if (yourLanguageName === targetLanguageName && yourCountry === targetCountry) {
is_selected_same_language = true;
break;
}
}
}
return is_selected_same_language;
};
const is_selected_same_language = checkIsSelectedSameLanguage();
const getSelectedLabel = () => {
const selected_engine = translation_engines.find(
d => d.id === selected_engine_id
);
return selected_engine?.label;
};
const is_loading = currentTranslationEngines.state === "pending";
const selected_label = is_loading ? "Loading..." : getSelectedLabel();
const { currentIsOpenedTranslatorSelector, updateIsOpenedTranslatorSelector} = useStore_IsOpenedTranslatorSelector();
const openTranslatorSelector = () => {
updateIsOpenedTranslatorSelector(!currentIsOpenedTranslatorSelector.data);
};
return (
<div className={styles.container}>
<div className={styles.translator_selector_button} onClick={openTranslatorSelector}>
<p className={styles.label}>{t("main_page.translator")}:</p>
<p className={styles.label}>{selected_label}</p>
{is_selected_same_language
? <WarningSvg className={styles.warning_svg}/>
: null
}
</div>
{currentIsOpenedTranslatorSelector.data &&
<TranslatorSelector
selected_id={selected_engine_id}
translation_engines={translation_engines}
is_selected_same_language={is_selected_same_language}
/>
}
</div>
);
};

View File

@@ -0,0 +1,35 @@
.container {
position: relative;
width: 100%;
}
.translator_selector_button {
position: relative;
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;
white-space: nowrap;
}
.warning_svg {
margin-left: 0.2rem;
padding-bottom: 0.2rem;
width: 1.8rem;
color: var(--warning_color);
}

View File

@@ -0,0 +1,77 @@
import clsx from "clsx";
import styles from "./TranslatorSelector.module.scss";
import { useI18n } from "@useI18n";
import { chunkArray } from "@utils";
import { useStore_IsOpenedTranslatorSelector } from "@store";
import { useLanguageSettings } from "@logics_main";
export const TranslatorSelector = ({selected_id, translation_engines, is_selected_same_language}) => {
const { t } = useI18n();
const columns = chunkArray(translation_engines, 2);
return (
<div className={styles.container}>
<div className={styles.relative_container}>
<div className={styles.wrapper}>
{columns.map((column, column_index) => (
<div className={styles.column_wrapper} key={`column_${column_index}`}>
{column.map(({ id, label, is_available, is_default }) => (
<TranslatorBox
key={id}
id={id}
label={label}
is_available={is_available}
is_default={is_default}
is_selected={(id === selected_id)}
/>
))}
</div>
))}
</div>
{is_selected_same_language ?
<div className={styles.is_selected_same_language_wrapper}>
<p className={styles.is_selected_same_language_text}>
{t("main_page.translator_selector.is_selected_same_language", {
your_language: t("main_page.your_language"),
target_language: t("main_page.target_language"),
ctranslate2: "CTranslate2",
})}
</p>
</div>
: null
}
</div>
</div>
);
};
const TranslatorBox = (props) => {
const { t } = useI18n();
const { setSelectedTranslationEngines} = useLanguageSettings();
const { updateIsOpenedTranslatorSelector} = useStore_IsOpenedTranslatorSelector();
const box_class_name = clsx(
styles.box,
{ [styles.is_selected]: props.is_selected },
{ [styles.is_available]: props.is_available }
);
const label_default_class_name = clsx(
styles.label_default,
{ [styles.is_selected]: props.is_selected },
);
const selectTranslator = () => {
if (props.is_selected === false) {
setSelectedTranslationEngines(props.id);
}
updateIsOpenedTranslatorSelector(false);
};
return (
<div className={box_class_name} onClick={selectTranslator}>
<p className={styles.translator_name}>{props.label}</p>
{props.is_default && <p className={label_default_class_name}>{t("main_page.translator_label_default")}</p>}
</div>
);
};

View File

@@ -0,0 +1,105 @@
.container {
position: absolute;
bottom: 100%;
width: 100%;
height: 26rem;
background-color: var(--dark_1000_color_dd);
backdrop-filter: blur(0.1rem);
display: flex;
justify-content: center;
align-items: center;
}
.relative_container {
position: relative;
width: 100%;
height: 100%;
}
.wrapper {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1.4rem;
}
.column_wrapper {
display: flex;
justify-content: center;
align-items: center;
gap: 1.2rem;
}
$box_size: 6.2rem;
.box {
position: relative;
width: 9.4rem;
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.2rem;
cursor: pointer;
&:hover {
background-color: var(--dark_825_color);
}
&:active {
background-color: var(--dark_900_color);
outline: 0.1rem solid var(--primary_300_color);
}
&.is_selected {
outline: 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;
}
.label_default {
background-color: var(--dark_875_color);
outline: 0.1rem solid var(--dark_1000_color);
padding: 0.2rem 0.4rem;
border-radius: 0.2rem;
font-size: 1.2rem;
position: absolute;
top: -0.8rem;
right: -0.8rem;
pointer-events: none;
&.is_selected {
outline: 0.1rem solid var(--primary_300_color);
}
}
.is_selected_same_language_wrapper {
position: absolute;
bottom: 0;
width: 100%;
height: 100%;
background-color: var(--dark_1000_color_66);
display: flex;
justify-content: center;
align-items: start;
pointer-events: none;
padding: 4rem 1rem;
}
.is_selected_same_language_text {
font-size: 1.4rem;
text-align: center;
text-wrap: balance;
}

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 { useIsMainPageCompactMode } from "@logics_main";
export const LogoBox = () => {
const { currentIsMainPageCompactMode } = useIsMainPageCompactMode();
if (currentIsMainPageCompactMode.data === 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,19 @@
.container {
height: var(--main_page_topbar_height);
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
.logo {
width: 12rem;
height: auto;
margin: auto;
}
.logo_chato {
width: 2rem;
height: auto;
margin: auto;
}

View File

@@ -0,0 +1,118 @@
import { useI18n } from "@useI18n";
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 {
useIsMainPageCompactMode,
useMainFunction,
} from "@logics_main";
export const MainFunctionSwitch = () => {
const { t } = useI18n();
const {
toggleTranslation, currentTranslationStatus,
toggleTranscriptionSend, currentTranscriptionSendStatus,
toggleTranscriptionReceive, currentTranscriptionReceiveStatus,
toggleForeground, currentForegroundStatus,
} = useMainFunction();
const switch_items = [
{
switch_id: "translation",
label: t("main_page.translation"),
SvgComponent: TranslationSvg,
currentState: currentTranslationStatus,
toggleFunction: toggleTranslation,
},
{
switch_id: "transcription_send",
label: t("main_page.transcription_send"),
SvgComponent: MicSvg,
currentState: currentTranscriptionSendStatus,
toggleFunction: toggleTranscriptionSend,
},
{
switch_id: "transcription_receive",
label: t("main_page.transcription_receive"),
SvgComponent: HeadphonesSvg,
currentState: currentTranscriptionReceiveStatus,
toggleFunction: toggleTranscriptionReceive,
},
{
switch_id: "foreground",
label: t("main_page.foreground"),
SvgComponent: ForegroundSvg,
currentState: currentForegroundStatus,
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 { currentIsMainPageCompactMode } = useIsMainPageCompactMode();
const getClassNames = (baseClass) => clsx(baseClass, {
[styles.is_compact_mode]: currentIsMainPageCompactMode.data,
[styles.is_active]: (currentState.data === true),
[styles.is_pending]: (currentState.state === "pending"),
[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 === "pending")
? <span className={styles.loader}></span>
: null
}
</div>
);
};

View File

@@ -0,0 +1,90 @@
@import "@scss_mixins";
.container {
display: flex;
flex-direction: column;
justify-content: center;
gap: 0.1rem;
}
.switch_container {
position: relative;
display: flex;
justify-content: space-between;
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;
justify-content: center;
}
&.is_pending {
pointer-events: none;
}
}
.label_wrapper {
display: flex;
justify-content: left;
align-items: center;
gap: 0.8rem;
}
$pending_label_color: var(--dark_500_color);
.switch_label {
font-size: 1.4rem;
&.is_compact_mode {
display: none;
}
&.is_pending {
color: $pending_label_color;
}
}
.switch_svg {
width: 1.8rem;
&.is_pending {
color: $pending_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;
@include toggle_control_styles($toggle_width: 3.6rem, $toggle_height: 1.4rem);
display: flex;
justify-content: end;
align-items: center;
&.is_compact_mode {
display: none;
}
}

View File

@@ -0,0 +1,19 @@
import styles from "./OpenSettings.module.scss";
import { useIsOpenedConfigPage } from "@logics_common";
import ConfigurationSvg from "@images/configuration.svg?react";
export const OpenSettings = () => {
const { setIsOpenedConfigPage } = useIsOpenedConfigPage();
const openConfigPage = () => {
setIsOpenedConfigPage(true);
};
return (
<div className={styles.container}>
<div className={styles.open_config_page_button} onClick={openConfigPage}>
<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_page_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);
}