[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,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);
}
}