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

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

No files matched your search

+21
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>
);
};
@@ -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;
}
@@ -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;
}
};
@@ -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%;
}
@@ -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;
})}
</>
);
};
@@ -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>
);
};
@@ -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;
}
@@ -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>
);
};
@@ -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);
}
@@ -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>
));
@@ -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%;
}
@@ -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;
};
@@ -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.
}
@@ -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>
))}
</>
);
};
@@ -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);
}
}
@@ -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>;
};
@@ -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;
}
@@ -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>
);
};
@@ -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);
}
@@ -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>
);
};
@@ -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;
}
@@ -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>
);
};
@@ -0,0 +1,7 @@
.container {
height: var(--main_page_topbar_height);
display: flex;
justify-content: space-between;
align-items: center;
margin-right: 1.6rem;
}
@@ -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>
);
};
@@ -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;
}
@@ -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>
);
};
@@ -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);
}
}
@@ -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>
);
};
@@ -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);
}
}
@@ -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>
);
};
@@ -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);
}
}
}
@@ -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>
);
};
@@ -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;
}
@@ -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>
);
};
@@ -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);
}
@@ -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>
);
};
@@ -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);
}
}
@@ -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>
);
};
@@ -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);
}
}
@@ -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>
);
};
@@ -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;
}
@@ -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>
);
};
@@ -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);
}
@@ -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>
);
};
@@ -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;
}
@@ -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" />;
}
};
@@ -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;
}
@@ -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>
);
};
@@ -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;
}
}
@@ -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>
);
};
@@ -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);
}