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

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

View File

@@ -0,0 +1,21 @@
import styles from "./ConfigPage.module.scss";
import { Topbar } from "./topbar/Topbar.jsx";
import { SidebarSection } from "./sidebar_section/SidebarSection.jsx";
import { SettingSection } from "./setting_section/SettingSection.jsx";
import { VersionLabel } from "./version_label/VersionLabel.jsx";
export const ConfigPage = () => {
return (
<div className={styles.page}>
<div className={styles.container}>
<Topbar />
<div className={styles.main_container}>
<SidebarSection />
<SettingSection />
</div>
<VersionLabel />
</div>
</div>
);
};

View File

@@ -0,0 +1,26 @@
.page {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
overflow: hidden;
}
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: var(--dark_900_color);
overflow: hidden;
position: relative;
}
.main_container {
width: 100%;
height: 100%;
display: flex;
padding-top: var(--config_page_topbar_height);
}

View File

@@ -0,0 +1,28 @@
import { useRef, useLayoutEffect, useEffect } from "react";
import styles from "./SettingSection.module.scss";
import { SettingBox } from "./setting_box/SettingBox";
import { store, useStore_SelectedConfigTabId } from "@store";
import { useSettingBoxScrollPosition } from "@logics_configs";
export const SettingSection = () => {
const { currentSelectedConfigTabId } = useStore_SelectedConfigTabId();
const { resetScrollPosition } = useSettingBoxScrollPosition();
const scrollContainerRef = useRef(null);
useLayoutEffect(() => {
store.setting_box_scroll_container = scrollContainerRef;
}, []);
useEffect(() => {
resetScrollPosition();
}, [currentSelectedConfigTabId.data]);
return (
<div ref={scrollContainerRef} className={styles.scroll_container}>
<div className={styles.container}>
<SettingBox />
</div>
</div>
);
};

View File

@@ -0,0 +1,9 @@
.scroll_container {
width: 100%;
overflow-y: scroll;
overflow-x: hidden;
}
.container {
margin: 0rem 4rem 16rem 0.6rem;
}

View File

@@ -0,0 +1,46 @@
import { useStore_SelectedConfigTabId } from "@store";
import {
Device,
Appearance,
Translation,
Transcription,
Others,
AdvancedSettings,
Vr,
Hotkeys,
Plugins,
Supporters,
AboutVrct,
} from "@setting_box";
export const SettingBox = () => {
const { currentSelectedConfigTabId } = useStore_SelectedConfigTabId();
switch (currentSelectedConfigTabId.data) {
case "device":
return <Device />;
case "appearance":
return <Appearance />;
case "translation":
return <Translation />;
case "transcription":
return <Transcription />;
case "others":
return <Others />;
case "vr":
return <Vr />;
case "hotkeys":
return <Hotkeys />;
case "advanced_settings":
return <AdvancedSettings />;
case "plugins":
return <Plugins />;
case "supporters":
return <Supporters />;
case "about_vrct":
return <AboutVrct />;
default:
return null;
}
};

View File

@@ -0,0 +1,50 @@
import { useI18n } from "@useI18n";
import CircularProgress from "@mui/material/CircularProgress";
import styles from "./_DownloadButton.module.scss";
export const _DownloadButton = ({option, ...props}) => {
const { t } = useI18n();
const renderContent = () => {
const circular_progress = Math.floor(option.progress / 10) * 10;
switch (true) {
case option.progress !== null:
return (
<>
<CircularProgress
variant={(option.progress === 100) ? "indeterminate" : "determinate"}
value={circular_progress}
size="3rem"
sx={{ color: "var(--primary_300_color)" }}
/>
<p className={styles.progress_label}>{`${Math.round(option.progress)}%`}</p>
</>
);
case option.is_pending:
return <CircularProgress size="3rem" sx={{ color: "var(--dark_600_color)" }}/>;
case !option.is_downloaded:
return (
<button
className={styles.download_button}
onClick={() => props.downloadStartFunction(option.id)}
>
<p className={styles.download_button_label}>{t("config_page.common.model_download_button_label")}</p>
</button>
);
case option.update_button:
return (
<button
className={styles.update_button}
onClick={() => props.downloadStartFunction(option.id)}
>
<p className={styles.download_button_label}>Update</p>
</button>
);
default:
return null;
}
};
return <div className={styles.download_container}>{renderContent()}</div>;
};

View File

@@ -0,0 +1,45 @@
@import "@scss_mixins";
.download_container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
max-width: 8rem;
}
.download_button {
pointer-events: auto;
background-color: var(--dark_800_color);
padding: 0.8rem;
flex-shrink: 0;
border-radius: 0.2rem;
&:hover {
background-color: var(--dark_750_color);
}
&:active {
background-color: var(--dark_800_color);
}
}
.download_button_label {
font-size: 1.2rem;
}
.progress_label {
position: absolute;
font-size: 1rem;
}
.update_button {
pointer-events: auto;
background-color: var(--primary_400_color);
padding: 0.8rem;
flex-shrink: 0;
border-radius: 0.2rem;
&:hover {
background-color: var(--primary_450_color);
}
&:active {
background-color: var(--primary_500_color);
}
}

View File

@@ -0,0 +1,49 @@
import clsx from "clsx";
import React, { useRef, forwardRef, useImperativeHandle } from "react";
import styles from "./_Entry.module.scss";
const _Entry = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
},
blur: () => {
inputRef.current.blur();
}
}));
const input_class_names = clsx(styles.entry_input_area, {
[styles.is_disabled]: props.is_disabled,
});
const input_wrapper_class_names = clsx(styles.entry_wrapper, {
[styles.is_activated]: props.is_activated,
});
return (
<div
className={styles.entry_container}
style={{width: props.width || "100%" }}
>
<div className={input_wrapper_class_names}>
<input
ref={inputRef}
text={props.text ? props.text : "text"}
placeholder={props.placeholder ? props.placeholder : ""}
className={input_class_names}
value={props.ui_variable === null ? "" : props.ui_variable}
onChange={(e) => props.onChange?.(e)}
onFocus={(e) => props.onFocus?.(e)}
onBlur={(e) => props.onBlur?.(e)}
onKeyDown={(e) => props.onKeyDown?.(e)}
onKeyUp={(e) => props.onKeyUp?.(e)}
readOnly={props.readOnly === true ? true : false}
/>
</div>
</div>
);
});
_Entry.displayName = "_Entry";
export { _Entry };

View File

@@ -0,0 +1,26 @@
.entry_container {
width: 100%;
}
.entry_wrapper {
height: 100%;
padding: 0.6rem;
background-color: var(--dark_875_color);
border: 0.1rem solid var(--dark_750_color);
border-radius: 0.4rem;
&.is_activated {
border: 0.1rem solid var(--primary_400_color);
}
}
.entry_input_area {
width: 100%;
height: 100%;
font-size: 1.4rem;
resize: none;
font-family: Arial, sans-serif;
&.is_disabled {
color: var(--dark_500_color);
pointer-events: none;
}
}

View File

@@ -0,0 +1,11 @@
import styles from "./ActionButton.module.scss";
export const ActionButton = ({IconComponent, onclickFunction}) => {
return (
<div className={styles.container}>
<button className={styles.button_wrapper} onClick={onclickFunction}>
<IconComponent className={styles.button_svg}/>
</button>
</div>
);
};

View File

@@ -0,0 +1,15 @@
.button_wrapper {
padding: 1.6rem;
border-radius: 0.4rem;
&:hover {
background-color: var(--dark_825_color);
}
&:active {
background-color: var(--dark_900_color);
}
}
.button_svg {
width: 2.4rem;
color: var(--dark_400_color);
}

View File

@@ -0,0 +1,27 @@
import styles from "./ComputeDevice.module.scss";
import { DropdownMenu } from "../dropdown_menu/DropdownMenu";
import { ActionButton } from "../action_button/ActionButton";
import HelpSvg from "@images/help.svg?react";
import { useStore_OpenedQuickSetting } from "@store"
export const ComputeDevice = (props) => {
const { updateOpenedQuickSetting } = useStore_OpenedQuickSetting();
const onClickFunction = () => {
updateOpenedQuickSetting("update_software");
};
return (
<div className={styles.container}>
<DropdownMenu
{...props}
is_disabled={true}
/>
<ActionButton
{...props}
IconComponent={HelpSvg}
onclickFunction={onClickFunction}
/>
</div>
);
};

View File

@@ -0,0 +1,6 @@
.container {
display: flex;
justify-content: center;
align-items: center;
gap: 2rem;
}

View File

@@ -0,0 +1,72 @@
import styles from "./DeeplAuthKey.module.scss";
import { useI18n } from "@useI18n";
import clsx from "clsx";
import CircularProgress from "@mui/material/CircularProgress";
import ExternalLink from "@images/external_link.svg?react";
import { _Entry } from "../_atoms/_entry/_Entry";
import { useState, useRef } from "react";
import { useEffect } from "react";
export const DeeplAuthKey = (props) => {
const { t } = useI18n();
const [is_editable, seIsEditable] = useState(false);
const entryRef = useRef(null);
const revealEditAuthKey = () => {
seIsEditable(true);
entryRef.current.focus();
};
const onchangeEntryAuthKey = (e) => {
props.onChangeFunction(e.target.value);
};
const saveAuthKey = () => {
props.saveFunction();
};
useEffect(() => {
if (props.variable === "" || props.variable === null) {
seIsEditable(true);
}
}, [props.variable]);
const is_disabled = props.state === "pending";
const save_button_class_names = clsx(styles.save_button, {
[styles.is_disabled]: is_disabled
});
return (
<div className={styles.container}>
<div className={styles.entry_section_wrapper}>
<_Entry ref={entryRef} width="30rem" onChange={onchangeEntryAuthKey} ui_variable={props.variable} is_disabled={is_disabled}/>
<button className={save_button_class_names} onClick={saveAuthKey}>
{is_disabled
? <CircularProgress size="1.4rem" sx={{ color: "var(--dark_basic_text_color)" }}/>
: <p className={styles.save_button_label}>{t("config_page.translation.deepl_auth_key.save")}</p>
}
</button>
{is_editable
? null
:
<div className={styles.entry_edit_cover} onClick={revealEditAuthKey}>
<button className={styles.edit_button}>{t("config_page.translation.deepl_auth_key.edit")}</button>
</div>
}
</div>
</div>
);
};
export const OpenWebpage_DeeplAuthKey = () => {
const { t } = useI18n();
return (
<div className={styles.open_webpage_button_wrapper}>
<a className={styles.open_webpage_button} href="https://www.deepl.com/ja/your-account/keys" target="_blank" rel="noreferrer" >
<p className={styles.open_webpage_text}>{t("config_page.translation.deepl_auth_key.open_auth_key_webpage")}</p>
<ExternalLink className={styles.external_link_svg} />
</a>
</div>
);
};

View File

@@ -0,0 +1,98 @@
.container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1rem;
flex-shrink: 0;
}
.entry_section_wrapper {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
position: relative;
}
.entry_edit_cover {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
border-radius: 0.4rem;
background-color: var(--dark_1000_color_66);
backdrop-filter: blur(4rem);
border: solid 0.1rem var(--dark_700_color);
&:hover {
background-color: var(--dark_1000_color_aa);
}
&:active {
backdrop-filter: blur(1.4rem);
}
}
.edit_button {
padding: 0.8rem 1.2rem;
height: 100%;
width: 100%;
font-size: 1.4rem;
text-align: center;
}
.save_button {
padding: 0.8rem 1.2rem;
background-color: var(--primary_600_color);
border-radius: 0.4rem;
text-align: center;
flex-shrink: 0;
min-width: 5.4rem;
&:hover {
background-color: var(--primary_500_color);
}
&:active {
background-color: var(--primary_700_color);
}
&.is_disabled {
pointer-events: none;
background-color: var(--primary_800_color);
}
}
.save_button_label {
font-size: 1.4rem;
}
.open_webpage_button_wrapper {
display: flex;
justify-content: center;
align-items: center;
}
.open_webpage_button {
padding: 0.6rem 2.8rem;
display: flex;
gap: 1rem;
justify-content: center;
align-items: center;
border-radius: 0.4rem;
cursor: pointer;
flex-shrink: 0;
&:hover {
background-color: var(--dark_825_color);
}
&:active {
background-color: var(--dark_900_color);
}
}
.open_webpage_text {
font-size: 1.2rem;
}
.external_link_svg {
color: var(--dark_500_color);
width: 1.6rem;
flex-shrink: 0;
}

View File

@@ -0,0 +1,25 @@
import {
RadioButton,
} from "../index";
import { _DownloadButton } from "../_atoms/_download_button/_DownloadButton";
export const DownloadModels = (props) => {
const options = props.options.map(item => ({
...item,
disabled: !item.is_downloaded
}));
return (
<>
<RadioButton
selectFunction={props.selectFunction}
name={props.name}
options={options}
checked_variable={props.checked_variable}
column={true}
ChildComponent={_DownloadButton}
downloadStartFunction={props.downloadStartFunction}
/>
</>
);
};

View File

@@ -0,0 +1,76 @@
import styles from "./DropdownMenu.module.scss";
import clsx from "clsx";
import ArrowLeftSvg from "@images/arrow_left.svg?react";
import { useStore_IsOpenedDropdownMenu } from "@store";
export const DropdownMenu = (props) => {
const { updateIsOpenedDropdownMenu, currentIsOpenedDropdownMenu } = useStore_IsOpenedDropdownMenu();
const toggleDropdownMenu = () => {
if (currentIsOpenedDropdownMenu.data === props.dropdown_id) {
updateIsOpenedDropdownMenu("");
} else {
if (props.openListFunction !== undefined) props.openListFunction();
updateIsOpenedDropdownMenu(props.dropdown_id);
}
};
const selectValue = (key) => {
updateIsOpenedDropdownMenu("");
props.selectFunction({
dropdown_id: props.dropdown_id,
selected_id: key,
});
};
const dropdown_content_wrapper_class_name = clsx(styles["dropdown_content_wrapper"], {
[styles.is_opened]: (currentIsOpenedDropdownMenu.data === props.dropdown_id) ? true : false,
[styles.is_disabled]: props.is_disabled,
});
const dropdown_toggle_button_class_name = clsx(styles["dropdown_toggle_button"], {
[styles.is_pending]: (props.state === "pending") ? true : false,
[styles.is_disabled]: props.is_disabled,
});
const arrow_class_names = clsx(styles["arrow_left_svg"], {
[styles.is_opened]: (currentIsOpenedDropdownMenu.data === props.dropdown_id) ? true : false
});
const getSelectedText = () => {
if (props.state !== "ok") return;
if (props.list[props.selected_id] === undefined) return props.selected_id; // [Fix me]
return props.list[props.selected_id];
};
const list = (props.list === undefined) ? {} : props.list;
return (
<div className={styles.container}>
<div className={dropdown_toggle_button_class_name} onClick={toggleDropdownMenu} style={props.style}>
{(props.state === "pending")
? <p className={styles.dropdown_selected_text}>Loading...</p>
: <p className={styles.dropdown_selected_text}>{getSelectedText()}</p>
}
{(props.state === "pending")
? <span className={styles.loader}></span>
: <ArrowLeftSvg className={arrow_class_names} />
}
</div>
<div className={dropdown_content_wrapper_class_name}>
<div className={styles.dropdown_content}>
{(props.state === "ok")
? Object.entries(list).map(([key, value]) => {
return (
<div key={key} className={styles.value_button} onClick={() => selectValue(key)}>
<p className={styles.value_text}>{value}</p>
</div>
);
})
: null
}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,97 @@
@import "@scss_mixins";
.container {
position: relative;
}
.dropdown_toggle_button {
position: relative;
background-color: var(--dark_950_color);
min-width: 20rem;
padding: 0.8rem 1.4rem;
cursor: pointer;
border-radius: 0.4rem;
&:hover {
background-color: var(--dark_925_color);
}
&:active {
background-color: var(--dark_975_color);
}
&.is_pending {
pointer-events: none;
}
&.is_disabled {
pointer-events: none;
.dropdown_selected_text, .arrow_left_svg {
color: var(--dark_550_color);
}
}
}
.dropdown_selected_text {
font-size: 1.4rem;
padding-right: 2.8rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dropdown_content_wrapper {
display: none;
position: absolute;
top: 100%; // Position it below the toggle button
right: 0;
min-width: 20rem;
z-index: 1;
&.is_opened {
display: block;
}
&.is_disabled {
pointer-events: none;
.value_text {
color: var(--dark_550_color);
}
}
}
.dropdown_content {
background-color: var(--dark_900_color);
border: 0.1rem solid var(--dark_600_color);
display: flex;
flex-direction: column;
gap: 0.1rem;
white-space: nowrap;
max-height: 20rem;
overflow-y: scroll;
}
.value_button {
background-color: var(--dark_875_color);
padding: 1.2rem;
cursor: pointer;
&:hover {
background-color: var(--dark_800_color);
}
&:active {
background-color: var(--dark_900_color);
}
}
.value_text {
font-size: 1.4rem;
}
.loader {
@include loader(2rem, 0.2rem, right, 0);
}
.arrow_left_svg {
position: absolute;
top: 50%;
right: 0;
transform: translate(-50%, -50%) rotate(-90deg);
width: 1.4rem;
&.is_opened {
transform: translate(-50%, -50%) rotate(90deg);
}
}

View File

@@ -0,0 +1,10 @@
import styles from "./Entry.module.scss";
import { _Entry } from "../_atoms/_entry/_Entry";
export const Entry = (props) => {
return (
<div className={styles.entry_container}>
<_Entry {...props} />
</div>
);
};

View File

@@ -0,0 +1,32 @@
import styles from "./EntryWithSaveButton.module.scss";
import { _Entry } from "../_atoms/_entry/_Entry";
import CircularProgress from "@mui/material/CircularProgress";
import { useI18n } from "@useI18n";
import { clsx } from "clsx";
export const EntryWithSaveButton = (props) => {
const { t } = useI18n();
const onChangeFunction = (e) => {
props.onChangeFunction?.(e.target.value);
};
const saveFunction = () => {
props.saveFunction();
};
const is_disabled = props.state === "pending";
const save_button_class_names = clsx(styles.save_button, {
[styles.is_disabled]: is_disabled
});
return (
<div className={styles.container}>
<_Entry width={props.width} onChange={onChangeFunction} ui_variable={props.variable} is_disabled={is_disabled}/>
<button className={save_button_class_names} onClick={saveFunction}>
{is_disabled
? <CircularProgress size="1.4rem" sx={{ color: "var(--dark_basic_text_color)" }}/>
: <p className={styles.save_button_label}>{t("config_page.translation.deepl_auth_key.save")}</p>
}
</button>
</div>
);
};

View File

@@ -0,0 +1,30 @@
.container {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
flex-shrink: 0;
}
.save_button {
padding: 0.8rem 1.2rem;
background-color: var(--primary_600_color);
border-radius: 0.4rem;
text-align: center;
flex-shrink: 0;
min-width: 5.4rem;
&:hover {
background-color: var(--primary_500_color);
}
&:active {
background-color: var(--primary_700_color);
}
&.is_disabled {
pointer-events: none;
background-color: var(--primary_800_color);
}
}
.save_button_label {
font-size: 1.4rem;
}

View File

@@ -0,0 +1,122 @@
import styles from "./HotkeysEntry.module.scss";
import { _Entry } from "../_atoms/_entry/_Entry";
import { useState, useRef, useEffect } from "react";
import DeleteSvg from "@images/cancel.svg?react";
import clsx from "clsx";
export const HotkeysEntry = (props) => {
const [isAcceptingInput, setIsAcceptingInput] = useState(false);
const [displayValue, setDisplayValue] = useState("");
const lastKeyRef = useRef(null);
const isModifierOnlyRef = useRef(false);
const entryRef = useRef(null);
const pressedKeys = useRef(new Set());
const keysRef = useRef([]);
useEffect(() => {
const init_display_value = props.value[props.hotkey_id] ? props.value[props.hotkey_id].join(" + ") : "";
setDisplayValue(init_display_value);
}, []);
const updateHotkeys = (keys) => {
entryRef.current.blur();
const result = props.setHotkeys({ [props.hotkey_id]: keys });
if (result === false) setDisplayValue("");
};
const processKey = (key) => {
if (/^[a-zA-Z]$/.test(key)) return key.toUpperCase();
if (key === "Meta") return "Super";
return key;
};
const handleKeyInput = (event) => {
const keys = [];
const nonModifierKeys = [];
["Ctrl", "Shift", "Alt", "Meta"].forEach((modKey) => {
if (event[`${modKey.toLowerCase()}Key`] && !keys.includes(modKey)) {
let register_mod_key = (modKey === "Meta") ? "Super" : modKey;
keys.push(register_mod_key);
}
});
const key = processKey(event.key);
if (!["Control", "Shift", "Alt", "Meta"].includes(event.key)) {
keys.push(key);
nonModifierKeys.push(key);
}
if (!pressedKeys.current.has(key)) {
pressedKeys.current.add(key);
}
keysRef.current = keys;
setDisplayValue(keys.join(" + "));
isModifierOnlyRef.current = nonModifierKeys.length === 0;
};
const handleKeyDown = (event) => {
event.preventDefault();
if (lastKeyRef.current === event.key) return;
lastKeyRef.current = event.key;
handleKeyInput(event);
};
const handleKeyUp = (event) => {
lastKeyRef.current = null;
const key = processKey(event.key);
pressedKeys.current.delete(key);
if (isModifierOnlyRef.current) {
setDisplayValue("");
}
if (pressedKeys.current.size === 0) {
const hasNonModifierKeys = keysRef.current.some(
(key) => !["Ctrl", "Shift", "Alt", "Super"].includes(key)
);
if (hasNonModifierKeys) {
updateHotkeys(keysRef.current);
} else {
const display_value = props.value[props.hotkey_id] ? props.value[props.hotkey_id].join(" + ") : "";
setDisplayValue(display_value);
}
}
};
const handleBlur = () => {
setIsAcceptingInput(false);
pressedKeys.current.clear();
};
const handleDelete = () => {
updateHotkeys(null);
setDisplayValue("");
};
const is_pending = props.state === "pending";
return (
<div className={styles.container}>
{is_pending && <span className={styles.loader}></span>}
<_Entry
ref={entryRef}
type="text"
onFocus={() => setIsAcceptingInput(true)}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
ui_variable={displayValue}
width="20rem"
is_activated={isAcceptingInput}
is_disabled={is_pending}
readOnly
/>
<button className={clsx(styles.delete_button, { [styles.is_pending]: is_pending })} onClick={handleDelete}>
<DeleteSvg className={styles.delete_svg}/>
</button>
</div>
);
};

View File

@@ -0,0 +1,41 @@
@import "@scss_mixins";
.container {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
position: relative;
}
.delete_button {
padding: 0.4rem;
font-size: 1.4rem;
// background-color: var(--dark_800_color);
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
border-radius: 0.2rem;
&:hover {
background-color: var(--dark_850_color);
}
&:active {
background-color: var(--dark_900_color);
}
&.is_pending {
pointer-events: none;
& .delete_svg {
color: var(--dark_600_color);
}
}
}
.delete_svg {
width: 2.2rem;
color: var(--error_bc_color);
}
.loader {
@include loader(2rem, 0.2rem, left, -2.2rem);
}

View File

@@ -0,0 +1,16 @@
export { ActionButton } from "./action_button/ActionButton";
export { ComputeDevice } from "./compute_device/ComputeDevice";
export { DeeplAuthKey, OpenWebpage_DeeplAuthKey } from "./deepl_auth_key/DeeplAuthKey";
export { DropdownMenu } from "./dropdown_menu/DropdownMenu";
export { Entry } from "./entry/Entry";
export { EntryWithSaveButton } from "./entry_with_save_button/EntryWithSaveButton";
export { HotkeysEntry } from "./hotkeys_entry/HotkeysEntry";
export { LabelComponent } from "./label_component/LabelComponent";
export { RadioButton } from "./radio_button/RadioButton";
export { SectionLabelComponent } from "./section_label_component/SectionLabelComponent";
export { Slider } from "./slider/Slider";
export { SwitchBox } from "./switch_box/SwitchBox";
export { ThresholdComponent } from "./threshold_component/ThresholdComponent";
export { WordFilter, WordFilterListToggleComponent } from "./word_filter/WordFilter";
export { DownloadModels } from "./download_models/DownloadModels";
export { MessageFormat } from "./message_format/MessageFormat";

View File

@@ -0,0 +1,13 @@
import styles from "./LabelComponent.module.scss";
export const LabelComponent = (props) => {
return (
<div className={styles.label_component}>
<p className={styles.label}>{props.label}</p>
{props.desc
? <p className={styles.desc}>{props.desc}</p>
: null
}
</div>
);
};

View File

@@ -0,0 +1,21 @@
.label_component {
display: flex;
flex-direction: column;
gap: 0.4rem;
// flex-shrink: 0;
}
.label {
font-size: 1.6rem;
font-weight: 400;
white-space: nowrap;
width: max-content;
}
.desc {
font-size: 1.4rem;
font-weight: 300;
color: var(--dark_500_color);
max-width: 38rem;
overflow-wrap: break-word;
}

View File

@@ -0,0 +1,309 @@
import styles from "./MessageFormat.module.scss";
import { useTranslation } from "react-i18next";
import { _Entry } from "../_atoms/_entry/_Entry";
import SwapImg from "@images/swap_icon.png";
import ArrowLeftSvg from "@images/arrow_left.svg?react";
import {
useStore_IsBreakPoint,
useStore_MessageFormat_ExampleViewFilter,
} from "@store";
import { useAppearance } from "@logics_configs";
import { ui_configs } from "@ui_configs";
import { ResetButton } from "@common_components";
const ENTRY_WIDTH = "8rem";
const EXAMPLE_TEXTS = {
en: "Hello",
ja: "こんにちは",
ko: "안녕하세요",
fr: "Bonjour",
};
export const MessageFormat = (props) => {
const { currentIsBreakPoint } = useStore_IsBreakPoint();
const message_format_container_class = clsx(styles.container, {
[styles.is_break_point]: currentIsBreakPoint.data,
});
return (
<div className={message_format_container_class}>
<ExampleComponent
format={props.variable.data}
format_id={props.format_id}
/>
<div className={styles.border}></div>
<InputComponent
variable={props.variable.data}
setFunction={props.setFunction}
format_id={props.format_id}
/>
</div>
);
};
const ExampleComponent = ({ format, format_id }) => {
const { currentUiLanguage } = useAppearance();
const { t } = useTranslation();
const {
currentMessageFormat_ExampleViewFilter,
updateMessageFormat_ExampleViewFilter,
} = useStore_MessageFormat_ExampleViewFilter();
const locale_base_path = "config_page.others.message_format_common.example_view.";
const label_title = t(locale_base_path + "title");
const label_original_translated = t(locale_base_path + "original_translated");
const label_original_translated_multi = t(locale_base_path + "original_translated_multi");
const label_translated_only_multi = t(locale_base_path + "translated_only_multi");
const label_translated_only = t(locale_base_path + "translated_only");
const label_original_only = t(locale_base_path + "original_only");
const createExampleMessage = (id) => {
// 言語順序を決定
let example_text_order = [];
switch (currentUiLanguage.data) {
case "ja":
example_text_order = ["ja", "en", "ko", "fr"];
break;
case "ko":
example_text_order = ["ko", "ja", "en", "fr"];
break;
default: // en
example_text_order = ["en", "ja", "ko", "fr"];
break;
}
const original = EXAMPLE_TEXTS[example_text_order[0]];
const translations = example_text_order.slice(1).map(lang => EXAMPLE_TEXTS[lang]);
const originalPart = `${format.message.prefix}${original}${format.message.suffix}`;
const translationSingle = `${format.translation.prefix}${translations[0]}${format.translation.suffix}`;
const translationMulti = `${format.translation.prefix}${translations.join(format.translation.separator)}${format.translation.suffix}`;
switch (id) {
case "original_translated":
return format.translation_first
? `${translationSingle}${format.separator}${originalPart}`
: `${originalPart}${format.separator}${translationSingle}`;
case "original_only":
return originalPart;
case "translated_only":
return translationSingle;
case "translated_only_multi":
return translationMulti;
case "original_translated_multi":
return format.translation_first
? `${translationMulti}${format.separator}${originalPart}`
: `${originalPart}${format.separator}${translationMulti}`;
default:
throw new Error(`Unexpected id: ${id}`);
}
};
const ExampleBox = ({label, example_text_id}) => {
return (
<div className={styles.example_wrapper}>
<p className={styles.example_label}>{label}</p>
<div className={styles.example_chatbox}>
<p className={styles.example_text}>{createExampleMessage(example_text_id)}</p>
</div>
</div>
);
};
const svg_class_names = clsx(styles.arrow_left_svg, {
[styles.to_down]: currentMessageFormat_ExampleViewFilter.data[format_id] === "Simplified",
[styles.to_up]: currentMessageFormat_ExampleViewFilter.data[format_id] === "All"
});
const FilteredExampleBox = ({format_id, id}) => {
if (format_id === "send" && id === "Simplified") {
return (
<>
<ExampleBox label={label_original_translated} example_text_id="original_translated" />
<ExampleBox label={label_original_translated_multi} example_text_id="original_translated_multi" />
</>
);
} else if ( format_id === "send" && id === "All") {
return (
<>
<ExampleBox label={label_original_translated} example_text_id="original_translated" />
<ExampleBox label={label_original_translated_multi} example_text_id="original_translated_multi" />
<ExampleBox label={label_translated_only_multi} example_text_id="translated_only_multi" />
<ExampleBox label={label_translated_only} example_text_id="translated_only" />
<ExampleBox label={label_original_only} example_text_id="original_only" />
</>
);
} else if (format_id === "received") {
return (
<>
<ExampleBox label={label_original_translated} example_text_id="original_translated" />
<ExampleBox label={label_original_only} example_text_id="original_only" />
<ExampleBox label={label_translated_only} example_text_id="translated_only" />
</>
);
}
};
const exampleViewFilterToggleFunction = (format_id) => {
if (["send", "received"].includes(format_id) === false) return console.error(`format_id should be small case 'send' or 'received'. got format_id: ${format_id}`);
updateMessageFormat_ExampleViewFilter({
...currentMessageFormat_ExampleViewFilter.data,
[format_id]: currentMessageFormat_ExampleViewFilter.data[format_id] === "Simplified"
? "All"
: "Simplified"
});
};
return (
<div className={styles.example_container}>
<p className={styles.section_title}>{label_title}</p>
<div className={styles.example_view_container}>
<FilteredExampleBox format_id={format_id} id={currentMessageFormat_ExampleViewFilter.data[format_id]} />
</div>
{ format_id === "send" &&
<div className={styles.show_more_container} onClick={() => exampleViewFilterToggleFunction(format_id)}>
<ArrowLeftSvg className={svg_class_names}/>
</div>
}
</div>
);
};
const InputComponent = ({id, variable, setFunction, format_id }) => {
const { t } = useTranslation();
const locale_base_path = "config_page.others.message_format_common.settings.";
const label_title = t(locale_base_path + "title");
const LABEL_ORIGINAL = t(locale_base_path + "original");
const LABEL_TRANSLATED = t(locale_base_path + "translated");
const LABEL_FOR_MULTI_TRANSLATION = t(locale_base_path + "for_multi_translation");
const replaceValue = (value) => {
if (value === "") return "";
const replaced = value.replace(/\\n/g, "\n");
return replaced;
};
const handleChange = (parent_key, child_key) => (e) => {
const rawValue = e.target.value;
const parsedValue = replaceValue(rawValue);
if (child_key !== undefined) {
setFunction({
...variable,
[parent_key]: {
...variable[parent_key],
[child_key]: parsedValue
}
});
} else {
setFunction({
...variable,
[parent_key]: parsedValue
});
}
};
const toUiValue = (v) => {
if (typeof v === "string") {
return v.replace(/\n/g, "\\n");
}
console.log("Empty");
return v ?? "";
};
const resetFunction = () => {
if (format_id === "send") {
setFunction(ui_configs.send_message_format_parts);
} else if (format_id === "received") {
setFunction(ui_configs.received_message_format_parts);
}
};
const SwapButton = ({ variable, setFunction }) => {
const swapMessageAndTranslate = () => {
setFunction({ ...variable, translation_first: !variable.translation_first });
};
return (
<div className={styles.swap_button_wrapper} onClick={swapMessageAndTranslate}>
<p className={styles.swap_text}>{variable.translation_first ? LABEL_TRANSLATED : LABEL_ORIGINAL}</p>
<img className={styles.swap_img} src={SwapImg} alt="Swap Icon" />
<p className={styles.swap_text}>{variable.translation_first ? LABEL_ORIGINAL : LABEL_TRANSLATED}</p>
</div>
);
};
return (
<div className={styles.message_format_settings_container}>
<p className={styles.section_title}>{label_title}</p>
<div className={styles.message_format_settings_wrapper}>
<div className={styles.swap_button_container}>
<SwapButton variable={variable} setFunction={setFunction} />
</div>
{ !variable.translation_first ?
<div className={styles.input_wrapper}>
<div className={styles.input_contents}>
<_Entry ui_variable={toUiValue(variable.message.prefix)} width={ENTRY_WIDTH} onChange={handleChange("message", "prefix")} />
<p className={styles.preset_text}>{LABEL_ORIGINAL}</p>
<_Entry ui_variable={toUiValue(variable.message.suffix)} width={ENTRY_WIDTH} onChange={handleChange("message", "suffix")} />
</div>
<div className={styles.input_contents}>
<_Entry ui_variable={toUiValue(variable.separator)} width={ENTRY_WIDTH} onChange={handleChange("separator")} />
</div>
<div className={styles.input_contents}>
<_Entry ui_variable={toUiValue(variable.translation.prefix)} width={ENTRY_WIDTH} onChange={handleChange("translation", "prefix")} />
<p className={styles.preset_text}>{LABEL_TRANSLATED}</p>
<_Entry ui_variable={toUiValue(variable.translation.suffix)} width={ENTRY_WIDTH} onChange={handleChange("translation", "suffix")} />
</div>
</div>
:
<div className={styles.input_wrapper}>
<div className={styles.input_contents}>
<_Entry ui_variable={toUiValue(variable.translation.prefix)} width={ENTRY_WIDTH} onChange={handleChange("translation", "prefix")} />
<p className={styles.preset_text}>{LABEL_TRANSLATED}</p>
<_Entry ui_variable={toUiValue(variable.translation.suffix)} width={ENTRY_WIDTH} onChange={handleChange("translation", "suffix")} />
</div>
<div className={styles.input_contents}>
<_Entry ui_variable={toUiValue(variable.separator)} width={ENTRY_WIDTH} onChange={handleChange("separator")} />
</div>
<div className={styles.input_contents}>
<_Entry ui_variable={toUiValue(variable.message.prefix)} width={ENTRY_WIDTH} onChange={handleChange("message", "prefix")} />
<p className={styles.preset_text}>{LABEL_ORIGINAL}</p>
<_Entry ui_variable={toUiValue(variable.message.suffix)} width={ENTRY_WIDTH} onChange={handleChange("message", "suffix")} />
</div>
</div>
}
{ format_id === "send" &&
<div className={styles.multi_translation_input_wrapper}>
<p className={styles.multi_translation_title}>{LABEL_FOR_MULTI_TRANSLATION}</p>
<div className={styles.input_contents}>
<p className={styles.preset_text}>{LABEL_TRANSLATED}</p>
<_Entry ui_variable={toUiValue(variable.translation.separator)} width={ENTRY_WIDTH} onChange={handleChange("translation", "separator")} />
<p className={styles.preset_text}>{LABEL_TRANSLATED}</p>
</div>
</div>
}
<div className={styles.reset_button_wrapper}>
<ResetButton onClickFunction={resetFunction}/>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,200 @@
.container {
display: flex;
justify-content: center;
width: 100%;
gap: 2.6rem;
padding-bottom: 2rem;
margin: 1rem 0;
&.is_break_point {
flex-direction: column;
gap: 2.2rem;
align-items: center;
.border {
height: 0.1rem;
width: 60%;
margin: 0;
flex-shrink: 0;
}
.show_more_container {
margin-top: 2rem;
}
.example_container {
gap: 0;
}
.message_format_settings_container {
gap: 0;
}
}
}
.border {
height: auto;
width: 0.1rem;
background-color: var(--dark_800_color);
margin: 3.2rem 0;
flex-shrink: 0;
}
.section_title {
font-size: 1.4rem;
text-align: center;
width: 100%;
}
.example_container {
display: flex;
flex-direction: column;
gap: 2rem;
min-width: 14rem;
max-width: 34rem;
width: 100%;
}
.example_view_container {
display: flex;
flex-direction: column;
gap: 1.2rem;
}
.example_wrapper {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.4rem;
}
.example_label {
font-size: 1.4rem;
text-align: start;
width: 100%;
color: var(--dark_basic_text_color);
}
.example_chatbox {
padding: 0.6rem;
background-color: #3A4554;
border-radius: 1rem;
width: 100%;
text-align: center;
}
.example_text {
font-size: 1.2rem;
}
.show_more_container {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 0.6rem 0;
cursor: pointer;
border-radius: 0.6rem;
&:hover {
background-color: var(--dark_850_color);
}
&:active {
background-color: var(--dark_875_color);
}
}
.arrow_left_svg {
width: 2rem;
color: var(--dark_450_color);
&.to_down {
transform: rotate(-90deg);
}
&.to_up {
transform: rotate(90deg);
}
}
.message_format_settings_container {
flex-direction: column;
display: flex;
align-items: center;
flex-shrink: 0;
gap: 2rem;
}
.message_format_settings_wrapper {
display: flex;
flex-direction: column;
gap: 3.8rem;
width: 34rem;
flex-shrink: 0;
}
.swap_button_container {
width: 100%;
display: flex;
justify-content: end;
}
.swap_button_wrapper {
display: flex;
justify-content: center;
align-items: center;
gap: 0.8rem;
padding: 0.6rem 1.2rem;
border-radius: 0.4rem;
background-color: var(--dark_850_color);
cursor: pointer;
&:hover {
background-color: var(--dark_800_color);
}
&:active {
background-color: var(--dark_900_color);
}
}
.swap_text {
font-size: 1.4rem;
}
.swap_img {
width: 2rem;
}
.input_wrapper {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1.8rem;
}
.input_contents {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
color: var(--dark_basic_text_color);
}
.preset_text {
font-size: 1.6rem;
text-align: center;
flex-shrink: 0;
color: var(--dark_basic_text_color);
}
.multi_translation_input_wrapper {
display: flex;
flex-direction: column;
gap: 1rem;
justify-content: center;
color: var(--dark_basic_text_color);
}
.multi_translation_title {
font-size: 1.2rem;
}
.reset_button_wrapper {
width: 100%;
display: flex;
justify-content: end;
align-items: center;
}

View File

@@ -0,0 +1,42 @@
import styles from "./RadioButton.module.scss";
import clsx from "clsx";
export const RadioButton = (props) => {
const containerClass = clsx(styles.container, {
[styles.column]: props.column === true,
});
return (
<div className={containerClass}>
{props.checked_variable.state === "pending" && <span className={styles.loader}></span>}
{props.options.map((option) => {
const radioWrapperClass = clsx(styles.radio_button_container, {
[styles.is_selected]: props.checked_variable.data === option.id,
});
const labelClass = clsx(styles.radio_button_wrapper, {
[styles.is_selected]: props.checked_variable.data === option.id,
[styles.disabled]: option.disabled === true || props.checked_variable.state === "pending",
});
return (
<div key={option.id} className={radioWrapperClass}>
<label className={labelClass}>
<input
className={styles.radio_button_input}
type="radio"
name={props.name}
value={option.id}
onChange={() => props.selectFunction(option.id)}
checked={props.checked_variable.data === option.id}
disabled={option.disabled === true || props.checked_variable.state === "pending"}
/>
<p className={styles.radio_button_label}>{option.label}</p>
</label>
{props.ChildComponent && <props.ChildComponent option={option} {...props} />}
</div>
);
})}
</div>
);
};

View File

@@ -0,0 +1,74 @@
@import "@scss_mixins";
.container {
display: flex;
gap: 0.4rem;
position: relative;
flex-shrink: 0;
flex-wrap: wrap;
max-width: 70%;
&.column {
flex-direction: column;
}
}
.radio_button_container {
display: flex;
align-items: center;
justify-content: space-between;
gap: 2rem;
position: relative;
}
.radio_button_wrapper {
display: flex;
flex-shrink: 0;
align-items: center;
cursor: pointer;
gap: 1rem;
padding: 0.6rem 0.8rem;
border-radius: 0.4rem;
&:hover {
background-color: var(--dark_850_color);
}
&:active {
background-color: var(--dark_925_color);
}
&.is_selected {
pointer-events: none;
}
&.disabled {
pointer-events: none;
color: var(--dark_600_color);
}
}
.radio_button_input {
appearance: none;
margin: 0;
width: 2rem;
height: 2rem;
border: 0.3rem solid var(--dark_600_color);
border-radius: 50%;
transition: border-color .1s ease, border-width .1s ease;
flex-shrink: 0;
cursor: inherit;
&:checked {
border-color: var(--primary_400_color);
border-width: 0.6rem;
}
&:disabled {
border-color: var(--dark_825_color);
}
}
.radio_button_label {
font-size: 1.4rem;
font-weight: 400;
flex-shrink: 0;
}
.loader {
@include loader(2rem, 0.2rem, left, -1.6rem);
}

View File

@@ -0,0 +1,11 @@
import styles from "./SectionLabelComponent.module.scss";
import clsx from "clsx";
export const SectionLabelComponent = (props) => {
return (
<div className={styles.container}>
<label className={styles.section_label}>{props.label}</label>
<div className={styles.section_line}></div>
</div>
);
};

View File

@@ -0,0 +1,16 @@
.container {
display: flex;
justify-content: center;
align-items: center;
gap: 2rem;
padding-bottom: 2rem;
}
.section_label {
font-size: 2rem;
flex-shrink: 0;
}
.section_line {
width: 100%;
height: 0.1rem;
background: linear-gradient(90deg, var(--dark_400_color) 0%, var(--dark_600_color) 35%, var(--dark_800_color) 100%);
}

View File

@@ -0,0 +1,111 @@
import React from "react";
import styles from "./Slider.module.scss";
import MUI_Slider from "@mui/material/Slider";
import clsx from "clsx";
export const Slider = (props) => {
const location = props.valueLabelDisplayLocation || "top";
const sliderSx = {
color: "var(--dark_700_color)",
"& .MuiSlider-thumb": {
backgroundColor: "var(--primary_600_color)",
"&:hover, &.Mui-focusVisible, &.Mui-active": {
boxShadow: `0 0 0 0.8rem var(--primary_600_color_44)`,
},
"& .MuiSlider-valueLabel": {
position: "absolute",
backgroundColor: "var(--dark_800_color)",
width: "fit-content",
minWidth: "4.8rem",
padding: "0.4rem 0.8rem",
lineHeight: "1.15",
"& .MuiSlider-valueLabelLabel": {
fontSize: "1.4rem",
},
...(location === "top" && {
top: "-110%",
left: "50%",
transform: "translate(-50%, -50%) scale(0)",
transformOrigin: "bottom center",
"&.MuiSlider-valueLabelOpen": {
transform: "translate(-50%, -50%) scale(1)",
},
"&::before": {
bottom: "0%",
left: "50%",
},
}),
...(location === "right" && {
top: "50%",
left: "150%",
transform: "translate(0, -50%) scale(0)",
transformOrigin: "left center",
"&.MuiSlider-valueLabelOpen": {
transform: "translate(0, -50%) scale(1)",
},
"&::before": {
bottom: "50%",
left: "0",
},
}),
...(location === "left" && {
// top: "50%",
// right: "50%",
// transform: "translate(-50%, -50%) scale(0)",
// transformOrigin: "bottom center",
// "&.MuiSlider-valueLabelOpen": {
// transform: "translate(-50%, -50%) scale(1)",
// },
// "&::before": {
// bottom: "50%",
// left: "100%",
// },
}),
},
},
"& .MuiSlider-markLabel": {
fontSize: "1.4rem",
color: "var(--dark_550_color)",
whiteSpace: "nowrap",
},
"& .MuiSlider-markLabelActive": {
color: "var(--primary_300_color)",
},
};
return (
<div
className={clsx(
styles.container,
props.className,
{ [styles.no_padding]: props.no_padding || props.is_break_point }
)}
>
<MUI_Slider
aria-label="Default"
// valueLabelDisplay="on"
valueLabelDisplay={props.valueLabelDisplay ? props.valueLabelDisplay : "auto"}
value={props.variable}
step={props.step}
min={Number(props.min)}
max={Number(props.max)}
onChange={(_e, value) => props.onchangeFunction(value)}
onChangeCommitted={(_e, value) =>
props.onchangeCommittedFunction ? props.onchangeCommittedFunction(value) : null
}
onMouseEnter={(event) =>
props.onMouseEnterFunction ? props.onMouseEnterFunction(event) : null
}
onMouseLeave={(event) =>
props.onMouseLeaveFunction ? props.onMouseLeaveFunction(event) : null
}
marks={props.marks}
track={props.track}
orientation={props.orientation}
valueLabelFormat={`${props.valueLabelFormat ? props.valueLabelFormat : props.variable}`}
sx={sliderSx}
/>
</div>
);
};

View File

@@ -0,0 +1,11 @@
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
padding-left: 4rem;
&.no_padding {
padding-left: 0;
}
}

View File

@@ -0,0 +1,44 @@
import clsx from "clsx";
import { useState } from "react";
import styles from "./SwitchBox.module.scss";
export const SwitchBox = (props) => {
const [is_hovered, setIsHovered] = useState(false);
const [is_mouse_down, setIsMouseDown] = useState(false);
const is_pending = (props.variable.state === "pending");
const getClassNames = (base_class) => clsx(base_class, {
[styles.is_active]: (props.variable.data === true),
[styles.is_pending]: is_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);
const toggleFunction = () => {
props.toggleFunction();
};
return (
<div className={styles.switchbox_container}>
<div className={getClassNames(styles.switchbox_wrapper)}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onClick={toggleFunction}
>
<div className={getClassNames(styles.toggle_control)}>
<span className={getClassNames(styles.control)}></span>
{is_pending && <span className={styles.loader}></span>}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,33 @@
@import "@scss_mixins";
.switchbox_container {
display: flex;
justify-content: end;
align-items: center;
height: 2rem;
}
.switchbox_wrapper {
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
padding: 2rem;
height: 100%;
flex-shrink: 0;
&.is_pending {
pointer-events: none;
}
}
.toggle_control {
position: relative;
@include toggle_control_styles;
display: flex;
justify-content: center;
align-items: center;
}
.loader {
@include loader(2rem, 0.2rem, right, -4rem);
}

View File

@@ -0,0 +1,135 @@
import { useEffect, useState } from "react";
import styles from "./ThresholdComponent.module.scss";
import { SliderAndMeter } from "./slider_and_meter/SliderAndMeter";
import { ThresholdEntry } from "./threshold_entry/ThresholdEntry";
import { VolumeCheckButton } from "./volume_check_button/VolumeCheckButton";
import { useVolume } from "@logics_common";
import MicSvg from "@images/mic.svg?react";
import HeadphonesSvg from "@images/headphones.svg?react";
import {
useDevice,
} from "@logics_configs";
export const ThresholdComponent = (props) => {
return (
<div className={styles.container}>
{props.id === "mic_threshold"
? <MicComponent {...props} />
: <SpeakerComponent {...props} />
}
</div>
);
};
const MicComponent = (props) => {
const {
currentMicThreshold,
setMicThreshold,
currentEnableAutomaticMicThreshold,
} = useDevice();
const [ui_threshold, setUiThreshold] = useState(currentMicThreshold.data);
const {
volumeCheckStart_Mic,
volumeCheckStop_Mic,
currentMicThresholdCheckStatus,
} = useVolume();
useEffect(() => {
if (currentEnableAutomaticMicThreshold.data === true) {
setUiThreshold("Auto");
} else {
setUiThreshold(currentMicThreshold.data);
}
}, [currentMicThreshold.data, currentEnableAutomaticMicThreshold]);
const setUiThresholdFunction = (payload_ui_threshold) => {
setUiThreshold(payload_ui_threshold);
};
const setThresholdFunction = (payload_threshold) => {
setMicThreshold(payload_threshold);
};
const is_disable = currentEnableAutomaticMicThreshold.data === true ? true : false;
return (
<>
<VolumeCheckButton
{...props}
SvgComponent={MicSvg}
startFunction={volumeCheckStart_Mic}
stopFunction={volumeCheckStop_Mic}
isChecking={currentMicThresholdCheckStatus}
/>
<SliderAndMeter
{...props}
ui_threshold={ui_threshold}
setUiThresholdFunction={setUiThresholdFunction}
setThresholdFunction={setThresholdFunction}
/>
<ThresholdEntry
{...props}
ui_threshold={ui_threshold}
setUiThresholdFunction={setUiThresholdFunction}
setThresholdFunction={setThresholdFunction}
is_disable={is_disable}
/>
</>
);
};
const SpeakerComponent = (props) => {
const {
currentSpeakerThreshold,
setSpeakerThreshold,
currentEnableAutomaticSpeakerThreshold,
} = useDevice();
const [ui_threshold, setUiThreshold] = useState(currentSpeakerThreshold.data);
const {
volumeCheckStart_Speaker,
volumeCheckStop_Speaker,
currentSpeakerThresholdCheckStatus,
} = useVolume();
useEffect(() => {
if (currentEnableAutomaticSpeakerThreshold.data === true) {
setUiThreshold("Auto");
} else {
setUiThreshold(currentSpeakerThreshold.data);
}
}, [currentSpeakerThreshold.data, currentEnableAutomaticSpeakerThreshold]);
const setUiThresholdFunction = (payload_ui_threshold) => {
setUiThreshold(payload_ui_threshold);
};
const setThresholdFunction = (payload_threshold) => {
setSpeakerThreshold(payload_threshold);
};
const is_disable = currentEnableAutomaticSpeakerThreshold.data === true ? true : false;
return (
<>
<VolumeCheckButton
{...props}
SvgComponent={HeadphonesSvg}
startFunction={volumeCheckStart_Speaker}
stopFunction={volumeCheckStop_Speaker}
isChecking={currentSpeakerThresholdCheckStatus}
/>
<SliderAndMeter
{...props}
ui_threshold={ui_threshold}
setUiThresholdFunction={setUiThresholdFunction}
setThresholdFunction={setThresholdFunction}
/>
<ThresholdEntry
{...props}
ui_threshold={ui_threshold}
setUiThresholdFunction={setUiThresholdFunction}
setThresholdFunction={setThresholdFunction}
is_disable={is_disable}
/>
</>
);
};

View File

@@ -0,0 +1,7 @@
.container {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
gap: 2rem;
}

View File

@@ -0,0 +1,88 @@
import styles from "./SliderAndMeter.module.scss";
import {
useStore_MicVolume,
useStore_SpeakerVolume,
} from "@store";
import {
useDevice,
} from "@logics_configs";
export const SliderAndMeter = (props) => {
return (
<div className={styles.container}>
<div className={styles.meter_container}>
{props.id === "mic_threshold"
? <ThresholdVolumeMeter_Mic {...props}/>
: <ThresholdVolumeMeter_Speaker {...props}/>
}
</div>
</div>
);
};
const ThresholdVolumeMeter_Mic = (props) => {
const { currentMicVolume } = useStore_MicVolume();
const { currentEnableAutomaticMicThreshold } = useDevice();
const currentVolumeVariable = Math.min(currentMicVolume.data, props.max);
const volume_width_percentage = (currentVolumeVariable / props.max) * 100;
return (
<>
<VolumeMeter volume_width_percentage={volume_width_percentage} volume={currentVolumeVariable} threshold={props.ui_threshold}/>
{currentEnableAutomaticMicThreshold.data === false &&
<input
type="range"
min={props.min}
max={props.max}
value={props.ui_threshold}
onChange={(e) => props.setUiThresholdFunction(e.target.value)}
onMouseUp={(e) => props.setThresholdFunction(e.target.value)}
className={styles.threshold_slider}
/>
}
</>
);
};
const ThresholdVolumeMeter_Speaker = (props) => {
const { currentSpeakerVolume } = useStore_SpeakerVolume();
const { currentEnableAutomaticSpeakerThreshold } = useDevice();
const currentVolumeVariable = Math.min(currentSpeakerVolume.data, props.max);
const volume_width_percentage = (currentVolumeVariable / props.max) * 100;
return (
<>
<VolumeMeter volume_width_percentage={volume_width_percentage} volume={currentVolumeVariable} threshold={props.ui_threshold} />
{currentEnableAutomaticSpeakerThreshold.data === false &&
<input
type="range"
min={props.min}
max={props.max}
value={props.ui_threshold}
onChange={(e) => props.setUiThresholdFunction(e.target.value)}
onMouseUp={(e) => props.setThresholdFunction(e.target.value)}
className={styles.threshold_slider}
/>
}
</>
);
};
const VolumeMeter = ({ volume_width_percentage, volume, threshold }) => {
return (
<div
className={styles.volume_meter}
style={{
width: `${volume_width_percentage}%`,
backgroundColor: (volume < threshold) ? "var(--primary_750_color)" : "var(--primary_400_color)"
}}
/>
);
};

View File

@@ -0,0 +1,55 @@
.container {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
flex: 1;
// width: 100%;
position: relative; // for dev
}
.meter_container {
position: relative;
width: 100%;
height: 0.8rem;
background: var(--dark_800_color);
border-radius: 0.4rem;
}
.volume_meter {
height: 100%;
border-radius: inherit;
transition: width 0.1s ease, background-color 0.1s ease;
}
.threshold_slider {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: all;
&::-webkit-slider-runnable-track {
width: 100%;
background: transparent;
cursor: pointer;
}
&::-webkit-slider-thumb {
-webkit-appearance: none;
width: 0.4rem;
height: 4rem;
background: var(--primary_600_color);
border-radius: 0.2rem;
cursor: pointer;
}
&:hover::-webkit-slider-thumb{
background: var(--primary_500_color);
}
&:focus {
outline: none;
}
}

View File

@@ -0,0 +1,59 @@
import clsx from "clsx";
import styles from "./ThresholdEntry.module.scss";
export const ThresholdEntry = (props) => {
return (
<div className={styles.container}>
<div className={styles.entry_wrapper}>
{props.id === "mic_threshold"
? <ThresholdEntry_Mic {...props}/>
: <ThresholdEntry_Speaker {...props}/>
}
</div>
</div>
);
};
const ThresholdEntry_Mic = (props) => {
const onChangeFunction = (e) => {
if (e.currentTarget.value === "") {
props.setThresholdFunction("0");
} else {
props.setThresholdFunction(e.currentTarget.value);
}
};
const class_names = clsx(styles.entry_input_area, {
[styles.is_disable]: props.is_disable
});
return (
<input
className={class_names}
onChange={onChangeFunction}
value={props.ui_threshold}
/>
);
};
const ThresholdEntry_Speaker = (props) => {
const onChangeFunction = (e) => {
if (e.currentTarget.value === "") {
props.setThresholdFunction("0");
} else {
props.setThresholdFunction(e.currentTarget.value);
}
};
const class_names = clsx(styles.entry_input_area, {
[styles.is_disable]: props.is_disable
});
return (
<input
className={class_names}
onChange={onChangeFunction}
value={props.ui_threshold}
/>
);
};

View File

@@ -0,0 +1,23 @@
.container {
}
.entry_wrapper {
width: 6rem;
height: 100%;
padding: 0.6rem;
background-color: var(--dark_875_color);
border: 0.1rem solid var(--dark_750_color);
border-radius: 0.4rem;
}
.entry_input_area {
width: 100%;
height: 100%;
font-size: 1.4rem;
resize: none;
&.is_disable {
color: var(--dark_500_color);
pointer-events: none;
}
}

View File

@@ -0,0 +1,33 @@
import React from "react";
import { useI18n } from "@useI18n";
import clsx from "clsx";
import styles from "./VolumeCheckButton.module.scss";
export const VolumeCheckButton = React.memo((props) => {
const { t } = useI18n();
const getClassNames = (baseClass) => clsx(baseClass, {
[styles.is_active]: (props.isChecking?.data === true),
[styles.is_pending]: (props.isChecking.state === "pending"),
});
const toggleFunction = () => {
if (props.isChecking?.data === true) {
props.stopFunction();
} else if (props.isChecking?.data === false) {
props.startFunction();
}
};
return (
<div className={styles.container}>
<div className={getClassNames(styles.button)} onClick={toggleFunction}>
<props.SvgComponent className={styles.button_svg} />
<p className={styles.button_text}>{t("config_page.device.check_volume")}</p>
</div>
</div>
);
});
VolumeCheckButton.displayName = "VolumeCheckButton";

View File

@@ -0,0 +1,44 @@
.button {
width: 100%;
background-color: var(--dark_800_color);
display: flex;
justify-content: center;
align-items: center;
padding: 0.8rem 1rem;
border-radius: 0.4rem;
gap: 0.4rem;
cursor: pointer;
&.is_active {
background-color: var(--primary_500_color);
&:hover {
background-color: var(--primary_450_color);
}
&:active {
background-color: var(--primary_550_color);
}
}
&.is_pending {
background-color: var(--dark_850_color);
pointer-events: none;
.button_svg, .button_text {
color: var(--dark_500_color);
}
}
&:hover {
background-color: var(--dark_775_color);
}
&:active {
background-color: var(--dark_850_color);
}
}
.button_svg {
width: 1.4rem;
color: var(--dark_350_color);
}
.button_text {
font-size: 1.2rem;
}

View File

@@ -0,0 +1,109 @@
import { useI18n } from "@useI18n";
import styles from "./WordFilter.module.scss";
import { _Entry } from "../_atoms/_entry/_Entry";
import { useState } from "react";
import { useStore_IsOpenedMicWordFilterList } from "@store";
import { useTranscription } from "@logics_configs";
export const WordFilter = () => {
const { t } = useI18n();
const [input_value, setInputValue] = useState("");
const { currentMicWordFilterList, updateMicWordFilterList, setMicWordFilterList } = useTranscription();
const { currentIsOpenedMicWordFilterList, updateIsOpenedMicWordFilterList } = useStore_IsOpenedMicWordFilterList();
const onChangeEntry = (e) => {
setInputValue(e.target.value);
};
const addWords = () => {
if (input_value === undefined) return;
updateMicWordFilterList((prev_list) => {
const input_value_array = input_value.split(",");
let updated_list = [...prev_list.data];
for (let each_input_value of input_value_array) {
each_input_value = each_input_value.trim();
if (each_input_value) {
const exists = updated_list.find((item) => item === each_input_value);
if (!exists) {
updated_list = [...updated_list, each_input_value];
}
}
}
setMicWordFilterList(updated_list);
return updated_list;
});
updateIsOpenedMicWordFilterList(true);
setInputValue("");
};
const deleteAction = (target_item_value) => {
updateMicWordFilterList((prev_list) => {
const updated_list = prev_list.data.filter((item) => item !== target_item_value);
setMicWordFilterList(updated_list);
return updated_list;
});
};
return (
<div className={styles.container}>
{ currentIsOpenedMicWordFilterList.data &&
<div className={styles.list_section_wrapper}>
{
currentMicWordFilterList.data.map((item, index) => {
return <WordFilterItem value={item} key={index} deleteAction={deleteAction}/>;
})
}
</div>
}
<div className={styles.entry_section_wrapper}>
<_Entry width="30rem" onChange={onChangeEntry} ui_variable={input_value}/>
<button className={styles.add_button} onClick={addWords}>{t("config_page.transcription.mic_word_filter.add_button_label")}</button>
</div>
</div>
);
};
import DeleteSvg from "@images/cancel.svg?react";
import clsx from "clsx";
const WordFilterItem = (props) => {
const item_wrapper_class_names = clsx(styles["item_wrapper"]);
const item_text_class_names = clsx(styles["item_text"]);
return (
<div className={item_wrapper_class_names}>
<p className={item_text_class_names}>{props.value}</p>
<button className={clsx(styles.action_button, styles.delete)} onClick={() => props.deleteAction(props.value)}>
<DeleteSvg className={styles.delete_svg}/>
</button>
</div>
);
};
import ArrowLeftSvg from "@images/arrow_left.svg?react";
export const WordFilterListToggleComponent = () => {
const { t } = useI18n();
const { currentIsOpenedMicWordFilterList, updateIsOpenedMicWordFilterList } = useStore_IsOpenedMicWordFilterList();
const { currentMicWordFilterList } = useTranscription();
const svg_class_names = clsx(styles["arrow_left_svg"], {
[styles.to_down]: !currentIsOpenedMicWordFilterList.data,
[styles.to_up]: currentIsOpenedMicWordFilterList.data
});
const OnclickFunction = () => {
updateIsOpenedMicWordFilterList(!currentIsOpenedMicWordFilterList.data);
};
return (
<div className={styles.toggle_button_container}>
<p className={styles.words_count_text}>{t("config_page.transcription.mic_word_filter.count_desc", {count: currentMicWordFilterList.data.length} )}</p>
<button className={styles.toggle_button_wrapper} onClick={OnclickFunction}>
<ArrowLeftSvg className={svg_class_names}/>
</button>
</div>
);
};

View File

@@ -0,0 +1,117 @@
.container {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 2rem;
}
.list_section_wrapper {
display: flex;
flex-wrap: wrap;
width: 100%;
gap: 0.6rem;
overflow-y: auto;
max-height: 20rem;
padding-right: 2rem;
}
.item_wrapper {
display: flex;
justify-content: center;
align-items: center;
gap: 0.4rem;
background-color: var(--dark_800_color);
padding: 0.2rem 0.2rem 0.2rem 1rem;
border-radius: 0.4rem;
}
.item_text {
font-size: 1.4rem;
font-weight: 300;
}
.action_button {
border-radius: 0.4rem;
&:hover {
background-color: var(--dark_750_color);
}
&:active {
background-color: var(--dark_850_color);
}
&.delete {
padding: 0.2rem;
}
&.redo {
padding: 0.6rem;
}
}
.delete_svg {
width: 2.4rem;
color: var(--error_bc_color);
}
.redo_svg {
width: 1.6rem;
color: var(--dark_500_color);
}
.entry_section_wrapper {
display: flex;
justify-content: center;
align-items: center;
gap: 2rem;
}
.add_button {
padding: 0.8rem 1.2rem;
background-color: var(--primary_600_color);
font-size: 1.4rem;
border-radius: 0.4rem;
text-align: center;
flex-shrink: 0;
&:hover {
background-color: var(--primary_500_color);
}
&:active {
background-color: var(--primary_700_color);
}
}
.toggle_button_container {
display: flex;
justify-content: center;
align-items: center;
gap: 2rem;
}
.words_count_text {
font-size: 1.6rem;
}
.toggle_button_wrapper {
padding: 1.6rem;
border-radius: 0.4rem;
&:hover {
background-color: var(--dark_825_color);
}
&:active {
background-color: var(--dark_900_color);
}
}
.arrow_left_svg {
width: 2.4rem;
&.to_down {
transform: rotate(-90deg);
}
&.to_up {
transform: rotate(90deg);
}
}

View File

@@ -0,0 +1,146 @@
import clsx from "clsx";
import styles from "./Templates.module.scss";
import { useStore_IsOpenedDropdownMenu, useStore_IsBreakPoint } from "@store";
import {
LabelComponent,
DropdownMenu,
Slider,
SwitchBox,
Entry,
EntryWithSaveButton,
HotkeysEntry,
RadioButton,
OpenWebpage_DeeplAuthKey,
DeeplAuthKey,
ActionButton,
ComputeDevice,
WordFilter,
WordFilterListToggleComponent,
DownloadModels,
MessageFormat,
} from "../_components";
import { Checkbox } from "@common_components";
const LabeledContainer = ({ children, label, desc, custom_class_name }) => (
<div className={clsx(styles.container, custom_class_name)}>
<LabelComponent label={label} desc={desc} />
{children}
</div>
);
export const useOnMouseLeaveDropdownMenu = () => {
const { updateIsOpenedDropdownMenu } = useStore_IsOpenedDropdownMenu();
const onMouseLeaveFunction = () => {
updateIsOpenedDropdownMenu("");
};
return { onMouseLeaveFunction };
};
export const DropdownMenuContainer = (props) => {
const { onMouseLeaveFunction } = useOnMouseLeaveDropdownMenu();
return (
<div className={styles.container} onMouseLeave={onMouseLeaveFunction}>
<LabelComponent label={props.label} desc={props.desc} />
<DropdownMenu {...props} />
</div>
);
};
const CommonContainer = ({ Component, ...props }) => {
const { currentIsBreakPoint } = useStore_IsBreakPoint();
const container_class = clsx(styles.container, {
[styles.is_break_point]: props.add_break_point ?? currentIsBreakPoint.data,
});
return (
<LabeledContainer label={props.label} desc={props.desc} custom_class_name={container_class}>
<Component {...props} is_break_point={currentIsBreakPoint.data} />
</LabeledContainer>
);
};
export const SliderContainer = (props) => (
<CommonContainer Component={Slider} {...props} />
);
export const CheckboxContainer = (props) => (
<CommonContainer Component={Checkbox} {...props} add_break_point={false} />
);
export const SwitchBoxContainer = (props) => (
<CommonContainer Component={SwitchBox} {...props} add_break_point={false}/>
);
export const EntryContainer = (props) => (
<CommonContainer Component={Entry} {...props} add_break_point={false} />
);
export const EntryWithSaveButtonContainer = (props) => (
<CommonContainer Component={EntryWithSaveButton} {...props} add_break_point={false} />
);
export const HotkeysEntryContainer = (props) => (
<CommonContainer Component={HotkeysEntry} {...props} />
);
export const RadioButtonContainer = (props) => (
<CommonContainer Component={RadioButton} {...props} />
);
export const DeeplAuthKeyContainer = (props) => {
const { currentIsBreakPoint } = useStore_IsBreakPoint();
const container_class = clsx(styles.container, {
[styles.is_break_point]: currentIsBreakPoint.data,
});
return (
<div className={container_class}>
<div className={styles.deepl_auth_key_label_section}>
<LabelComponent label={props.label} desc={props.desc} />
<OpenWebpage_DeeplAuthKey />
</div>
<DeeplAuthKey {...props} />
</div>
);
};
export const ActionButtonContainer = (props) => (
<CommonContainer Component={ActionButton} {...props} add_break_point={false}/>
);
export const ComputeDeviceContainer = (props) => (
<CommonContainer Component={ComputeDevice} {...props} />
);
export const WordFilterContainer = (props) => (
<div className={styles.word_filter_container}>
<div className={styles.word_filter_switch_section}>
<div className={styles.word_filter_label_wrapper}>
<LabelComponent label={props.label} desc={props.desc} />
</div>
<WordFilterListToggleComponent />
</div>
<div className={styles.word_filter_section}>
<WordFilter {...props} />
</div>
</div>
);
export const DownloadModelsContainer = (props) => (
<CommonContainer Component={DownloadModels} {...props} />
);
export const MessageFormatContainer = (props) => {
return (
<div className={clsx(styles.container, styles.flex_column)}>
<div className={styles.label_only_section}>
<LabelComponent label={props.label} desc={props.desc} />
</div>
<div className={styles.message_format_section}>
<MessageFormat {...props}/>
</div>
</div>
);
};

View File

@@ -0,0 +1,57 @@
.container {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
padding: 2rem;
gap: 2rem;
&.flex_column {
flex-direction: column;
}
border-bottom: solid 0.1rem var(--dark_800_color);
&.is_break_point {
flex-direction: column;
gap: 2rem;
align-items: start;
}
}
.label_only_section {
width: 100%;
}
.deepl_auth_key_label_section {
max-width: 34rem;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
gap: 1.4rem;
}
.message_format_section {
width: 100%;
}
.word_filter_container {
display: flex;
width: 100%;
flex-direction: column;
justify-content: space-between;
align-items: center;
gap: 2rem;
padding: 2rem;
}
.word_filter_switch_section {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
}
.word_filter_label_wrapper {
max-width: 34rem;
}

View File

@@ -0,0 +1,178 @@
import styles from "./AboutVrct.module.scss";
import dev_section_title from "@images/about_vrct/dev_section_title.png";
import dev_misya from "@images/about_vrct/dev_misya.png";
import dev_shiina from "@images/about_vrct/dev_shiina.png";
import vrct_logo_for_about_vrct from "@images/about_vrct/vrct_logo_for_about_vrct.png";
import contributors_section_title from "@images/about_vrct/contributors_section_title.png";
import contributor_done from "@images/about_vrct/contributor_done.png";
import contributor_iya from "@images/about_vrct/contributor_iya.png";
import contributor_rera from "@images/about_vrct/contributor_rera.png";
import contributor_poposuke from "@images/about_vrct/contributor_poposuke.png";
import contributor_kumaguma from "@images/about_vrct/contributor_kumaguma.png";
import contributor_riku from "@images/about_vrct/contributor_riku.png";
import localization_section_title from "@images/about_vrct/localization_section_title.png";
import localization_1 from "@images/about_vrct/localization_1.png";
import localization_2 from "@images/about_vrct/localization_2.png";
import localization_3 from "@images/about_vrct/localization_3.png";
import localization_4 from "@images/about_vrct/localization_4.png";
import localization_5 from "@images/about_vrct/localization_5.png";
import special_thanks_section_title from "@images/about_vrct/special_thanks_section_title.png";
import special_thanks_members from "@images/about_vrct/special_thanks_members.png";
import special_thanks_message_en from "@images/about_vrct/special_thanks_message_en.png";
import special_thanks_message_ja from "@images/about_vrct/special_thanks_message_ja.png";
import poster_showcase_section_title from "@images/about_vrct/poster_showcase_section_title.png";
import clsx from "clsx";
import { useI18n } from "@useI18n";
import { useAppearance } from "@logics_configs";
import { PosterShowcaseContents } from "./poster_showcase_contents/PosterShowcaseContents";
export const AboutVrct = () => {
const { t } = useI18n();
const { currentUiLanguage } = useAppearance();
return (
<div className={styles.container}>
<div className={styles.dev_section}>
<img src={dev_section_title} className={clsx(styles.section_title, styles.the_developers)} />
<div className={styles.dev_section_wrapper}>
<div className={styles.dev_card_wrapper}>
<img src={dev_misya} className={styles.dev_card_img} />
<OpenLinkContainer className={styles.dev_misya_x} href_id="dev_misya_x" />
<OpenLinkContainer className={styles.dev_misya_github} href_id="dev_misya_github" />
</div>
<div className={styles.dev_card_wrapper}>
<img src={dev_shiina} className={styles.dev_card_img} />
<OpenLinkContainer className={styles.dev_shiina_x} href_id="dev_shiina_x" />
</div>
</div>
</div>
<div className={styles.project_links_and_logo_section}>
<div className={styles.about_vrct_logo_wrapper}>
<img src={vrct_logo_for_about_vrct} className={styles.about_vrct_logo} />
</div>
<div className={styles.project_links_wrapper}>
<OpenLinkContainer className={styles.project_link} href_id="project_link_booth" />
<OpenLinkContainer className={styles.project_link} href_id="project_link_documents" />
<OpenLinkContainer className={styles.project_link} href_id="project_link_vrct_github" />
<OpenLinkContainer className={styles.project_link} href_id="project_link_contact_us" />
</div>
</div>
<div className={styles.contributors_section}>
<img src={contributors_section_title} className={clsx(styles.section_title, styles.contributors)} />
<div className={styles.contributors_img_wrapper}>
<div className={styles.contributor_card_wrapper}>
<img src={contributor_done} className={clsx(styles.contributors_img, styles.contributors)} />
<OpenLinkContainer className={styles.contributors_done_san_x} href_id="contributors_done_san_x" />
</div>
<div className={styles.contributor_card_wrapper}>
<img src={contributor_iya} className={clsx(styles.contributors_img, styles.contributors)} />
<OpenLinkContainer className={styles.contributors_iya_x} href_id="contributors_iya_x" />
</div>
<div className={styles.contributor_card_wrapper}>
<img src={contributor_rera} className={clsx(styles.contributors_img, styles.contributors)} />
<OpenLinkContainer className={styles.contributors_rera_x} href_id="contributors_rera_x" />
<OpenLinkContainer className={styles.contributors_rera_github} href_id="contributors_rera_github" />
</div>
<div className={styles.contributor_card_wrapper}>
<img src={contributor_poposuke} className={clsx(styles.contributors_img, styles.contributors)} />
<OpenLinkContainer className={styles.contributors_poposuke_x} href_id="contributors_poposuke_x" />
</div>
<div className={styles.contributor_card_wrapper}>
<img src={contributor_kumaguma} className={clsx(styles.contributors_img, styles.contributors)} />
<OpenLinkContainer className={styles.contributors_kumaguma_x} href_id="contributors_kumaguma_x" />
</div>
<div className={styles.contributor_card_wrapper}>
<img src={contributor_riku} className={clsx(styles.contributors_img, styles.contributors)} />
<OpenLinkContainer className={styles.contributors_riku_x} href_id="contributors_riku_x" />
</div>
</div>
</div>
<div className={styles.localization_section}>
<img src={localization_section_title} className={clsx(styles.section_title, styles.localization)} />
<div className={styles.localization_members_wrapper}>
<div className={styles.localization_members_row_wrapper}>
<img src={localization_1} className={styles.localization_members_img} />
<img src={localization_2} className={styles.localization_members_img} />
</div>
<div className={styles.localization_members_row_wrapper}>
<img src={localization_3} className={styles.localization_members_img} />
<img src={localization_4} className={styles.localization_members_img} />
</div>
<div className={styles.localization_members_row_wrapper}>
<img src={localization_5} className={styles.localization_members_img} />
{/* <img src={localization_6} className={styles.localization_members_img} /> */}
</div>
</div>
</div>
<div className={styles.special_thanks_section}>
<img src={special_thanks_section_title} className={clsx(styles.section_title, styles.special_thanks)} />
<img src={special_thanks_members} className={styles.special_thanks_members_img} />
{
currentUiLanguage.data === "ja"
? <img src={special_thanks_message_ja} className={styles.special_thanks_message_img} />
: <img src={special_thanks_message_en} className={styles.special_thanks_message_img} />
}
</div>
<div className={styles.poster_showcase_section}>
<img src={poster_showcase_section_title} className={clsx(styles.section_title, styles.poster_showcase)} />
<PosterShowcaseContents />
</div>
<div className={styles.vrchat_disclaimer_section}>
<p className={styles.vrchat_disclaimer}>VRCT is not endorsed by VRChat and does not reflect the views or opinions of VRChat or anyone officially involved in producing or managing VRChat properties. VRChat and all associated properties are trademarks or registered trademarks of VRChat Inc. VRChat © VRChat Inc.</p>
</div>
</div>
);
};
import dev_x_icon from "@images/about_vrct/dev_x_icon.png";
import dev_github_icon from "@images/about_vrct/dev_github_icon.png";
import contributors_x_icon from "@images/about_vrct/contributors_x_icon.png";
import contributors_github_icon from "@images/about_vrct/contributors_github_icon.png";
import project_link_booth from "@images/about_vrct/project_link_booth.png";
import project_link_documents from "@images/about_vrct/project_link_documents.png";
import project_link_vrct_github from "@images/about_vrct/project_link_vrct_github.png";
import project_link_contact_us from "@images/about_vrct/project_link_contact_us.png";
const about_vrct_links = {
dev_misya_x: { img: dev_x_icon, href: "https://twitter.com/misya_ai" },
dev_misya_github: { img: dev_github_icon, href: "https://github.com/misyaguziya" },
dev_shiina_x: { img: dev_x_icon, href: "https://twitter.com/Shiina_12siy" },
project_link_booth: { img: project_link_booth, href: "https://misyaguziya.booth.pm/items/5155325" },
project_link_documents: { img: project_link_documents, href: "https://mzsoftware.notion.site/VRCT-Documents-be79b7a165f64442ad8f326d86c22246" },
project_link_vrct_github: { img: project_link_vrct_github, href: "https://github.com/misyaguziya/VRCT" },
project_link_contact_us: { img: project_link_contact_us, href: "https://docs.google.com/forms/d/e/1FAIpQLSei-xoydOY60ivXqhOjaTzNN8PiBQIDcNhzfy6cw2sjYkcg_g/viewform" },
contributors_done_san_x: { img: contributors_x_icon, href: "https://twitter.com/done_vrc" },
contributors_iya_x: { img: contributors_x_icon, href: "https://twitter.com/IYAA_HHHH" },
contributors_rera_x: { img: contributors_x_icon, href: "https://twitter.com/rerassi" },
contributors_rera_github: { img: contributors_github_icon, href: "https://github.com/soumt-r" },
contributors_poposuke_x: { img: contributors_x_icon, href: "https://twitter.com/sig_popo" },
contributors_kumaguma_x: { img: contributors_x_icon, href: "https://twitter.com/K_kumaguma_A" },
contributors_riku_x: { img: contributors_x_icon, href: "https://twitter.com/Riku7302" },
};
const OpenLinkContainer = ({className, href_id}) => {
const href = about_vrct_links[href_id].href;
const img = about_vrct_links[href_id].img;
return (
<a className={className} href={href} target="_blank" rel="noreferrer" >
{/* for adjust size to their parent component's width. */}
<img style={ {height: "100%", width: "100%", "objectFit": "contain" }} src={img} />
</a>
);
};

View File

@@ -0,0 +1,215 @@
.container {
display: flex;
gap: 2.2rem;
justify-content: center;
align-items: center;
flex-direction: column;
max-width: 72rem;
margin: auto;
}
.section_title {
height: 1.2rem;
object-fit: contain;
object-position: left;
&.the_developers {
margin-bottom: 0.8rem;
}
&.contributors {
margin-bottom: 0.8rem;
}
&.special_thanks {
margin-bottom: 0.6rem;
}
&.poster_showcase {
margin-bottom: 0.6rem;
}
}
.dev_section {
display: flex;
flex-direction: column;
}
.dev_section_wrapper {
display: flex;
gap: 2rem;
flex-wrap: wrap;
width: 100%;
justify-content: center;
}
.dev_card_wrapper {
position: relative;
// width: 100%;
}
.dev_card_img {
width: 34.6rem;
}
@mixin dev_sns_styles($right) {
position: absolute;
right: $right;
bottom: 0.6rem;
width: 2.8rem;
padding: 0.4rem;
border-radius: 0.4rem;
&:hover {
background-color: var(--dark_800_color);
}
&:active {
background-color: var(--dark_900_color)
}
}
.dev_misya_x {
@include dev_sns_styles(6rem);
}
.dev_misya_github {
@include dev_sns_styles(3rem);
}
.dev_shiina_x {
@include dev_sns_styles(2.4rem);
}
.project_links_and_logo_section {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 0 5.5rem;
flex-wrap: wrap;
gap: 1rem;
}
.about_vrct_logo_wrapper {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
padding: 2rem 0;
}
.about_vrct_logo {
width: 20rem;
object-fit: contain;
}
.project_links_wrapper {
display: flex;
flex-wrap: wrap;
gap: 0.2rem;
align-items: start;
}
.project_link {
height: 2.6rem;
padding: 0.4rem 1rem;
border-radius: 0.4rem;
flex-shrink: 0;
&:hover {
background-color: var(--dark_850_color);
}
&:active {
background-color: var(--dark_900_color)
}
}
.contributors_img_wrapper {
display: flex;
flex-wrap: wrap;
gap: 1rem;
justify-content: center;
align-items: center;
}
.contributor_card_wrapper {
position: relative;
}
.contributors_img {
width: 22rem;
}
@mixin contributors_sns_styles($bottom, $left) {
position: absolute;
left: $left;
bottom: $bottom;
width: 2.4rem;
padding: 0.4rem;
border-radius: 0.4rem;
transform: translate(50%, 50%);
&:hover {
background-color: var(--dark_825_color);
}
&:active {
background-color: var(--dark_888_color)
}
}
$bottom_pos: 16%;
$sns_left_pos: 0.8rem;
.contributors_done_san_x {
@include contributors_sns_styles($bottom_pos, $sns_left_pos);
}
.contributors_iya_x {
@include contributors_sns_styles($bottom_pos, $sns_left_pos);
}
.contributors_rera_x {
@include contributors_sns_styles($bottom_pos, calc($sns_left_pos - 1.4rem));
}
.contributors_rera_github {
@include contributors_sns_styles($bottom_pos, calc($sns_left_pos + 1.4rem));
}
.contributors_poposuke_x {
@include contributors_sns_styles($bottom_pos, $sns_left_pos);
}
.contributors_kumaguma_x {
@include contributors_sns_styles($bottom_pos, $sns_left_pos);
}
.contributors_riku_x {
@include contributors_sns_styles($bottom_pos, $sns_left_pos);
}
.localization_section {
display: flex;
flex-direction: column;
width: 100%;
gap: 0.6rem;
}
.localization_members_wrapper {
display: flex;
justify-content: center;
align-items: start;
column-gap: 6rem;
row-gap: 1rem;
flex-wrap: wrap;
}
.localization_members_row_wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: start;
gap: 0.2rem;
}
.localization_members_img {
height: 2.2rem;
}
.special_thanks_section {
display: flex;
flex-direction: column;
}
.special_thanks_members_img {
width: 100%;
margin-bottom: 0.4rem;
}
.special_thanks_message_img {
width: 100%;
}
.poster_showcase_section {
display: flex;
flex-direction: column;
width: 100%;
}
.vrchat_disclaimer {
font-size: 1.2rem;
margin-top: 8rem;
color: var(--dark_600_color);
}

View File

@@ -0,0 +1,12 @@
import styles from "./PosterShowcaseContents.module.scss";
import { PostersContents } from "./posters_contents/PostersContents";
import { PosterShowcaseWorldsContents } from "./poster_showcase_worlds_contents/PosterShowcaseWorldsContents";
export const PosterShowcaseContents = () => {
return (
<div className={styles.container}>
<PosterShowcaseWorldsContents />
<PostersContents />
</div>
);
};

View File

@@ -0,0 +1,7 @@
.container {
display: flex;
justify-content: center;
align-items: start;
gap: 2rem;
flex-wrap: wrap;
}

View File

@@ -0,0 +1,93 @@
import clsx from "clsx";
import styles from "./PosterShowcaseWorldsContents.module.scss";
import { useStore_PosterShowcaseWorldPageIndex } from "@store";
const images = import.meta.glob("@images/about_vrct/showcased_worlds/*.{png,jpg,jpeg,svg}", { eager: true });
const getImageByFileName = (file_name) => {
const imagePath = Object.keys(images).find((path) => path.endsWith(file_name + ".png"));
return imagePath ? images[imagePath]?.default : null;
};
import poster_showcase_worlds_settings from "./poster_showcase_worlds_settings";
import { chunkArray } from "@utils";
export const PosterShowcaseWorldsContents = () => {
const { currentPosterShowcaseWorldPageIndex } = useStore_PosterShowcaseWorldPageIndex();
const poster_showcase_world_images = poster_showcase_worlds_settings.map((setting) => ({
img: getImageByFileName(setting.image_file_name),
x_post_num: setting.x_post_num
}));
const chunked_poster_showcase_world_images = chunkArray(poster_showcase_world_images, 8);
const target_poster_showcase_world_images = chunked_poster_showcase_world_images[currentPosterShowcaseWorldPageIndex.data];
return (
<div className={styles.container}>
<div className={styles.poster_showcase_world_container}>
{target_poster_showcase_world_images.map((poster, index) => {
const class_names = clsx(styles.poster_showcase_world_wrapper, {
[styles.clickable]: (poster.x_post_num !== null)
});
const content = (
<div className={styles.poster_showcase_world_img} >
<img style={ {height: "100%", width: "100%", "objectFit": "contain" }} src={poster.img} />
</div>
);
if (poster.x_post_num !== null) {
return (
<a href={`https://x.com/Shiina_12siy/status/${poster.x_post_num}`} target="_blank" rel="noreferrer" className={class_names} key={index}>
{content}
</a>
);
} else {
return (
<div className={class_names} key={index}>
{content}
</div>
);
}
})}
</div>
<PosterShowcaseWorldsPagination page_length={chunked_poster_showcase_world_images.length}/>
</div>
);
};
import chat_white_square from "@images/chato_white_square.png";
import { useEffect } from "react";
import { randomIntMinMax } from "@utils";
const PosterShowcaseWorldsPagination = ({ page_length }) => {
const { currentPosterShowcaseWorldPageIndex, updatePosterShowcaseWorldPageIndex } = useStore_PosterShowcaseWorldPageIndex();
useEffect(() => {
updatePosterShowcaseWorldPageIndex(randomIntMinMax(page_length -1));
},[page_length]);
const setPage = (index) => {
updatePosterShowcaseWorldPageIndex(index);
};
const getClassNames = (index, baseClass) => clsx(baseClass, {
[styles.is_active]: (currentPosterShowcaseWorldPageIndex.data === index),
});
return (
<div className={styles.pagination_container}>
{[...Array(page_length).keys()].map((index) => {
return (
<div key={index} className={getClassNames(index, styles.pagination_box)} onClick={() => setPage(index)}>
<div className={styles.chato_box}>
<img src={chat_white_square} className={getClassNames(index, styles.pagination_chato_img)}/>
</div>
<div className={styles.indicator_box}>
<div className={getClassNames(index, styles.indicator)}></div>
<p className={getClassNames(index, styles.pagination_num)}>{index + 1}</p>
</div>
</div>
);
})}
</div>
);
};

View File

@@ -0,0 +1,128 @@
.container {
display: flex;
flex-direction: column;
// flex: 1;
gap: 1rem;
justify-content: space-between;
width: 42rem;
}
$image_height: 2.8rem;
$y_padding: 0.4rem;
$image_height_gap: 0.4rem;
.poster_showcase_world_container {
display: flex;
flex-direction: column;
align-items: stretch;
gap: $image_height_gap;
height: calc( (($image_height + ($y_padding*2)) * 8) + ($image_height_gap * (8 - 1)) );
}
.poster_showcase_world_wrapper {
display: flex;
padding: $y_padding 0.6rem $y_padding 0.8rem;
border-radius: 0.4rem 0 0 0.4rem;
&.clickable {
cursor: pointer;
&:hover {
background-color: var(--dark_850_color);
}
&:active {
background-color: var(--dark_925_color);
}
}
}
.poster_showcase_world_img {
height: $image_height;
flex-shrink: 0;
}
.pagination_container {
display: flex;
justify-content: space-between;
gap: 6%;
margin: 0 2.6rem;
}
$animation_duration: .1s;
.pagination_box {
width: 100%;
cursor: pointer;
&:active .pagination_chato_img {
animation: tremble_animation $animation_duration ease-out;
}
&:active.is_active .pagination_chato_img {
transform: translate(-50%, -50%) rotate(-22deg);
}
&.is_active .pagination_chato_img {
top: 48%;
animation: rotate_animation $animation_duration ease-out;
}
&:hover {
& .pagination_chato_img {
top: 108%;
}
&.is_active .pagination_chato_img {
animation: tremble_animation $animation_duration ease-out;
}
& .pagination_num {
color: var(--dark_400_color);
}
}
}
.indicator_box {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.indicator {
width: 100%;
height: 0.2rem;
background-color: var(--dark_825_color);
&.is_active {
background-color: var(--primary_400_color);
}
}
.pagination_num {
font-size: 1.8rem;
padding: 1rem;
color: var(--dark_600_color);
&.is_active {
color: var(--primary_300_color);
}
}
.chato_box {
position: relative;
width: 100%;
height: 6rem;
overflow: hidden;
}
.pagination_chato_img {
position: absolute;
top: 200%;
left: 51%;
transform: translate(-50%, -50%) rotate(22deg);
width: 2.8rem;
transition: top $animation_duration ease-out;
}
@keyframes rotate_animation {
0% {
transform: translate(-50%, -50%) rotate(22deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg + 22deg);
}
}
@keyframes tremble_animation {
0% { left: 51%; }
25% { left: 55%; }
50% { left: 45%; }
75% { left: 48%; }
100% { left: 51%; }
}

View File

@@ -0,0 +1,95 @@
const poster_showcase_worlds_settings = [
// トサカひよ
{ image_file_name: "kokekkopiyopiyo", x_post_num: "1779076974369276014" },
// MiuJepang
{ image_file_name: "ippaidou", x_post_num: "1787801976354513319" },
{ image_file_name: "nihongokurabu", x_post_num: "1779004631936614893" },
{ image_file_name: "language_exchange_tervern", x_post_num: "1779749425923150317" },
{ image_file_name: "japanese_culture_osenbeito", x_post_num: "1788522972409721137" },
{ image_file_name: "silakan_datang_ke_rumahku", x_post_num: "1788522607631056941" },
{ image_file_name: "uj_club", x_post_num: "1780791654196388201" },
{ image_file_name: "sushi_stand_guruguru", x_post_num: "1788523302404952218" },
{ image_file_name: "sushi_guru_annex", x_post_num: "1825426749770932457" },
{ image_file_name: "una_yosh", x_post_num: "1820329216598311065" },
{ image_file_name: "cam", x_post_num: "1825427064985686138" },
{ image_file_name: "language_exchange_park", x_post_num: "1825806455322324993" },
// poposuke_sig
{ image_file_name: "usanezumi_shrine2", x_post_num: "1781224020383506649" },
// KUROINU_YOUHEI
{ image_file_name: "kuroinu_work_room", x_post_num: "1779750007564112146" },
// いちや_ICHIYA
{ image_file_name: "ehon_no_heikousekai", x_post_num: "1843891361478783425" },
{ image_file_name: "ehon_no_heikousekai_1st_anniv", x_post_num: "1842088383746875535" },
{ image_file_name: "ehon_no_heikousekai_jimusho", x_post_num: "1780792306976850285" },
{ image_file_name: "ikoiba", x_post_num: "1782723006923780580" },
{ image_file_name: "kimodameshi", x_post_num: "1781224391692714133" },
{ image_file_name: "parallel_collar", x_post_num: "1820693442105934068" },
{ image_file_name: "yoru_color", x_post_num: "1842088701599396075" },
// HayaTikaze
{ image_file_name: "study_japanese_world_japanichijou", x_post_num: "1781539871829766550" },
// aji_3
{ image_file_name: "yuttari_eikaiwa", x_post_num: "1779002892999078046" },
// 八葉そるち
{ image_file_name: "re_yatuha_room", x_post_num: "1779830390435590196" },
// chakamoto
{ image_file_name: "chakachaka_multipurpose_room", x_post_num: "1818107831289295065" },
// MloYolM (よるむ)
{ image_file_name: "cafe_cian", x_post_num: "1787802552907739504" },
// ミラクル・オルカ
{ image_file_name: "mamehinata_dogrun", x_post_num: "1782723423179100471" },
// いんくEenkoo
{ image_file_name: "tyuuniti_kouryuukai", x_post_num: "1818109101731422617" },
// 1ban_meno
{ image_file_name: "bar_asagao", x_post_num: "1788523857642758370" },
// 沈黙静寂
{ image_file_name: "monogatari_meetup", x_post_num: "1781538415789674976" },
// tommie_500
{ image_file_name: "stretch_club_starting_from_minus", x_post_num: "1825048889550102597" },
// MiMi_Sorahana # VRC日韓交流会 (KRJPEX.1355)
{ image_file_name: "kr_jp_exchange", x_post_num: "1820328755950473668" },
// Einアイン
{ image_file_name: "smokerz_guild_v2", x_post_num: "1825049450190127187" },
// KokiM1018
{ image_file_name: "poker_room_elysion", x_post_num: "1818880695344980208" },
// NEET ENGINEER
{ image_file_name: "japan_street", x_post_num: "1818881593114861924" },
// RIKU_VR
{ image_file_name: "celestial_blooms", x_post_num: "1820694531568001061" },
// ღKAEDEಇ
{ image_file_name: "omoshiro_kotoba_asobi_game", x_post_num: "1825806909343199700" },
{ image_file_name: "chill_sleep_room_03", x_post_num: "1842741645231677506" },
{ image_file_name: "chill_sleep_room_04", x_post_num: "1842742135042555906" },
// アスタルテア
{ image_file_name: "oto_no_shitatei", x_post_num: "1831575615520305619" },
// Sayascape
{ image_file_name: "sayasuke_hotel", x_post_num: "1843537673224630740" },
// ふながし
{ image_file_name: "su", x_post_num: "1843537207401058558" },
// さや-sayasoft
{ image_file_name: "saya_town", x_post_num: null },
];
export default poster_showcase_worlds_settings;

View File

@@ -0,0 +1,69 @@
import clsx from "clsx";
import styles from "./PostersContents.module.scss";
import { useAppearance } from "@logics_configs";
import { useStore_VrctPosterIndex } from "@store";
import ArrowLeftSvg from "@images/arrow_left.svg?react";
import iya_vrct_poster_ja from "@images/about_vrct/vrct_posters/iya_vrct_poster_ja.png";
import iya_vrct_poster_en from "@images/about_vrct/vrct_posters/iya_vrct_poster_en.png";
import iya_vrct_poster_cn from "@images/about_vrct/vrct_posters/iya_vrct_poster_cn.png";
import iya_vrct_poster_ko from "@images/about_vrct/vrct_posters/iya_vrct_poster_ko.png";
import iya_vrct_manga_ja from "@images/about_vrct/vrct_posters/iya_vrct_manga_ja.png";
import iya_vrct_manga_en from "@images/about_vrct/vrct_posters/iya_vrct_manga_en.png";
import iya_vrct_manga_ko from "@images/about_vrct/vrct_posters/iya_vrct_manga_ko.png";
const poster_images = [
{ img: iya_vrct_poster_ja, poster_type: "poster" },
{ img: iya_vrct_poster_en, poster_type: "poster" },
{ img: iya_vrct_poster_cn, poster_type: "poster" },
{ img: iya_vrct_poster_ko, poster_type: "poster" },
{ img: iya_vrct_manga_ja, poster_type: "manga" },
{ img: iya_vrct_manga_en, poster_type: "manga" },
{ img: iya_vrct_manga_ko, poster_type: "manga" },
];
import poster_images_authors_ja from "@images/about_vrct/vrct_posters/authors/poster_images_authors_ja.png";
import poster_images_authors_en from "@images/about_vrct/vrct_posters/authors/poster_images_authors_en.png";
import poster_images_authors_m_ja from "@images/about_vrct/vrct_posters/authors/poster_images_authors_m_ja.png";
import poster_images_authors_m_en from "@images/about_vrct/vrct_posters/authors/poster_images_authors_m_en.png";
export const PostersContents = () => {
const { currentVrctPosterIndex, updateVrctPosterIndex } = useStore_VrctPosterIndex();
const { currentUiLanguage } = useAppearance();
const updateIndex = (delta) => {
const newIndex = (currentVrctPosterIndex.data + delta + poster_images.length) % poster_images.length;
updateVrctPosterIndex(newIndex);
};
const current_poster = poster_images[currentVrctPosterIndex.data];
const current_poster_authors_img_ja = (current_poster.poster_type === "poster") ? poster_images_authors_ja : poster_images_authors_m_ja;
const current_poster_authors_img_en = (current_poster.poster_type === "poster") ? poster_images_authors_en : poster_images_authors_m_en;
return (
<div className={styles.poster_pagination_container}>
<div className={styles.poster_pagination_wrapper}>
<button
className={clsx(styles.poster_pagination_button, styles.poster_prev)}
onClick={() => updateIndex(-1)}
>
<ArrowLeftSvg className={clsx(styles.poster_pagination_svg, styles.poster_prev_svg)} />
</button>
<img src={current_poster.img} className={styles.poster_img} />
<button
className={clsx(styles.poster_pagination_button, styles.poster_next)}
onClick={() => updateIndex(1)}
>
<ArrowLeftSvg className={clsx(styles.poster_pagination_svg, styles.poster_next_svg)} />
</button>
</div>
{
currentUiLanguage.data === "ja"
? <img src={current_poster_authors_img_ja} className={styles.poster_authors_img} />
: <img src={current_poster_authors_img_en} className={styles.poster_authors_img} />
}
</div>
);
};

View File

@@ -0,0 +1,47 @@
.poster_pagination_container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
.poster_pagination_wrapper {
display: flex;
}
$poster_img_width: 18rem;
.poster_img {
width: $poster_img_width;
}
$poster_pagination_button_width: 4.6rem;
.poster_pagination_button {
width: $poster_pagination_button_width;
display: flex;
justify-content: center;
align-items: center;
color: var(--dark_700_color);
&:hover {
background-color: var(--dark_900_color);
}
&:active {
background-color: var(--dark_925_color);
}
&.poster_prev {
border-radius: 0.8rem 0 0 0.8rem;
}
&.poster_next {
border-radius: 0 0.8rem 0.8rem 0;
}
}
.poster_pagination_svg {
width: 3.2rem;
&.poster_next_svg {
transform: rotate(180deg);
}
}
.poster_authors_img {
width: $poster_img_width + $poster_pagination_button_width;
}

View File

@@ -0,0 +1,217 @@
import { useEffect, useState } from "react";
import { useI18n } from "@useI18n";
import styles from "./AdvancedSettings.module.scss";
import { useOpenFolder } from "@logics_common";
import {
useAdvancedSettings,
} from "@logics_configs";
import {
CheckboxContainer,
ActionButtonContainer,
EntryWithSaveButtonContainer,
} from "../_templates/Templates";
import {
SectionLabelComponent,
} from "../_components";
import OpenFolderSvg from "@images/open_folder.svg?react";
import HelpSvg from "@images/help.svg?react";
export const AdvancedSettings = () => {
return (
<div className={styles.container}>
<div>
<OscIpAddressContainer />
<OscPortContainer />
<OpenConfigFolderContainer />
<OpenSwitchComputeDeviceModalContainer />
</div>
<WebsocketContainer />
</div>
);
};
const OscIpAddressContainer = () => {
const { t } = useI18n();
const { currentOscIpAddress, setOscIpAddress } = useAdvancedSettings();
const [input_value, setInputValue] = useState(currentOscIpAddress.data);
const onChangeFunction = (value) => {
setInputValue(value);
};
const saveFunction = () => {
setOscIpAddress(input_value);
};
useEffect(()=> {
if (currentOscIpAddress.state === "pending") return;
setInputValue(currentOscIpAddress.data);
}, [currentOscIpAddress]);
return (
<EntryWithSaveButtonContainer
label={t("config_page.advanced_settings.osc_ip_address.label")}
variable={input_value}
saveFunction={saveFunction}
onChangeFunction={onChangeFunction}
state={currentOscIpAddress.state}
width="14rem"
/>
);
};
const OscPortContainer = () => {
const { t } = useI18n();
const { currentOscPort, setOscPort } = useAdvancedSettings();
const [input_value, setInputValue] = useState(currentOscPort.data);
const onChangeFunction = (value) => {
value = value.replace(/[^0-9]/g, "");
setInputValue(value);
};
const saveFunction = () => {
setOscPort(input_value);
};
useEffect(()=> {
if (currentOscPort.state === "pending") return;
setInputValue(currentOscPort.data);
}, [currentOscPort]);
return (
<EntryWithSaveButtonContainer
label={t("config_page.advanced_settings.osc_port.label")}
variable={input_value}
saveFunction={saveFunction}
onChangeFunction={onChangeFunction}
state={currentOscPort.state}
width="10rem"
/>
);
};
const OpenConfigFolderContainer = () => {
const { t } = useI18n();
const { openFolder_ConfigFile } = useOpenFolder();
return (
<>
<ActionButtonContainer
label={t("config_page.advanced_settings.open_config_filepath.label")}
IconComponent={OpenFolderSvg}
onclickFunction={openFolder_ConfigFile}
/>
</>
);
};
// Duplicate
import { useStore_OpenedQuickSetting } from "@store";
const OpenSwitchComputeDeviceModalContainer = () => {
const { t } = useI18n();
const { updateOpenedQuickSetting } = useStore_OpenedQuickSetting();
const onClickFunction = () => {
updateOpenedQuickSetting("update_software");
};
return (
<>
<ActionButtonContainer
label={t("config_page.advanced_settings.switch_compute_device.label")}
IconComponent={HelpSvg}
onclickFunction={onClickFunction}
/>
</>
);
};
const WebsocketContainer = () => {
return (
<div>
<SectionLabelComponent label="WebSocket" />
<EnableWebsocketContainer />
<WebsocketHostContainer />
<WebsocketPortContainer />
</div>
);
};
const EnableWebsocketContainer = () => {
const { t } = useI18n();
const { currentEnableWebsocket, toggleEnableWebsocket } = useAdvancedSettings();
return (
<CheckboxContainer
label={t("config_page.advanced_settings.enable_websocket.label")}
variable={currentEnableWebsocket}
toggleFunction={toggleEnableWebsocket}
/>
);
};
const WebsocketHostContainer = () => {
const { t } = useI18n();
const { currentWebsocketHost, setWebsocketHost } = useAdvancedSettings();
const [input_value, setInputValue] = useState(currentWebsocketHost.data);
const onChangeFunction = (value) => {
setInputValue(value);
};
const saveFunction = () => {
setWebsocketHost(input_value);
};
useEffect(()=> {
if (currentWebsocketHost.state === "pending") return;
setInputValue(currentWebsocketHost.data);
}, [currentWebsocketHost]);
return (
<EntryWithSaveButtonContainer
label={t("config_page.advanced_settings.websocket_host.label")}
variable={input_value}
saveFunction={saveFunction}
onChangeFunction={onChangeFunction}
state={currentWebsocketHost.state}
width="14rem"
/>
);
};
const WebsocketPortContainer = () => {
const { t } = useI18n();
const { currentWebsocketPort, setWebsocketPort } = useAdvancedSettings();
const [input_value, setInputValue] = useState(currentWebsocketPort.data);
const onChangeFunction = (value) => {
value = value.replace(/[^0-9]/g, "");
setInputValue(value);
};
const saveFunction = () => {
setWebsocketPort(input_value);
};
useEffect(()=> {
if (currentWebsocketPort.state === "pending") return;
setInputValue(currentWebsocketPort.data);
}, [currentWebsocketPort]);
return (
<EntryWithSaveButtonContainer
label={t("config_page.advanced_settings.websocket_port.label")}
variable={input_value}
saveFunction={saveFunction}
onChangeFunction={onChangeFunction}
state={currentWebsocketPort.state}
width="10rem"
/>
);
};

View File

@@ -0,0 +1,5 @@
.container {
display: flex;
gap: 6.4rem;
flex-direction: column;
}

View File

@@ -0,0 +1,236 @@
import clsx from "clsx";
import { useEffect, useState } from "react";
import { useI18n } from "@useI18n";
import styles from "./Appearance.module.scss";
import { ui_configs } from "@ui_configs";
import { useStore_SelectableFontFamilyList } from "@store";
import {
useWindow,
} from "@logics_common";
import {
useAppearance,
} from "@logics_configs";
import {
SliderContainer,
DropdownMenuContainer,
RadioButtonContainer,
CheckboxContainer,
} from "../_templates/Templates";
export const Appearance = () => {
return (
<>
<UiLanguageContainer />
<UiScalingContainer />
<MessageLogUiScalingContainer />
<SendMessageButtonTypeContainer />
<ShowResendButtonContainer />
<FontFamilyContainer />
<TransparencyContainer />
</>
);
};
const UiLanguageContainer = () => {
const { t } = useI18n();
const { currentUiLanguage, setUiLanguage } = useAppearance();
const is_not_en_lang = currentUiLanguage.data !== "en" && currentUiLanguage.data !== undefined;
return (
<RadioButtonContainer
label={is_not_en_lang ? "UI Language" : t("config_page.appearance.ui_language.label")}
desc={is_not_en_lang ? t("config_page.appearance.ui_language.label") : false}
selectFunction={setUiLanguage}
name="ui_language"
options={ui_configs.selectable_ui_languages}
checked_variable={currentUiLanguage}
/>
);
};
const UiScalingContainer = () => {
const { t } = useI18n();
const { currentUiScaling, setUiScaling } = useAppearance();
const { asyncUpdateBreakPoint } = useWindow();
const [ui_ui_scaling, setUiUiScaling] = useState(currentUiScaling.data);
const onchangeFunction = (value) => {
setUiUiScaling(value);
};
const onchangeCommittedFunction = (value) => {
setUiScaling(value);
};
useEffect(() => {
setUiUiScaling(currentUiScaling.data);
asyncUpdateBreakPoint();
}, [currentUiScaling.data]);
// [Duplicated]
const createMarks = (min, max) => {
const marks = [];
for (let value = min; value <= max; value += 10) {
const label = ([50,70,90,110,130,150,170,190].includes(value)) ? "" : value;
marks.push({ value, label: `${label}` });
}
return marks;
};
const marks = createMarks(40, 200);
return (
<SliderContainer
label={t("config_page.appearance.ui_size.label") + " (%)"}
min="40"
max="200"
onchangeCommittedFunction={onchangeCommittedFunction}
onchangeFunction={onchangeFunction}
variable={ui_ui_scaling}
marks={marks}
step={null}
track={false}
/>
);
};
export const MessageLogUiScalingContainer = () => {
const { t } = useI18n();
const { currentMessageLogUiScaling, setMessageLogUiScaling } = useAppearance();
const [ui_message_log_ui_scaling, setUiMessageLogUiScaling] = useState(currentMessageLogUiScaling.data);
const onchangeFunction = (value) => {
setUiMessageLogUiScaling(value);
};
const onchangeCommittedFunction = (value) => {
setMessageLogUiScaling(value);
};
useEffect(() => {
setUiMessageLogUiScaling(currentMessageLogUiScaling.data);
}, [currentMessageLogUiScaling.data]);
// [Duplicated]
const createMarks = (min, max) => {
const marks = [];
for (let value = min; value <= max; value += 10) {
const label = ([50,70,90,110,130,150,170,190].includes(value)) ? "" : value;
marks.push({ value, label: `${label}` });
}
return marks;
};
const marks = createMarks(40, 200);
return (
<SliderContainer
label={t("config_page.appearance.textbox_ui_size.label") + " (%)"}
min="40"
max="200"
onchangeCommittedFunction={onchangeCommittedFunction}
onchangeFunction={onchangeFunction}
variable={ui_message_log_ui_scaling}
marks={marks}
step={null}
track={false}
/>
);
};
const SendMessageButtonTypeContainer = () => {
const { t } = useI18n();
const { currentSendMessageButtonType, setSendMessageButtonType } = useAppearance();
return (
<RadioButtonContainer
label={t("config_page.appearance.send_message_button_type.label")}
selectFunction={setSendMessageButtonType}
name="send_message_button_type"
options={[
{ id: "hide", label: t("config_page.appearance.send_message_button_type.hide") },
{ id: "show", label: t("config_page.appearance.send_message_button_type.show") },
{ id: "show_and_disable_enter_key", label: t("config_page.appearance.send_message_button_type.show_and_disable_enter_key") },
]}
checked_variable={currentSendMessageButtonType}
column={true}
/>
);
};
const ShowResendButtonContainer = () => {
const { t } = useI18n();
const { currentShowResendButton, toggleShowResendButton } = useAppearance();
return (
<CheckboxContainer
label={t("config_page.appearance.show_resend_button.label")}
desc={t("config_page.appearance.show_resend_button.desc")}
variable={currentShowResendButton}
toggleFunction={toggleShowResendButton}
/>
);
};
const FontFamilyContainer = () => {
const { t } = useI18n();
const { currentSelectedFontFamily, setSelectedFontFamily } = useAppearance();
const selectFunction = (selected_data) => {
setSelectedFontFamily(selected_data.selected_id);
};
const { currentSelectableFontFamilyList } = useStore_SelectableFontFamilyList();
return (
<DropdownMenuContainer
dropdown_id="font_family"
label={t("config_page.appearance.font_family.label")}
selected_id={currentSelectedFontFamily.data}
list={currentSelectableFontFamilyList.data}
selectFunction={selectFunction}
state={currentSelectedFontFamily.state}
/>
);
};
const TransparencyContainer = () => {
const { t } = useI18n();
const { currentTransparency, setTransparency } = useAppearance();
const [ui_message_log_ui_scaling, setUiTransparency] = useState(currentTransparency.data);
const onchangeFunction = (value) => {
setUiTransparency(value);
};
const onchangeCommittedFunction = (value) => {
setTransparency(value);
};
useEffect(() => {
setUiTransparency(currentTransparency.data);
}, [currentTransparency.data]);
// [Duplicated]
const createMarks = (min, max) => {
const marks = [];
for (let value = min; value <= max; value += 10) {
marks.push({ value, label: `${value}` });
}
return marks;
};
const marks = createMarks(40, 100);
return (
<SliderContainer
label={t("config_page.appearance.transparency.label") + " (%)"}
min="40"
max="100"
onchangeCommittedFunction={onchangeCommittedFunction}
onchangeFunction={onchangeFunction}
variable={ui_message_log_ui_scaling}
marks={marks}
step={null}
track={false}
/>
);
};

View File

@@ -0,0 +1 @@
@import "@scss_mixins";

View File

@@ -0,0 +1,225 @@
import { useI18n } from "@useI18n";
import styles from "./Device.module.scss";
import clsx from "clsx";
import { useStore_IsBreakPoint } from "@store";
import { ui_configs } from "@ui_configs";
import {
useDevice,
} from "@logics_configs";
import {
useOnMouseLeaveDropdownMenu,
} from "../_templates/Templates";
import {
LabelComponent,
DropdownMenu,
ThresholdComponent,
SwitchBox,
} from "../_components";
export const Device = () => {
return (
<>
<Mic_Container />
<Speaker_Container />
</>
);
};
const Mic_Container = () => {
const { t } = useI18n();
const {
currentEnableAutoMicSelect,
toggleEnableAutoMicSelect,
currentMicDeviceList,
currentMicHostList,
currentSelectedMicHost,
setSelectedMicHost,
currentSelectedMicDevice,
setSelectedMicDevice,
currentEnableAutomaticMicThreshold,
toggleEnableAutomaticMicThreshold,
} = useDevice();
const { onMouseLeaveFunction } = useOnMouseLeaveDropdownMenu();
const selectFunction_host = (selected_data) => {
setSelectedMicHost(selected_data.selected_id);
};
const selectFunction_device = (selected_data) => {
setSelectedMicDevice(selected_data.selected_id);
};
// [Fix me] currentEnableAutoMicSelect.data === "pending"; ? not currentEnableAutoMicSelect.state === "pending"; ??(.state)
const is_disabled_selector = currentEnableAutoMicSelect.data === true || currentEnableAutoMicSelect.data === "pending";
const getLabels = () => {
if (currentEnableAutomaticMicThreshold.data === true) {
return {
label: t("config_page.device.mic_dynamic_energy_threshold.label_for_automatic"),
desc: t("config_page.device.mic_dynamic_energy_threshold.desc_for_automatic"),
};
} else {
return {
label: t("config_page.device.mic_dynamic_energy_threshold.label_for_manual"),
desc: t("config_page.device.mic_dynamic_energy_threshold.desc_for_manual"),
};
}
};
const { currentIsBreakPoint } = useStore_IsBreakPoint();
const device_container_class = clsx(styles.device_container, {
[styles.is_break_point]: currentIsBreakPoint.data,
});
return (
<div className={styles.mic_container}>
<div className={device_container_class} onMouseLeave={onMouseLeaveFunction}>
<LabelComponent label={t("config_page.device.mic_host_device.label")} />
<div className={styles.device_contents}>
<div className={styles.device_auto_select_wrapper}>
<p className={styles.device_secondary_label}>{t("config_page.device.label_auto_select")}</p>
<SwitchBox
variable={currentEnableAutoMicSelect}
toggleFunction={toggleEnableAutoMicSelect}
/>
</div>
<div className={styles.device_dropdown_wrapper}>
<div className={styles.device_dropdown}>
<p className={styles.device_secondary_label}>{t("config_page.device.label_host")}</p>
<DropdownMenu
dropdown_id="mic_host"
selected_id={currentSelectedMicHost.data}
list={currentMicHostList.data}
selectFunction={selectFunction_host}
state={currentSelectedMicHost.state}
style={{ maxWidth: "20rem", minWidth: "10rem" }}
is_disabled={is_disabled_selector}
/>
</div>
<div className={styles.device_dropdown}>
<p className={styles.device_secondary_label}>{t("config_page.device.label_device")}</p>
<DropdownMenu
dropdown_id="mic_device"
selected_id={currentSelectedMicDevice.data}
list={currentMicDeviceList.data}
selectFunction={selectFunction_device}
state={currentSelectedMicDevice.state}
is_disabled={is_disabled_selector}
/>
</div>
</div>
</div>
</div>
<div className={styles.threshold_container}>
<div className={styles.threshold_switch_section}>
<LabelComponent {...getLabels()} />
<SwitchBox
variable={currentEnableAutomaticMicThreshold}
toggleFunction={toggleEnableAutomaticMicThreshold}
/>
</div>
<div className={styles.threshold_section}>
<ThresholdComponent
id="mic_threshold"
min={ui_configs.mic_threshold_min}
max={ui_configs.mic_threshold_max}
/>
</div>
</div>
</div>
);
};
const Speaker_Container = () => {
const { t } = useI18n();
const {
currentEnableAutoSpeakerSelect,
toggleEnableAutoSpeakerSelect,
currentSpeakerDeviceList,
currentSelectedSpeakerDevice,
setSelectedSpeakerDevice,
currentEnableAutomaticSpeakerThreshold,
toggleEnableAutomaticSpeakerThreshold,
} = useDevice();
const { onMouseLeaveFunction } = useOnMouseLeaveDropdownMenu();
const selectFunction = (selected_data) => {
setSelectedSpeakerDevice(selected_data.selected_id);
};
const is_disabled_selector = currentEnableAutoSpeakerSelect.data === true || currentEnableAutoSpeakerSelect.data === "pending";
const getLabels = () => {
if (currentEnableAutomaticSpeakerThreshold.data === true) {
return {
label: t("config_page.device.speaker_dynamic_energy_threshold.label_for_automatic"),
desc: t("config_page.device.speaker_dynamic_energy_threshold.desc_for_automatic"),
};
} else {
return {
label: t("config_page.device.speaker_dynamic_energy_threshold.label_for_manual"),
desc: t("config_page.device.speaker_dynamic_energy_threshold.desc_for_manual"),
};
}
};
const { currentIsBreakPoint } = useStore_IsBreakPoint();
const device_container_class = clsx(styles.device_container, {
[styles.is_break_point]: currentIsBreakPoint.data,
});
return (
<div className={styles.speaker_container}>
<div className={device_container_class} onMouseLeave={onMouseLeaveFunction}>
<LabelComponent label={t("config_page.device.speaker_device.label")} />
<div className={styles.device_contents}>
<div className={styles.device_auto_select_wrapper}>
<p className={styles.device_secondary_label}>{t("config_page.device.label_auto_select")}</p>
<SwitchBox
variable={currentEnableAutoSpeakerSelect}
toggleFunction={toggleEnableAutoSpeakerSelect}
/>
</div>
<div className={styles.device_dropdown}>
<p className={styles.device_secondary_label}>{t("config_page.device.label_device")}</p>
<DropdownMenu
dropdown_id="speaker_device"
label={t("config_page.device.speaker_device.label")}
selected_id={currentSelectedSpeakerDevice.data}
list={currentSpeakerDeviceList.data}
selectFunction={selectFunction}
state={currentSelectedSpeakerDevice.state}
is_disabled={is_disabled_selector}
/>
</div>
</div>
</div>
<div className={styles.threshold_container}>
<div className={styles.threshold_switch_section}>
<LabelComponent {...getLabels()}/>
<SwitchBox
variable={currentEnableAutomaticSpeakerThreshold}
toggleFunction={toggleEnableAutomaticSpeakerThreshold}
/>
</div>
<div className={styles.threshold_section}>
<ThresholdComponent
id="speaker_threshold"
min={ui_configs.speaker_threshold_min}
max={ui_configs.speaker_threshold_max}
/>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,105 @@
.mic_container {
display: flex;
flex-direction: column;
border-bottom: solid 0.1rem var(--dark_800_color);
padding-bottom: 1rem;
}
.speaker_container {
padding-top: 0rem;
}
.device_container {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
padding: 2rem;
margin-bottom: 0rem;
&.is_break_point {
flex-direction: column;
gap: 2rem;
align-items: start;
& .device_contents {
display: flex;
width: 100%;
justify-content: space-between;
padding-left: 0rem;
}
}
}
.threshold_container {
padding: 2rem;
}
.threshold_container {
display: flex;
width: 100%;
flex-direction: column;
justify-content: space-between;
align-items: center;
gap: 2rem;
}
.threshold_switch_section {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.threshold_section {
width: 100%;
}
.device_label {
font-size: 1.8rem;
}
.device_contents {
display: flex;
width: 100%;
justify-content: end;
padding-left: 2rem;
gap: 2rem;
}
.device_auto_select_wrapper {
display: flex;
flex-direction: column;
gap: 1.2rem;
justify-content: center;
align-items: center;
}
.device_dropdown_wrapper {
display: flex;
flex-direction: row;
gap: 2.8rem;
}
.device_dropdown {
display: flex;
flex-direction: column;
gap: 0.6rem;
white-space: nowrap;
max-width: 24rem;
&.is_disabled {
pointer-events: none;
}
}
.device_secondary_label {
padding-left: 0.2rem;
padding-right: 0.4rem;
font-size: 1.4rem;
color: var(--dark_500_color);
white-space: nowrap;
}

View File

@@ -0,0 +1,49 @@
import { useHotkeys } from "@logics_configs";
import styles from "./Hotkeys.module.scss";
import { HotkeysEntryContainer } from "../_templates/Templates";
import { useI18n } from "@useI18n";
export const Hotkeys = () => {
return (
<div className={styles.container}>
<HotkeysBoxContainer />
</div>
);
};
const HotkeysBoxContainer = () => {
const { t } = useI18n();
const { currentHotkeys, setHotkeys } = useHotkeys();
return (
<div className={styles.container}>
<HotkeysEntryContainer
label={t("config_page.hotkeys.toggle_vrct_visibility.label")}
hotkey_id="toggle_vrct_visibility"
value={currentHotkeys.data}
state={currentHotkeys.state}
setHotkeys={setHotkeys}
/>
<HotkeysEntryContainer
label={t("config_page.hotkeys.toggle_translation.label", {translation: t("main_page.translation")})}
hotkey_id="toggle_translation"
value={currentHotkeys.data}
state={currentHotkeys.state}
setHotkeys={setHotkeys}
/>
<HotkeysEntryContainer
label={t("config_page.hotkeys.toggle_transcription_send.label", {transcription_send: t("main_page.transcription_send")})}
hotkey_id="toggle_transcription_send"
value={currentHotkeys.data}
state={currentHotkeys.state}
setHotkeys={setHotkeys}
/>
<HotkeysEntryContainer
label={t("config_page.hotkeys.toggle_transcription_receive.label", {transcription_receive: t("main_page.transcription_receive")})}
hotkey_id="toggle_transcription_receive"
value={currentHotkeys.data}
state={currentHotkeys.state}
setHotkeys={setHotkeys}
/>
</div>
);
};

View File

@@ -0,0 +1,5 @@
.container {
display: flex;
// gap: 6.4rem;
flex-direction: column;
}

View File

@@ -0,0 +1,11 @@
export { Device } from "./device/Device";
export { Appearance, MessageLogUiScalingContainer } from "./appearance/Appearance";
export { Translation } from "./translation/Translation";
export { Transcription } from "./transcription/Transcription";
export { Others, VrcMicMuteSyncContainer } from "./others/Others";
export { AdvancedSettings } from "./advanced_settings/AdvancedSettings";
export { Vr } from "./vr/Vr";
export { Hotkeys } from "./hotkeys/Hotkeys";
export { Plugins } from "./plugins/Plugins";
export { AboutVrct } from "./about_vrct/AboutVrct";
export { Supporters } from "./supporters/Supporters";

View File

@@ -0,0 +1,240 @@
import { useI18n } from "@useI18n";
import styles from "./Others.module.scss";
import { useOpenFolder } from "@logics_common";
import {
useOthers,
} from "@logics_configs";
import {
CheckboxContainer,
MessageFormatContainer,
} from "../_templates/Templates";
import {
LabelComponent,
ActionButton,
SectionLabelComponent,
} from "../_components";
import { Checkbox } from "@common_components";
import OpenFolderSvg from "@images/open_folder.svg?react";
export const Others = () => {
const { t } = useI18n();
return (
<div className={styles.container}>
<div>
<AutoClearMessageInputBoxContainer />
<SendOnlyTranslatedMessagesContainer />
<AutoExportMessageLogsContainer />
<VrcMicMuteSyncContainer />
<SendMessageToVrcContainer />
</div>
<div>
<SectionLabelComponent label={t("config_page.others.section_label_sounds")} />
<EnableNotificationVrcSfxContainer />
</div>
<div>
<SectionLabelComponent label="Speaker2Chatbox" />
<SendReceivedMessageToVrcContainer />
</div>
<div>
<SectionLabelComponent label={t("config_page.others.section_label_message_formats")} />
<SendMessageFormatPartsContainer />
<ReceivedMessageFormatPartsContainer />
</div>
<div>
<ConvertMessageToRomajiContainer />
<ConvertMessageToHiraganaContainer />
</div>
</div>
);
};
const AutoClearMessageInputBoxContainer = () => {
const { t } = useI18n();
const { currentEnableAutoClearMessageInputBox, toggleEnableAutoClearMessageInputBox } = useOthers();
return (
<CheckboxContainer
label={t("config_page.others.auto_clear_the_message_box.label")}
variable={currentEnableAutoClearMessageInputBox}
toggleFunction={toggleEnableAutoClearMessageInputBox}
/>
);
};
const SendOnlyTranslatedMessagesContainer = () => {
const { t } = useI18n();
const { currentEnableSendOnlyTranslatedMessages, toggleEnableSendOnlyTranslatedMessages } = useOthers();
return (
<CheckboxContainer
label={t("config_page.others.send_only_translated_messages.label")}
variable={currentEnableSendOnlyTranslatedMessages}
toggleFunction={toggleEnableSendOnlyTranslatedMessages}
/>
);
};
const AutoExportMessageLogsContainer = () => {
const { t } = useI18n();
const { currentEnableAutoExportMessageLogs, toggleEnableAutoExportMessageLogs } = useOthers();
const { openFolder_MessageLogs } = useOpenFolder();
return (
<div className={styles.auto_export_message_logs_container}>
<LabelComponent
label={t("config_page.others.auto_export_message_logs.label")}
desc={t("config_page.others.auto_export_message_logs.desc")}
/>
<div className={styles.auto_export_message_logs_switch_section_container}>
<ActionButton
IconComponent={OpenFolderSvg}
onclickFunction={openFolder_MessageLogs}
/>
<Checkbox
variable={currentEnableAutoExportMessageLogs}
toggleFunction={toggleEnableAutoExportMessageLogs}
/>
</div>
</div>
);
};
export const VrcMicMuteSyncContainer = () => {
const { t } = useI18n();
const { currentEnableVrcMicMuteSync, toggleEnableVrcMicMuteSync } = useOthers();
const variable = {
state: currentEnableVrcMicMuteSync.state,
data: currentEnableVrcMicMuteSync.data.is_enabled,
};
return (
<CheckboxContainer
label={t("config_page.others.vrc_mic_mute_sync.label")}
desc={t("config_page.others.vrc_mic_mute_sync.desc")}
variable={variable}
is_available={currentEnableVrcMicMuteSync.data.is_available}
toggleFunction={toggleEnableVrcMicMuteSync}
/>
);
};
const SendMessageToVrcContainer = () => {
const { t } = useI18n();
const { currentEnableSendMessageToVrc, toggleEnableSendMessageToVrc } = useOthers();
return (
<CheckboxContainer
label={t("config_page.others.send_message_to_vrc.label")}
desc={t("config_page.others.send_message_to_vrc.desc")}
variable={currentEnableSendMessageToVrc}
toggleFunction={toggleEnableSendMessageToVrc}
/>
);
};
const EnableNotificationVrcSfxContainer = () => {
const { t } = useI18n();
const { currentEnableNotificationVrcSfx, toggleEnableNotificationVrcSfx } = useOthers();
return (
<CheckboxContainer
label={t("config_page.others.notification_vrc_sfx.label")}
desc={t("config_page.others.notification_vrc_sfx.desc")}
variable={currentEnableNotificationVrcSfx}
toggleFunction={toggleEnableNotificationVrcSfx}
/>
);
};
const SendReceivedMessageToVrcContainer = () => {
const { t } = useI18n();
const { currentEnableSendReceivedMessageToVrc, toggleEnableSendReceivedMessageToVrc } = useOthers();
return (
<CheckboxContainer
label={t("config_page.others.send_received_message_to_vrc.label")}
desc={t("config_page.others.send_received_message_to_vrc.desc")}
variable={currentEnableSendReceivedMessageToVrc}
toggleFunction={toggleEnableSendReceivedMessageToVrc}
/>
);
};
const SendMessageFormatPartsContainer = () => {
const { t } = useI18n();
const {
currentSendMessageFormatParts,
setSendMessageFormatParts,
} = useOthers();
return (
<MessageFormatContainer
label={t("config_page.others.send_message_format.label")}
desc={t("config_page.others.send_message_format.desc")}
variable={currentSendMessageFormatParts}
setFunction={setSendMessageFormatParts}
format_id="send"
/>
);
};
const ReceivedMessageFormatPartsContainer = () => {
const { t } = useI18n();
const {
currentReceivedMessageFormatParts,
setReceivedMessageFormatParts,
} = useOthers();
return (
<MessageFormatContainer
label={t("config_page.others.received_message_format.label")}
desc={t("config_page.others.received_message_format.desc")}
variable={currentReceivedMessageFormatParts}
setFunction={setReceivedMessageFormatParts}
format_id="received"
/>
);
};
const ConvertMessageToRomajiContainer = () => {
const { t } = useI18n();
const { currentConvertMessageToRomaji, toggleConvertMessageToRomaji } = useOthers();
const desc_1 = t("config_page.others.common_convert_message_hiragana_romaji.desc_1");
const desc_2 = t("config_page.others.common_convert_message_hiragana_romaji.desc_2");
const desc_romaji = t(
"config_page.others.convert_message_to_romaji.desc",
{ convert_message_to_hiragana: t("config_page.others.convert_message_to_hiragana.label") }
);
const desc = [desc_1, desc_2, desc_romaji].join("\n");
return (
<CheckboxContainer
label={t("config_page.others.convert_message_to_romaji.label")}
desc={desc}
variable={currentConvertMessageToRomaji}
toggleFunction={toggleConvertMessageToRomaji}
/>
);
};
const ConvertMessageToHiraganaContainer = () => {
const { t } = useI18n();
const { currentConvertMessageToHiragana, toggleConvertMessageToHiragana } = useOthers();
const desc_1 = t("config_page.others.common_convert_message_hiragana_romaji.desc_1");
const desc_2 = t("config_page.others.common_convert_message_hiragana_romaji.desc_2");
const desc = [desc_1, desc_2].join("\n");
return (
<CheckboxContainer
label={t("config_page.others.convert_message_to_hiragana.label")}
desc={desc}
variable={currentConvertMessageToHiragana}
toggleFunction={toggleConvertMessageToHiragana}
/>
);
};

View File

@@ -0,0 +1,28 @@
.container {
display: flex;
gap: 6.4rem;
flex-direction: column;
}
.auto_export_message_logs_container {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
padding: 2rem;
align-items: center;
gap: 2rem;
&.flex_column {
flex-direction: column;
}
border-bottom: solid 0.1rem var(--dark_800_color);
}
.auto_export_message_logs_switch_section_container {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
align-items: center;
gap: 2rem;
}

View File

@@ -0,0 +1,128 @@
import { useEffect, useRef } from "react";
import { useI18n } from "@useI18n";
import { usePlugins } from "@logics_configs";
import styles from "./Plugins.module.scss";
import { PluginsControlComponent } from "./plugins_control_component/PluginsControlComponent";
import { useNotificationStatus } from "@logics_common";
import { HomepageLinkButton } from "@common_components";
export const Plugins = () => {
const {
asyncFetchPluginsInfo,
} = usePlugins();
const hasRunRef = useRef(false);
useEffect(() => {
if (!hasRunRef.current) {
asyncFetchPluginsInfo();
}
return () => hasRunRef.current = true;
}, []);
return (
<div className={styles.container}>
<PluginDownloadContainer />
</div>
);
};
const PluginDownloadContainer = () => {
const { t, i18n } = useI18n();
const {
downloadAndExtractPlugin,
currentPluginsData,
currentSavedPluginsStatus,
toggleSavedPluginsStatus,
handlePendingPlugin,
currentFetchedPluginsInfo,
} = usePlugins();
const { showNotification_Success, showNotification_Error } = useNotificationStatus();
// ダウンロード開始時の状態更新処理
const downloadStartFunction = async (target_plugin_id) => {
handlePendingPlugin(target_plugin_id, true);
showNotification_Success(t("plugin_notifications.downloading"));
const target_plugin_info = currentPluginsData.data.find(
(d) => d.plugin_id === target_plugin_id
);
downloadAndExtractPlugin(target_plugin_info).then(() => {
handlePendingPlugin(target_plugin_id, false);
showNotification_Success(t("plugin_notifications.downloaded_success"));
}).catch(error => {
console.error(error);
showNotification_Error(t("plugin_notifications.downloaded_error"));
});
};
// プラグインのオンオフ切り替え処理
const toggleFunction = (target_plugin_id) => {
toggleSavedPluginsStatus(target_plugin_id);
};
const variable_state = currentSavedPluginsStatus.state;
const filtered_plugins_data = currentPluginsData.data.filter(plugin => !plugin.is_outdated)
// plugin_id で ABC 順にソート
const sorted_plugins_data = filtered_plugins_data.sort((a, b) =>
a.plugin_id.localeCompare(b.plugin_id)
);
// Duplicate
const is_failed_to_fetch = currentFetchedPluginsInfo.state === "error";
const is_fetching = currentFetchedPluginsInfo.state === "pending";
return (
<div className={styles.plugins_list_container}>
{is_failed_to_fetch && <p>Failed to fetch plugins data</p>}
{is_fetching && <p>Fetching plugins data...</p>}
{sorted_plugins_data.map((plugin) => {
const target_info = plugin.is_downloaded
? plugin.downloaded_plugin_info
: plugin.latest_plugin_info;
const target_locale = target_info.locales && target_info.locales[i18n.language]
? target_info.locales[i18n.language]
: {
title: target_info.title,
desc: target_info.desc || null,
};
const homepage_link = plugin.latest_plugin_info?.homepage_link;
return (
<div key={plugin.plugin_id} className={styles.plugin_wrapper}>
<div className={styles.labels_wrapper}>
<p className={styles.title}>
{target_locale.title}
</p>
<p className={styles.desc}>
{target_locale.desc}
</p>
{/* <p className={styles.plugin_id}>{plugin.plugin_id}</p> */}
{homepage_link && <HomepageLinkButton homepage_link={homepage_link}/>
}
</div>
<div className={styles.plugin_info_wrapper}>
{plugin.is_error ? (
<div>
<p style={{ color: "red" }}>{t(`plugin_notifications.${plugin.error_message_type}`)}</p>
<p style={{ color: "red" }}>plugin_version: {plugin.downloaded_plugin_info.plugin_version}</p>
<p style={{ color: "red" }}>min_supported_vrct_version: {plugin.downloaded_plugin_info.min_supported_vrct_version}</p>
<p style={{ color: "red" }}>max_supported_vrct_version: {plugin.downloaded_plugin_info.max_supported_vrct_version}</p>
</div>
) : (
<PluginsControlComponent
variable_state={variable_state}
toggleFunction={toggleFunction}
downloadStartFunction={downloadStartFunction}
plugin_status={plugin}
/>
)}
</div>
</div>
);
})}
</div>
);
};

View File

@@ -0,0 +1,59 @@
.container {
display: flex;
gap: 6.4rem;
flex-direction: column;
}
.plugins_list_container {
display: flex;
flex-direction: column;
align-items: center;
}
.plugin_wrapper {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 2rem;
gap: 2rem;
&:not(:last-child) {
border-bottom: 0.1rem solid var(--dark_750_color);
}
}
.labels_wrapper {
display: flex;
flex-direction: column;
gap: 0.4rem;
max-width: 50%;
flex-shrink: 0;
}
.plugin_info_wrapper {
display: flex;
justify-content: end;
align-items: center;
}
.title {
font-size: 1.6rem;
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.desc {
font-size: 1.4rem;
width: 100%;
overflow: hidden;
color: var(--dark_500_color);
}
// .plugin_id {
// font-size: 1rem;
// color: var(--dark_600_color);
// width: 100%;
// overflow: hidden;
// white-space: nowrap;
// text-overflow: ellipsis;
// }

View File

@@ -0,0 +1,155 @@
import { SwitchBox } from "../../_components";
import { _DownloadButton } from "../../_components/_atoms/_download_button/_DownloadButton";
import styles from "./PluginsControlComponent.module.scss";
import { useI18n } from "@useI18n";
export const PluginsControlComponent = ({
variable_state,
plugin_status,
toggleFunction,
downloadStartFunction,
}) => {
const { t } = useI18n();
const option = {
id: plugin_status.plugin_id,
is_pending: plugin_status.is_pending,
is_downloaded: plugin_status.is_downloaded,
data: plugin_status.is_enabled,
update_button: plugin_status.is_downloaded && plugin_status.is_latest_version_available,
state: variable_state,
progress: null,
};
const downloaded_version = plugin_status.downloaded_plugin_info?.plugin_version;
const latest_version = plugin_status.latest_plugin_info?.plugin_version;
const downloaded_version_label = t("config_page.plugins.downloaded_version",
{ downloaded_version: downloaded_version }
);
const latest_version_label = t("config_page.plugins.latest_version",
{ latest_version: latest_version }
);
if (plugin_status.is_downloaded) {
return (
<DownloadedPluginControl
option={option}
plugin_status={plugin_status}
toggleFunction={toggleFunction}
downloadStartFunction={downloadStartFunction}
downloaded_version_label={downloaded_version_label}
latest_version_label={latest_version_label}
/>
);
} else {
return (
<NotDownloadedPluginControl
option={option}
plugin_status={plugin_status}
downloadStartFunction={downloadStartFunction}
downloaded_version_label={downloaded_version_label}
latest_version_label={latest_version_label}
/>
);
}
};
const DownloadedPluginControl = ({
option,
plugin_status,
toggleFunction,
downloadStartFunction,
downloaded_version_label,
latest_version_label,
}) => {
const { t } = useI18n();
const togglePlugin = () => {
toggleFunction(plugin_status.plugin_id);
};
if (!plugin_status.downloaded_plugin_info.is_plugin_supported) {
if (plugin_status.is_latest_version_available) {
return (
<div className={styles.container}>
<p>{downloaded_version_label}</p>
<p>{latest_version_label}</p>
<p>{t("config_page.plugins.available_after_updating")}</p>
<_DownloadButton option={option} downloadStartFunction={downloadStartFunction} />
</div>
);
}
return (
<div className={styles.container}>
<p>{t("config_page.plugins.unavailable_downloaded")}</p>
</div>
);
} else if (plugin_status.is_outdated) {
return (
<div className={styles.container}>
<p>{t("config_page.plugins.no_latest_info")}</p>
<SwitchBox variable={option} toggleFunction={togglePlugin} />
</div>
);
} else if (plugin_status.is_latest_version_already) {
return (
<div className={styles.container}>
<p>{latest_version_label}</p>
<p>{t("config_page.plugins.using_latest_version")}</p>
<SwitchBox variable={option} toggleFunction={togglePlugin} />
</div>
);
} else if (plugin_status.is_latest_version_available) {
return (
<div className={styles.container}>
<p>{latest_version_label}</p>
<p>{t("config_page.plugins.available_latest_version")}</p>
<_DownloadButton option={option} downloadStartFunction={downloadStartFunction} />
<SwitchBox variable={option} toggleFunction={togglePlugin} />
</div>
);
} else {
return (
<div className={styles.container}>
<p>{t("config_page.plugins.available_latest_version")}</p>
<SwitchBox variable={option} toggleFunction={togglePlugin} />
</div>
);
}
};
const NotDownloadedPluginControl = ({
option,
plugin_status,
downloadStartFunction,
downloaded_version_label,
latest_version_label,
}) => {
const { t } = useI18n();
if (plugin_status.is_latest_version_available) {
return (
<div className={styles.container}>
<p>{latest_version_label}</p>
<_DownloadButton option={option} downloadStartFunction={downloadStartFunction} />
</div>
);
} else if (plugin_status.latest_plugin_info?.is_plugin_supported_latest_vrct) {
return (
<div className={styles.container}>
<p>{latest_version_label}</p>
<p>{t("config_page.plugins.available_in_latest_vrct_version")}</p>
</div>
);
} else {
return (
<div className={styles.container}>
<p>{latest_version_label}</p>
<p>{t("config_page.plugins.unavailable_not_downloaded")}</p>
</div>
);
}
};

View File

@@ -0,0 +1,11 @@
.container {
display: flex;
justify-content: center;
align-items: center;
gap: 2rem;
}
.unavailable_text {
padding: 1rem;
font-size: 1.2rem;
}

View File

@@ -0,0 +1,22 @@
import styles from "./Supporters.module.scss";
import { SupportUsContainer } from "./support_us_container/SupportUsContainer";
import { SupportersContainer } from "./supporters_container/SupportersContainer";
import { useSupporters } from "@logics_configs";
import { useEffect } from "react";
export const Supporters = () => {
const { asyncFetchSupportersData } = useSupporters();
useEffect(() => {
asyncFetchSupportersData();
}, []);
return (
<div className={styles.container}>
<SupportUsContainer />
<div className={styles.supportersWrapper}>
<SupportersContainer />
</div>
</div>
);
};

View File

@@ -0,0 +1,21 @@
.container {
display: flex;
gap: 3.2rem;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
}
.supportersWrapper {
opacity: 0;
animation: fadeIn 0.8s ease-in-out 1.6s forwards;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

View File

@@ -0,0 +1,48 @@
import top_img from "@images/supporters/patreon_1600x400px.png";
import fanbox_logo from "@images/supporters/fanbox_logo.png";
import kofi_logo from "@images/supporters/kofi_logo.png";
import patreon_logo from "@images/supporters/patreon_logo.png";
import styles from "./SupportUsContainer.module.scss";
import clsx from "clsx";
export const SupportUsContainer = () => {
return (
<div id="support_us_container" className={styles.support_us_container}>
<img className={styles.top_img} src={top_img} />
<div className={styles.support_buttons_wrapper}>
<div className={styles.support_us_button_wrapper}>
<a className={styles.support_button} href="https://vrct-dev.fanbox.cc" target="_blank" rel="noreferrer">
<img
src={fanbox_logo}
className={clsx(styles.support_img, styles.fanbox_logo)}
/>
<div className={styles.spiral_top}></div>
<div className={styles.spiral_bottom}></div>
</a>
<a className={styles.support_button} href="https://ko-fi.com/vrct_dev" target="_blank" rel="noreferrer">
<img
src={kofi_logo}
className={clsx(styles.support_img, styles.kofi_logo)}
/>
<div className={styles.spiral_top}></div>
<div className={styles.spiral_bottom}></div>
</a>
<a className={styles.support_button} href="https://www.patreon.com/vrct_dev" target="_blank" rel="noreferrer">
<img
src={patreon_logo}
className={clsx(styles.support_img, styles.patreon_logo)}
/>
<div className={styles.spiral_top}></div>
<div className={styles.spiral_bottom}></div>
</a>
</div>
<div className={styles.lines_container}>
<div className={styles.line_basic}></div>
<div className={styles.line_fuwa}></div>
<div className={styles.line_mochi}></div>
<div className={styles.line_mogu}></div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,220 @@
$progress_ease: cubic-bezier(0, 1, 0.75, 1);
// Duplicated
@keyframes revealTopImg {
0% {
clip-path: inset(0 50% 0 50%);
opacity: 0;
}
100% {
clip-path: inset(0 0 0 0);
opacity: 1;
}
}
@keyframes bounceIn {
0% {
transform: translateY(100%);
opacity: 0;
}
60% {
transform: translateY(-10%);
opacity: 1;
}
80% {
transform: translateY(10%);
opacity: 1;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
@keyframes expandWidth {
0% {
transform: scaleX(0);
}
100% {
transform: scaleX(1);
}
}
.support_us_container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1.4rem;
width: 100%;
}
.support_buttons_wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1.2rem;
}
.top_img {
width: 100%;
animation: revealTopImg 0.8s ease forwards;
}
.lines_container {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
gap: 3.6rem;
}
.line_basic,
.line_fuwa,
.line_mochi,
.line_mogu {
width: 8.6rem;
height: 0.2rem;
transform: scaleX(0);
transform-origin: left center;
}
.line_basic {
background-color: var(--dark_800_color);
animation: expandWidth 1s $progress_ease 0.4s forwards;
}
.line_fuwa {
background-color: var(--supporters_color_fuwa);
animation: expandWidth 1s $progress_ease 0.6s forwards;
}
.line_mochi {
background-color: var(--received_300_color);
animation: expandWidth 1s $progress_ease 0.8s forwards;
}
.line_mogu {
background-color: var(--dark_basic_text_color);
animation: expandWidth 1s $progress_ease 1s forwards;
}
.support_us_button_wrapper {
display: flex;
justify-content: center;
align-items: center;
column-gap: 3.6rem;
flex-wrap: wrap;
}
.support_button {
position: relative;
padding: 1.2rem 1.6rem;
opacity: 0;
transform: translateY(100%);
}
.support_button:nth-child(1) {
animation: bounceIn 0.4s ease-out 1.2s forwards;
}
.support_button:nth-child(2) {
animation: bounceIn 0.4s ease-out 1.4s forwards;
}
.support_button:nth-child(3) {
animation: bounceIn 0.4s ease-out 1.6s forwards;
}
.support_img {
&.fanbox_logo {
height: 1.8rem;
}
&.kofi_logo, &.patreon_logo {
height: 2.2rem;
}
}
.spiral_top::before,
.spiral_top::after,
.spiral_bottom::before,
.spiral_bottom::after {
content: "";
position: absolute;
transition-duration: 0.3s;
}
.spiral_top::before {
background: var(--dark_800_color);
box-shadow: 0 0 0.4rem 0 var(--dark_800_color);
}
.spiral_top::after {
background: var(--supporters_color_fuwa);
box-shadow: 0 0 0.4rem 0 var(--supporters_color_fuwa);
}
.spiral_bottom::before {
background: var(--received_300_color);
box-shadow: 0 0 0.4rem 0 var(--received_300_color);
}
.spiral_bottom::after {
background: var(--dark_basic_text_color);
box-shadow: 0 0 0.4rem 0 var(--dark_basic_text_color);
}
.spiral_top::before {
top: 0;
left: 0;
width: 0.1rem;
height: 0;
}
.spiral_top::after {
top: 0;
right: 0;
width: 0;
height: 0.1rem;
}
.spiral_bottom::before {
bottom: 0;
left: 0;
width: 0;
height: 0.1rem;
}
.spiral_bottom::after {
bottom: 0;
right: 0;
width: 0.1rem;
height: 0;
}
.support_button:hover .spiral_top::before {
height: 100%;
}
.support_button:hover .spiral_top::after {
width: 100%;
}
.support_button:hover .spiral_bottom::before {
width: 100%;
}
.support_button:hover .spiral_bottom::after {
height: 100%;
}
.supporters_container {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 1rem;
}
.vrct_supporters_title {
height: 6rem;
}
.vrct_supporters_desc {
font-size: 1.4rem;
text-align: start;
}

View File

@@ -0,0 +1,30 @@
import styles from "./SupportersContainer.module.scss";
import { SupportersWrapper } from "./supporters_wrapper/SupportersWrapper";
import { useSupporters } from "@logics_configs";
import { supporters_images_url } from "@ui_configs";
import vrct_supporters_title from "@images/supporters/vrct_supporters_title.png";
export const SupportersContainer = () => {
const { currentSupportersData } = useSupporters();
if (currentSupportersData.state === "error")
return <div>Failed to retrieve data.</div>;
if (currentSupportersData.state === "pending" || currentSupportersData.data === null)
return <div>Loading...</div>;
const supporters_settings = currentSupportersData.data.supporters_settings;
const last_updated_local_date = new Date(supporters_settings.last_updated_utc_date).toLocaleString();
return (
<div className={styles.supporters_container}>
<div className={styles.vrct_supporters_title_wrapper}>
<img className={styles.vrct_supporters_title} src={vrct_supporters_title}/>
<img className={styles.calc_period} src={`${supporters_images_url}/calc_period_label.png`}/>
<p className={styles.last_updated_local_date}>{`Last updated date: ${last_updated_local_date}`}</p>
</div>
<SupportersWrapper />
<p className={styles.vrct_supporters_desc_end}>{`みなさんのおかげで、みしゃ社長は布団で寝ることを許され(in開発室) しいなは喜び庭駆け回っていますふわもちもぐもぐですありがとうございます。これからもまだまだ進化するVRCTをどうかよろしくお願いします\nThanks to everyone, Misha has been granted the privilege of sleeping in a proper bed (in the development room), and Shiina is so happy, running around the yard! Fuwa-mochi-mogu-mogu! Thank you so much! We hope you'll continue to support the ever-evolving VRCT!`}</p>
</div>
);
};

View File

@@ -0,0 +1,34 @@
.supporters_container {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 1rem;
}
.vrct_supporters_title_wrapper {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 0.2rem;
width: 100%;
}
.vrct_supporters_title {
height: 4.2rem;
}
.calc_period {
height: 1.6rem;
}
.last_updated_local_date {
font-size: 1.2rem;
color: var(--dark_600_color);
width: 100%;
text-align: end;
}
.vrct_supporters_desc_end {
font-size: 1.4rem;
margin-top: 2rem;
color: var(--dark_300_color);
}

View File

@@ -0,0 +1,340 @@
import React, { useState, useCallback, useEffect } from "react";
import clsx from "clsx";
import Tooltip, { tooltipClasses } from '@mui/material/Tooltip';
import ArrowLeftSvg from "@images/arrow_left.svg?react";
import styles from "./SupportersWrapper.module.scss";
import { shuffleArray, randomIntMinMax, randomMinMax } from "@utils";
import {
useSettingBoxScrollPosition,
useSupporters,
} from "@logics_configs";
import { supporters_images_url } from "@ui_configs";
const SHUFFLE_INTERVAL_TIME = 20000;
const and_you_data = {
supporter_id: "and_you",
};
const image_sets = {
supporter_cards: `${supporters_images_url}/supporter_cards/`,
chato_expressions: `${supporters_images_url}/chato_expressions/`,
supporters_labels: `${supporters_images_url}/supporters_labels/`,
supporters_icons: `${supporters_images_url}/supporters_icons/`,
};
const getSupporterCard = (plan_name) => {
const card_map = {
"mogu_2000": "mogu_card",
"mochi_1000": "mochi_card",
"fuwa_500": "fuwa_card",
"basic_300": "basic_card",
};
if (!card_map[plan_name]) return `${image_sets.supporter_cards}basic_card.png`;
return `${image_sets.supporter_cards}${card_map[plan_name]}.png`;
};
const getChatoExpressionsPath = (file_name) => `${image_sets.chato_expressions}${file_name}.png`;
const getSupportersLabelsPath = (file_name) => `${image_sets.supporters_labels}${file_name}.png`;
const getSupportersIconsPath = (file_name) => `${image_sets.supporters_icons}${file_name}.png`;
export const SupportersWrapper = () => {
const { saveScrollPosition, restoreScrollPosition } = useSettingBoxScrollPosition();
const { currentSupportersData } = useSupporters();
const [json_data, setJsonData] = useState();
const [supportersData, setSupportersData] = useState([]);
const [chatoExpressions, setChatoExpressions] = useState([]);
useEffect(() => {
setJsonData(currentSupportersData.data);
}, [currentSupportersData.data]);
const supporters_settings = currentSupportersData.data.supporters_settings;
const calc_support_period = supporters_settings.calc_support_period;
const chato_ex_count = supporters_settings.chato_ex_count;
const recalcAndUpdateSupporters = useCallback(() => {
if (!json_data) return;
const grouped_data = {
mogu_2000: [],
mochi_1000: [],
fuwa_500: [],
basic_300: [],
former_supporter: [],
and_you: [],
};
json_data.supporters_data.forEach((supporter) => {
const value = supporter.highest_plan_during_the_period || "former_supporter";
if (grouped_data[value]) {
grouped_data[value].push(supporter);
} else {
grouped_data["former_supporter"].push(supporter);
}
});
const newSupportersData = [
...shuffleArray(grouped_data["mogu_2000"]),
...shuffleArray(grouped_data["mochi_1000"]),
...shuffleArray(grouped_data["fuwa_500"]),
...shuffleArray(grouped_data["basic_300"]),
...shuffleArray(grouped_data["former_supporter"]),
and_you_data,
];
setSupportersData(newSupportersData);
setChatoExpressions(
newSupportersData.map(() =>
getChatoExpressionsPath(
`chato_expression_${randomIntMinMax(1, chato_ex_count)}`
)
)
);
}, [json_data]);
useEffect(() => {
recalcAndUpdateSupporters();
}, [json_data, recalcAndUpdateSupporters]);
const shuffleSupporters = useCallback(() => {
if (!json_data) return;
saveScrollPosition();
recalcAndUpdateSupporters();
setTimeout(() => restoreScrollPosition(), 0);
}, [json_data, recalcAndUpdateSupporters, saveScrollPosition, restoreScrollPosition]);
useEffect(() => {
const interval = setInterval(() => {
shuffleSupporters();
}, SHUFFLE_INTERVAL_TIME);
return () => clearInterval(interval);
}, [shuffleSupporters]);
return (
<div className={styles.container}>
<ProgressBar />
<div className={styles.supporters_wrapper}>
<SupporterCardsComponent
supportersData={supportersData}
chatoExpressions={chatoExpressions}
calc_support_period={calc_support_period}
/>
</div>
<ProgressBar />
</div>
);
};
const AndYouIcon = () => {
return (
<>
<div className={styles.and_you_container}>
<div className={styles.and_you_1}></div>
<div className={styles.and_you_2}></div>
</div>
<p className={styles.and_you_fanbox_link_text}>
FANBOX Ko-fi Patreon
</p>
<ArrowLeftSvg className={styles.arrow_left_svg} />
</>
);
};
const SupporterCardsComponent = ({ supportersData, chatoExpressions, calc_support_period }) => {
return supportersData.map((item, index) => {
const target_plan = item.highest_plan_during_the_period;
const img_src = getSupporterCard(target_plan);
const is_and_you = item.supporter_id === "and_you";
const random_delay = `${randomMinMax(0.1, 6).toFixed(1)}s`;
const supporter_image_wrapper_classname = clsx(
styles.supporter_image_wrapper,
{
[styles.mogu_image]: target_plan === "mogu_2000",
}
);
return is_and_you ? (
<a href="#support_us_container" key={item.supporter_id}>
<div className={styles.supporter_image_container}>
<div
className={supporter_image_wrapper_classname}
style={{ "--delay": random_delay }}
>
<img
className={styles.supporter_image}
src={img_src}
alt="supporter"
/>
<SupporterLabelComponent
target_plan={target_plan}
item={item}
chatoExpressions={chatoExpressions}
/>
<AndYouIcon />
</div>
</div>
</a>
) : img_src ? (
<div key={item.supporter_id} className={styles.supporter_image_container}>
<div
className={supporter_image_wrapper_classname}
style={{ "--delay": random_delay }}
>
<img
className={styles.supporter_image}
src={img_src}
alt="supporter"
/>
<SupporterLabelComponent
target_plan={target_plan}
item={item}
chato_src={chatoExpressions[index]}
index={index}
/>
</div>
<SupporterPeriodContainer settings={item} calc_support_period={calc_support_period}/>
</div>
) : null;
});
};
const SupporterLabelComponent = ({ item, target_plan, chato_src }) => {
const is_icon_plan = ["mogu_2000", "mochi_1000"].includes(
target_plan
);
const supporter_label_component_classname = clsx(
styles.supporter_label_component,
{
[styles.is_icon_plan]: is_icon_plan,
}
);
const is_and_you = item.supporter_id === "and_you";
const is_default_icon = item.supporter_icon_id === "";
const file_name = is_and_you ? "and_you" : `supporter_${item.supporter_id}`;
const label_img_src = getSupportersLabelsPath(file_name);
const icon_img_src = getSupportersIconsPath(
`supporter_icon_${item.supporter_icon_id}`
);
return (
<div className={supporter_label_component_classname}>
{is_icon_plan && (
<div className={styles.supporter_icon_wrapper}>
{is_default_icon ? (
<img
className={styles.default_chato_expression_image}
src={chato_src}
alt="chato expression"
/>
) : (
<img
className={styles.supporter_icon}
src={icon_img_src}
alt="supporter icon"
/>
)}
</div>
)}
<img
className={styles.supporter_label_image}
src={label_img_src}
alt="supporter label"
/>
</div>
);
};
const SupporterPeriodContainer = ({ settings, calc_support_period }) => {
const period_data = extractKeys(settings, calc_support_period);
const offset = {
popper: {
sx: {
[`&.${tooltipClasses.popper}[data-popper-placement*="top"] .${tooltipClasses.tooltip}`]: { marginBottom: "0.2em" },
}
}
};
return (
<div className={styles.supporter_period_container}>
<div className={styles.supporter_period_wrapper}>
{Object.entries(period_data).map(([key, item], index) => {
if (item === "") return null;
const period_box_class_name = clsx(styles.period_box, {
[styles.mogu_bar]: item === "mogu_2000",
[styles.mochi_bar]: item === "mochi_1000",
[styles.fuwa_bar]: item === "fuwa_500",
[styles.basic_bar]: item === "basic_300",
});
return (
<Tooltip
key={index}
title={
<p className={styles.tooltip_period_label}>{key}</p>
}
placement="top"
slotProps={offset}
>
<div className={styles.period_box_wrapper}>
<div className={period_box_class_name}></div>
</div>
</Tooltip>
);
})}
</div>
</div>
);
};
const extractKeys = (data, keys_to_extract) => {
const result = {};
for (const key of keys_to_extract) {
if (key in data) {
result[key] = data[key];
}
}
return result;
};
const ProgressBar = () => {
const [is_active, setIsActive] = useState(false);
useEffect(() => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
setIsActive(true);
});
});
const interval = setInterval(() => {
setIsActive(false);
setTimeout(() => setIsActive(true), 50);
}, SHUFFLE_INTERVAL_TIME);
return () => clearInterval(interval);
}, []);
return (
<div
className={clsx(styles.progress_bar, {
[styles.progress_bar_active]: is_active,
})}
/>
);
};

View File

@@ -0,0 +1,259 @@
.container {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 2rem;
}
.supporters_wrapper {
display: flex;
justify-content: center;
align-items: center;
align-content: start;
flex-wrap: wrap;
column-gap: 1.8rem;
row-gap: 2rem;
}
.supporter_image_container {
position: relative;
width: 18rem;
display: flex;
flex-direction: column;
}
.supporter_image_wrapper {
position: relative;
overflow: hidden;
&:hover .supporter_icon_wrapper{
transform: rotate(360deg);
}
}
.supporter_image {
position: relative;
width: 100%;
}
.supporter_label_component {
position: absolute;
left: 0.6rem;
&.is_icon_plan {
top: 50%;
transform: translateY(-50%);
}
top: 0.4rem;
display: flex;
justify-content: center;
gap: 0.4rem;
}
.supporter_label_image {
height: 2rem;
// margin-top: 0.2rem;
&.small {
height: 1.4rem;
}
}
$progress_ease: cubic-bezier(0, 1, 0.75, 1);
// Duplicated
.supporter_icon_wrapper {
height: 4rem;
aspect-ratio: 1 /1;
border-radius: 50%;
background-color: var(--dark_basic_text_color);
overflow: hidden;
position: relative;
transition: transform 0.6s $progress_ease;
}
.supporter_icon {
aspect-ratio: 1 / 1;
height: 100%;
width: 100%;
}
.default_chato_expression_image {
position: absolute;
top: 52%;
left: 50%;
transform: translate(-50%, -50%) rotate(10deg);
width: 2.8rem;
opacity: 0.8;
}
.mogu_image {
position: relative;
&::after {
content: "";
position: absolute;
top: -200%;
left: -200%;
width: 300%;
height: 300%;
background: linear-gradient(45deg, rgba(255, 255, 255, 0) 30%, rgba(255, 255, 255, 0.7) 50%, rgba(255, 255, 255, 0) 70%);
transform: rotate(90deg);
animation: shine 2.5s infinite;
filter: blur(0.4rem);
animation-delay: var(--delay, 0s);
}
}
@keyframes shine {
0% {
top: -200%;
left: -200%;
}
50% {
top: 50%;
left: 50%;
opacity: 0.7;
}
100% {
top: 200%;
left: 200%;
}
}
.and_you_container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transition: all 0.3s ease;
}
.and_you_1, .and_you_2 {
width: 2.2rem;
height: 0.2rem;
border-radius: 50%;
background-color: var(--dark_400_color);
}
.and_you_2 {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotate(90deg);
}
.supporter_image_container {
&:hover .and_you_container {
top: 36%;
transform: translate(-50%, -50%) rotate(180deg);
animation: disappear 0.3s forwards;
}
&:hover .and_you_fanbox_link_text {
top: 74%;
opacity: 1;
}
&:hover .arrow_left_svg {
opacity: 0;
animation: arrow_up_down 0.3s forwards;
animation-delay: 0.3s;
}
}
.supporter_image_container.and_you_image {
cursor: pointer;
&:active {
opacity: 0.6;
}
}
.and_you_fanbox_link_text {
font-size: 1.2rem;
color: var(--dark_400_color);
position: absolute;
top: 50%;
left: 50%;
width: 100%;
text-align: center;
transform: translate(-50%, -50%);
transition: all 0.3s ease;
opacity: 0;
}
.arrow_left_svg {
position: absolute;
top: 60%;
left: 50%;
transform: translate(-50%, -50%) rotate(90deg);
color: var(--dark_400_color);
width: 2rem;
opacity: 0;
}
@keyframes arrow_up_down {
0% {
top: 60%;
}
100% {
top: 36%;
opacity: 1;
}
}
@keyframes disappear {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.supporter_period_container {
position: absolute;
top: 100%;
left: 0;
}
.supporter_period_wrapper {
display: flex;
gap: 0.4rem 0.2rem;
flex-shrink: 0;
flex-wrap: wrap;
padding: 0.3rem 0.4rem 0.4rem 0.4rem;
}
.period_box_wrapper {
flex-shrink: 0;
}
.period_box {
width: 1.7rem;
height: 0.3rem;
border-radius: 0.3rem;
&.mogu_bar {
background-color: var(--dark_basic_text_color);
}
&.mochi_bar {
background-color: var(--received_300_color);
}
&.fuwa_bar {
background-color: var(--supporters_color_fuwa);
}
&.basic_bar {
background-color: var(--dark_800_color);
}
}
.tooltip_period_label {
font-size: 1.4rem;
}
.progress_bar {
height: 0.2rem;
width: 0%;
&.progress_bar_active {
transition: width 20000ms linear;
background-color: var(--primary_400_color);
width: 100%;
}
}

View File

@@ -0,0 +1,642 @@
import { useEffect, useState } from "react";
import { useI18n } from "@useI18n";
import styles from "./Transcription.module.scss";
import { updateLabelsById, genNumObjArray, arrayToObject } from "@utils";
import { useStore_IsBreakPoint } from "@store";
import {
useTranscription,
} from "@logics_configs";
import {
WordFilterContainer,
DownloadModelsContainer,
RadioButtonContainer,
DropdownMenuContainer,
SliderContainer,
useOnMouseLeaveDropdownMenu,
} from "../_templates/Templates";
import {
DropdownMenu,
LabelComponent,
SectionLabelComponent,
} from "../_components";
export const Transcription = () => {
return (
<div className={styles.container}>
<Mic_Container />
<Speaker_Container />
<TranscriptionEngine_Container />
<Advanced_Container />
</div>
);
};
const Mic_Container = () => {
const { t } = useI18n();
return (
<div>
<SectionLabelComponent label={t("config_page.transcription.section_label_mic")} />
<MicRecordTimeout_Box />
<MicPhraseTimeout_Box />
<MicMaxWords_Box />
<MicWordFilter_Box />
</div>
);
};
const MicRecordTimeout_Box = () => {
const { t } = useI18n();
const { currentMicRecordTimeout, setMicRecordTimeout } = useTranscription();
const selectFunction = (selected_data) => {
setMicRecordTimeout(selected_data.selected_id);
};
return (
<DropdownMenuContainer
dropdown_id="mic_record_timeout"
label={t("config_page.transcription.mic_record_timeout.label")}
desc={t("config_page.transcription.mic_record_timeout.desc")}
selected_id={currentMicRecordTimeout.data}
list={genNumObjArray(31)}
selectFunction={selectFunction}
state={currentMicRecordTimeout.state}
/>
);
};
const MicPhraseTimeout_Box = () => {
const { t } = useI18n();
const { currentMicPhraseTimeout, setMicPhraseTimeout } = useTranscription();
const selectFunction = (selected_data) => {
setMicPhraseTimeout(selected_data.selected_id);
};
return (
<DropdownMenuContainer
dropdown_id="mic_phrase_timeout"
label={t("config_page.transcription.mic_phrase_timeout.label")}
desc={t("config_page.transcription.mic_phrase_timeout.desc")}
selected_id={currentMicPhraseTimeout.data}
list={genNumObjArray(31)}
selectFunction={selectFunction}
state={currentMicPhraseTimeout.state}
/>
);
};
const MicMaxWords_Box = () => {
const { t } = useI18n();
const { currentMicMaxWords, setMicMaxWords } = useTranscription();
const selectFunction = (selected_data) => {
setMicMaxWords(selected_data.selected_id);
};
return (
<DropdownMenuContainer
dropdown_id="mic_max_phrase"
label={t("config_page.transcription.mic_max_phrase.label")}
desc={t("config_page.transcription.mic_max_phrase.desc")}
selected_id={currentMicMaxWords.data}
list={genNumObjArray(31)}
selectFunction={selectFunction}
state={currentMicMaxWords.state}
/>
);
};
const MicWordFilter_Box = () => {
const { t } = useI18n();
return (
<WordFilterContainer
label={t("config_page.transcription.mic_word_filter.label")}
desc={t("config_page.transcription.mic_word_filter.desc")}
/>
);
};
const Speaker_Container = () => {
const { t } = useI18n();
return (
<div>
<SectionLabelComponent label={t("config_page.transcription.section_label_speaker")} />
<SpeakerRecordTimeout_Box />
<SpeakerPhraseTimeout_Box />
<SpeakerMaxWords_Box />
</div>
);
};
const SpeakerRecordTimeout_Box = () => {
const { t } = useI18n();
const { currentSpeakerRecordTimeout, setSpeakerRecordTimeout } = useTranscription();
const selectFunction = (selected_data) => {
setSpeakerRecordTimeout(selected_data.selected_id);
};
return (
<DropdownMenuContainer
dropdown_id="speaker_record_timeout"
desc={t("config_page.transcription.speaker_record_timeout.desc")}
label={t("config_page.transcription.speaker_record_timeout.label")}
selected_id={currentSpeakerRecordTimeout.data}
list={genNumObjArray(31)}
selectFunction={selectFunction}
state={currentSpeakerRecordTimeout.state}
/>
);
};
const SpeakerPhraseTimeout_Box = () => {
const { t } = useI18n();
const { currentSpeakerPhraseTimeout, setSpeakerPhraseTimeout } = useTranscription();
const selectFunction = (selected_data) => {
setSpeakerPhraseTimeout(selected_data.selected_id);
};
return (
<DropdownMenuContainer
dropdown_id="speaker_phrase_timeout"
label={t("config_page.transcription.speaker_phrase_timeout.label")}
desc={t("config_page.transcription.speaker_phrase_timeout.desc")}
selected_id={currentSpeakerPhraseTimeout.data}
list={genNumObjArray(31)}
selectFunction={selectFunction}
state={currentSpeakerPhraseTimeout.state}
/>
);
};
const SpeakerMaxWords_Box = () => {
const { t } = useI18n();
const { currentSpeakerMaxWords, setSpeakerMaxWords } = useTranscription();
const selectFunction = (selected_data) => {
setSpeakerMaxWords(selected_data.selected_id);
};
return (
<DropdownMenuContainer
dropdown_id="speaker_max_phrase"
label={t("config_page.transcription.speaker_max_phrase.label")}
desc={t("config_page.transcription.speaker_max_phrase.desc")}
selected_id={currentSpeakerMaxWords.data}
list={genNumObjArray(61)}
selectFunction={selectFunction}
state={currentSpeakerMaxWords.state}
/>
);
};
const TranscriptionEngine_Container = () => {
const { t } = useI18n();
return (
<div>
<SectionLabelComponent label={t("config_page.transcription.section_label_transcription_engines")} />
<TranscriptionEngine_Box />
<WhisperWeightType_Box />
<TranscriptionComputeDevice_Box />
</div>
);
};
const TranscriptionEngine_Box = () => {
const { t } = useI18n();
const { currentSelectedTranscriptionEngine, setSelectedTranscriptionEngine } = useTranscription();
return (
<RadioButtonContainer
label={t("config_page.transcription.select_transcription_engine.label")}
selectFunction={setSelectedTranscriptionEngine}
name="select_transcription_engine"
options={[
{ id: "Google", label: "Google" },
{ id: "Whisper", label: "Whisper" },
]}
checked_variable={currentSelectedTranscriptionEngine}
/>
);
};
const WhisperWeightType_Box = () => {
const { t } = useI18n();
const {
currentWhisperWeightTypeStatus,
pendingWhisperWeightTypeStatus,
downloadWhisperWeightTypeStatus,
} = useTranscription();
const { currentSelectedWhisperWeightType, setSelectedWhisperWeightType } = useTranscription();
const selectFunction = (id) => {
setSelectedWhisperWeightType(id);
};
const downloadStartFunction = (id) => {
pendingWhisperWeightTypeStatus(id);
downloadWhisperWeightTypeStatus(id);
};
const whisper_weight_types = currentWhisperWeightTypeStatus.data.map(item => {
return {
...item,
label: `${item.id} (${item.capacity})`,
};
});
return (
<>
<DownloadModelsContainer
label={t("config_page.transcription.whisper_weight_type.label")}
desc={t(
"config_page.transcription.whisper_weight_type.desc",
{translator: t("main_page.translator")}
)}
name="whisper_weight_type"
options={whisper_weight_types}
checked_variable={currentSelectedWhisperWeightType}
selectFunction={selectFunction}
downloadStartFunction={downloadStartFunction}
/>
</>
);
};
// Duplicate
const TranscriptionComputeDevice_Box = () => {
const { t } = useI18n();
const {
currentSelectableTranscriptionComputeDeviceList,
currentSelectedTranscriptionComputeDevice,
setSelectedTranscriptionComputeDevice,
currentSelectedTranscriptionComputeType,
setSelectedTranscriptionComputeType,
} = useTranscription();
const { onMouseLeaveFunction } = useOnMouseLeaveDropdownMenu();
const { currentIsBreakPoint } = useStore_IsBreakPoint();
const list_for_ui = transformDeviceArray(currentSelectableTranscriptionComputeDeviceList.data);
const target_index = findKeyByDeviceValue(currentSelectableTranscriptionComputeDeviceList.data, currentSelectedTranscriptionComputeDevice.data);
const DEFAULT_ORDER = [
"auto",
"int8",
"int8_bfloat16",
"int8_float16",
"int8_float32",
"bfloat16",
"float16",
"int16",
"float32"
];
const sortComputeTypesArray = (compute_types_array = [], order) => {
const src_set = new Set(compute_types_array);
const from_order = order.filter((id) => src_set.has(id));
const invalid_ids = compute_types_array.filter((id) => !order.includes(id));
if (invalid_ids.length > 0) {
console.error("[sortComputeTypesArray] Unsupported compute types ignored:", invalid_ids);
}
return from_order;
};
const buildSimpleLabels = (ordered_array = []) => {
const n = ordered_array.length;
if (n === 0) return {};
const labels = {};
ordered_array.forEach((id, idx) => {
if (idx === 0 && id === "auto") {
labels[id] = t("config_page.common.compute_device.type_template_auto");
return;
}
if (idx === 1) {
labels[id] = t(
"config_page.common.compute_device.type_template_low",
{ type_name: id }
);
return;
}
if (idx === n - 1) {
labels[id] = t(
"config_page.common.compute_device.type_template_high",
{ type_name: id }
);
return;
}
labels[id] = id;
});
return labels;
};
const computeTypesArray = currentSelectableTranscriptionComputeDeviceList.data[target_index].compute_types;
const ordered_array = sortComputeTypesArray(computeTypesArray, DEFAULT_ORDER);
const new_compute_types_labels = buildSimpleLabels(ordered_array);
const selectFunction_ComputeDevice = (selected_data) => {
const target_obj = currentSelectableTranscriptionComputeDeviceList.data[selected_data.selected_id];
setSelectedTranscriptionComputeDevice(target_obj);
};
const selectFunction_ComputeType = (selected_data) => {
setSelectedTranscriptionComputeType(selected_data.selected_id);
};
const device_container_class = clsx(styles.device_container, {
[styles.is_break_point]: currentIsBreakPoint.data,
});
const is_disabled_selector = currentSelectedTranscriptionComputeDevice.state === "pending" || currentSelectedTranscriptionComputeType.state === "pending";
return (
<div className={styles.mic_container}>
<div className={device_container_class} onMouseLeave={onMouseLeaveFunction}>
<LabelComponent
label={t("config_page.transcription.transcription_compute_device.label")}
desc={t("config_page.common.compute_device.desc")}
/>
<div className={styles.device_contents}>
<div className={styles.device_dropdown_wrapper}>
<div className={styles.device_dropdown}>
<p className={styles.device_secondary_label}>{t("config_page.common.compute_device.label_device")}</p>
<DropdownMenu
dropdown_id="transcription_compute_device"
selected_id={target_index}
list={list_for_ui}
selectFunction={selectFunction_ComputeDevice}
state={currentSelectedTranscriptionComputeDevice.state}
style={{ maxWidth: "20rem", minWidth: "10rem" }}
is_disabled={is_disabled_selector}
/>
</div>
<div className={styles.device_dropdown}>
<p className={styles.device_secondary_label}>{t("config_page.common.compute_device.label_type")}</p>
<DropdownMenu
dropdown_id="transcription_compute_type"
selected_id={currentSelectedTranscriptionComputeType.data}
list={new_compute_types_labels}
selectFunction={selectFunction_ComputeType}
state={currentSelectedTranscriptionComputeType.state}
is_disabled={is_disabled_selector}
/>
</div>
</div>
</div>
</div>
</div>
);
};
// Duplicate
const transformDeviceArray = (devices) => {
const name_counts = Object.values(devices).reduce((counts, device) => {
const name = device.device_name;
counts[name] = (counts[name] || 0) + 1;
return counts;
}, {});
const name_indices = {};
const result = {};
Object.entries(devices).forEach(([key, device]) => {
const name = device.device_name;
if (name_counts[name] > 1) {
name_indices[name] = (name_indices[name] || 0);
const value = `${name}:${name_indices[name]}`;
name_indices[name]++;
result[key] = value;
} else {
result[key] = name;
}
});
return result;
};
const findKeyByDeviceValue = (devices, target_value) => {
for (const [key, value] of Object.entries(devices)) {
if (
value.device === target_value.device &&
value.device_index === target_value.device_index &&
value.device_name === target_value.device_name
) {
return parseInt(key);
}
}
return null;
};
const Advanced_Container = () => {
const { t } = useI18n();
return (
<div>
<SectionLabelComponent label="Advanced Settings (Whisper Model)" />
{/* <SectionLabelComponent label={t("config_page.transcription.section_label_transcription_engines")} /> */}
<MicAvgLogprobContainer />
<MicNoSpeechProbContainer />
<SpeakerAvgLogprobContainer />
<SpeakerNoSpeechProbContainer />
</div>
);
};
export const MicAvgLogprobContainer = () => {
const { t } = useI18n();
const { currentMicAvgLogprob, setMicAvgLogprob } = useTranscription();
const [ui_mic_avg_logprob, setUiMicAvgLogprob] = useState(currentMicAvgLogprob.data);
const onchangeFunction = (value) => {
setUiMicAvgLogprob(value);
};
const onchangeCommittedFunction = (value) => {
setMicAvgLogprob(value);
};
useEffect(() => {
setUiMicAvgLogprob(currentMicAvgLogprob.data);
}, [currentMicAvgLogprob.data]);
// [Duplicated]
const createMarks = (min, max) => {
const marks = [];
for (let value = min; value <= max; value += 0.2) {
value = parseFloat(value.toFixed(1));
marks.push({ value, label: `${value}` });
}
return marks;
};
const marks = createMarks(-2, 0);
return (
<SliderContainer
label="Mic Avg Logprob"
desc="Default: -0.8"
min="-2"
max="0"
onchangeCommittedFunction={onchangeCommittedFunction}
onchangeFunction={onchangeFunction}
variable={ui_mic_avg_logprob}
marks={marks}
step={0.1}
track={false}
/>
);
};
export const MicNoSpeechProbContainer = () => {
const { t } = useI18n();
const { currentMicNoSpeechProb, setMicNoSpeechProb } = useTranscription();
const [ui_mic_no_speech_prob, setUiMicNoSpeechProb] = useState(currentMicNoSpeechProb.data);
const onchangeFunction = (value) => {
setUiMicNoSpeechProb(value);
};
const onchangeCommittedFunction = (value) => {
setMicNoSpeechProb(value);
};
useEffect(() => {
setUiMicNoSpeechProb(currentMicNoSpeechProb.data);
}, [currentMicNoSpeechProb.data]);
// [Duplicated]
const createMarks = (min, max) => {
const marks = [];
for (let value = min; value <= max; value += 0.1) {
value = parseFloat(value.toFixed(1));
marks.push({ value, label: `${value}` });
}
return marks;
};
const marks = createMarks(0, 1);
return (
<SliderContainer
label="Mic No Speech Prob"
desc="Default: 0.6"
min="0"
max="1"
onchangeCommittedFunction={onchangeCommittedFunction}
onchangeFunction={onchangeFunction}
variable={ui_mic_no_speech_prob}
marks={marks}
step={0.1}
track={false}
/>
);
};
export const SpeakerAvgLogprobContainer = () => {
const { t } = useI18n();
const { currentSpeakerAvgLogprob, setSpeakerAvgLogprob } = useTranscription();
const [ui_speaker_avg_logprob, setUiSpeakerAvgLogprob] = useState(currentSpeakerAvgLogprob.data);
const onchangeFunction = (value) => {
setUiSpeakerAvgLogprob(value);
};
const onchangeCommittedFunction = (value) => {
setSpeakerAvgLogprob(value);
};
useEffect(() => {
setUiSpeakerAvgLogprob(currentSpeakerAvgLogprob.data);
}, [currentSpeakerAvgLogprob.data]);
// [Duplicated]
const createMarks = (min, max) => {
const marks = [];
for (let value = min; value <= max; value += 0.2) {
value = parseFloat(value.toFixed(1));
marks.push({ value, label: `${value}` });
}
return marks;
};
const marks = createMarks(-2, 0);
return (
<SliderContainer
label="Speaker Avg Logprob"
desc="Default: -0.8"
min="-2"
max="0"
onchangeCommittedFunction={onchangeCommittedFunction}
onchangeFunction={onchangeFunction}
variable={ui_speaker_avg_logprob}
marks={marks}
step={0.1}
track={false}
/>
);
};
export const SpeakerNoSpeechProbContainer = () => {
const { t } = useI18n();
const { currentSpeakerNoSpeechProb, setSpeakerNoSpeechProb } = useTranscription();
const [ui_speaker_no_speech_prob, setUiSpeakerNoSpeechProb] = useState(currentSpeakerNoSpeechProb.data);
const onchangeFunction = (value) => {
setUiSpeakerNoSpeechProb(value);
};
const onchangeCommittedFunction = (value) => {
setSpeakerNoSpeechProb(value);
};
useEffect(() => {
setUiSpeakerNoSpeechProb(currentSpeakerNoSpeechProb.data);
}, [currentSpeakerNoSpeechProb.data]);
// [Duplicated]
const createMarks = (min, max) => {
const marks = [];
for (let value = min; value <= max; value += 0.1) {
value = parseFloat(value.toFixed(1));
marks.push({ value, label: `${value}` });
}
return marks;
};
const marks = createMarks(0, 1);
return (
<SliderContainer
label="Speaker No Speech Prob"
desc="Default: 0.6"
min="0"
max="1"
onchangeCommittedFunction={onchangeCommittedFunction}
onchangeFunction={onchangeFunction}
variable={ui_speaker_no_speech_prob}
marks={marks}
step={0.1}
track={false}
/>
);
};

View File

@@ -0,0 +1,121 @@
.container {
display: flex;
flex-direction: column;
gap: 6.4rem;
}
// [Fix me] Need refactor.
.mic_container {
display: flex;
flex-direction: column;
border-bottom: solid 0.1rem var(--dark_800_color);
padding-bottom: 1rem;
}
.speaker_container {
padding-top: 0rem;
}
.device_container {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
padding: 2rem;
margin-bottom: 0rem;
&.is_break_point {
flex-direction: column;
gap: 2rem;
align-items: start;
& .device_contents {
display: flex;
width: 100%;
justify-content: space-between;
padding-left: 0rem;
}
}
}
.threshold_container {
padding: 2rem;
}
.threshold_container {
display: flex;
width: 100%;
flex-direction: column;
justify-content: space-between;
align-items: center;
gap: 2rem;
}
.threshold_switch_section {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.threshold_section {
width: 100%;
}
.device_label {
font-size: 1.8rem;
}
.device_contents {
display: flex;
width: 100%;
justify-content: end;
padding-left: 2rem;
gap: 2rem;
}
.device_auto_select_wrapper {
display: flex;
flex-direction: column;
gap: 1.2rem;
justify-content: center;
align-items: center;
}
.device_dropdown_wrapper {
display: flex;
flex-direction: row;
gap: 2.8rem;
}
.device_dropdown {
display: flex;
flex-direction: column;
gap: 0.6rem;
white-space: nowrap;
max-width: 24rem;
&.is_disabled {
pointer-events: none;
}
}
.device_secondary_label {
padding-left: 0.2rem;
padding-right: 0.4rem;
font-size: 1.4rem;
color: var(--dark_500_color);
white-space: nowrap;
}

View File

@@ -0,0 +1,296 @@
import { useEffect, useState } from "react";
import { useI18n } from "@useI18n";
import styles from "./Translation.module.scss";
import { updateLabelsById, arrayToObject } from "@utils";
import { useStore_IsBreakPoint } from "@store";
import {
useTranslation,
} from "@logics_configs";
import {
DownloadModelsContainer,
DeeplAuthKeyContainer,
useOnMouseLeaveDropdownMenu,
} from "../_templates/Templates";
import {
DropdownMenu,
LabelComponent,
} from "../_components";
export const Translation = () => {
return (
<>
<CTranslate2WeightType_Box />
<TranslationComputeDevice_Box />
<DeeplAuthKey_Box />
</>
);
};
const CTranslate2WeightType_Box = () => {
const { t } = useI18n();
const {
currentCTranslate2WeightTypeStatus,
pendingCTranslate2WeightTypeStatus,
downloadCTranslate2WeightTypeStatus,
currentSelectedCTranslate2WeightType,
setSelectedCTranslate2WeightType,
} = useTranslation();
const selectFunction = (id) => {
setSelectedCTranslate2WeightType(id);
};
const downloadStartFunction = (id) => {
pendingCTranslate2WeightTypeStatus(id);
downloadCTranslate2WeightTypeStatus(id);
};
const c_translate2_weight_types_object = currentCTranslate2WeightTypeStatus.data.map(item => {
return {
...item,
label: `${item.id} (${item.capacity})`,
};
});
return (
<>
<DownloadModelsContainer
label={t(
"config_page.translation.ctranslate2_weight_type.label",
{ctranslate2: "CTranslate2"}
)}
desc={t(
"config_page.translation.ctranslate2_weight_type.desc",
{ctranslate2: "CTranslate2"}
)}
name="ctranslate2_weight_type"
options={c_translate2_weight_types_object}
checked_variable={currentSelectedCTranslate2WeightType}
selectFunction={selectFunction}
downloadStartFunction={downloadStartFunction}
/>
</>
);
};
// Duplicate
const TranslationComputeDevice_Box = () => {
const { t } = useI18n();
const {
currentSelectableTranslationComputeDeviceList,
currentSelectedTranslationComputeDevice,
setSelectedTranslationComputeDevice,
currentSelectedTranslationComputeType,
setSelectedTranslationComputeType,
} = useTranslation();
const { onMouseLeaveFunction } = useOnMouseLeaveDropdownMenu();
const { currentIsBreakPoint } = useStore_IsBreakPoint();
const list_for_ui = transformDeviceArray(currentSelectableTranslationComputeDeviceList.data);
const target_index = findKeyByDeviceValue(currentSelectableTranslationComputeDeviceList.data, currentSelectedTranslationComputeDevice.data);
const DEFAULT_ORDER = [
"auto",
"int8",
"int8_bfloat16",
"int8_float16",
"int8_float32",
"bfloat16",
"float16",
"int16",
"float32"
];
const sortComputeTypesArray = (compute_types_array = [], order) => {
const src_set = new Set(compute_types_array);
const from_order = order.filter((id) => src_set.has(id));
const invalid_ids = compute_types_array.filter((id) => !order.includes(id));
if (invalid_ids.length > 0) {
console.error("[sortComputeTypesArray] Unsupported compute types ignored:", invalid_ids);
}
return from_order;
};
const buildSimpleLabels = (ordered_array = []) => {
const n = ordered_array.length;
if (n === 0) return {};
const labels = {};
ordered_array.forEach((id, idx) => {
if (idx === 0 && id === "auto") {
labels[id] = t("config_page.common.compute_device.type_template_auto");
return;
}
if (idx === 1) {
labels[id] = t(
"config_page.common.compute_device.type_template_low",
{ type_name: id }
);
return;
}
if (idx === n - 1) {
labels[id] = t(
"config_page.common.compute_device.type_template_high",
{ type_name: id }
);
return;
}
labels[id] = id;
});
return labels;
};
const computeTypesArray = currentSelectableTranslationComputeDeviceList.data[target_index].compute_types;
const ordered_array = sortComputeTypesArray(computeTypesArray, DEFAULT_ORDER);
const new_compute_types_labels = buildSimpleLabels(ordered_array);
const selectFunction_ComputeDevice = (selected_data) => {
const target_obj = currentSelectableTranslationComputeDeviceList.data[selected_data.selected_id];
setSelectedTranslationComputeDevice(target_obj);
};
const selectFunction_ComputeType = (selected_data) => {
setSelectedTranslationComputeType(selected_data.selected_id);
};
const device_container_class = clsx(styles.device_container, {
[styles.is_break_point]: currentIsBreakPoint.data,
});
const is_disabled_selector = currentSelectedTranslationComputeDevice.state === "pending" || currentSelectedTranslationComputeType.state === "pending";
return (
<div className={styles.mic_container}>
<div className={device_container_class} onMouseLeave={onMouseLeaveFunction}>
<LabelComponent
label={t("config_page.translation.translation_compute_device.label")}
desc={t("config_page.common.compute_device.desc")}
/>
<div className={styles.device_contents}>
<div className={styles.device_dropdown_wrapper}>
<div className={styles.device_dropdown}>
<p className={styles.device_secondary_label}>{t("config_page.common.compute_device.label_device")}</p>
<DropdownMenu
dropdown_id="translation_compute_device"
selected_id={target_index}
list={list_for_ui}
selectFunction={selectFunction_ComputeDevice}
state={currentSelectedTranslationComputeDevice.state}
style={{ maxWidth: "20rem", minWidth: "10rem" }}
is_disabled={is_disabled_selector}
/>
</div>
<div className={styles.device_dropdown}>
<p className={styles.device_secondary_label}>{t("config_page.common.compute_device.label_type")}</p>
<DropdownMenu
dropdown_id="translation_compute_type"
selected_id={currentSelectedTranslationComputeType.data}
list={new_compute_types_labels}
selectFunction={selectFunction_ComputeType}
state={currentSelectedTranslationComputeType.state}
is_disabled={is_disabled_selector}
/>
</div>
</div>
</div>
</div>
</div>
);
};
const DeeplAuthKey_Box = () => {
const { t } = useI18n();
const { currentDeepLAuthKey, setDeepLAuthKey, deleteDeepLAuthKey } = useTranslation();
const [input_value, seInputValue] = useState(currentDeepLAuthKey.data);
const onChangeFunction = (value) => {
seInputValue(value);
};
const saveFunction = () => {
if (input_value === "") return deleteDeepLAuthKey();
setDeepLAuthKey(input_value);
};
useEffect(() => {
if (currentDeepLAuthKey.state === "pending") return;
seInputValue(currentDeepLAuthKey.data);
}, [currentDeepLAuthKey]);
return (
<>
<DeeplAuthKeyContainer
label={t("config_page.translation.deepl_auth_key.label")}
desc={t(
"config_page.translation.deepl_auth_key.desc",
{translator: t("main_page.translator")}
)}
variable={input_value}
state={currentDeepLAuthKey.state}
onChangeFunction={onChangeFunction}
saveFunction={saveFunction}
/>
</>
);
};
// Duplicate
const transformDeviceArray = (devices) => {
const name_counts = Object.values(devices).reduce((counts, device) => {
const name = device.device_name;
counts[name] = (counts[name] || 0) + 1;
return counts;
}, {});
const name_indices = {};
const result = {};
Object.entries(devices).forEach(([key, device]) => {
const name = device.device_name;
if (name_counts[name] > 1) {
name_indices[name] = (name_indices[name] || 0);
const value = `${name}:${name_indices[name]}`;
name_indices[name]++;
result[key] = value;
} else {
result[key] = name;
}
});
return result;
};
const findKeyByDeviceValue = (devices, target_value) => {
for (const [key, value] of Object.entries(devices)) {
if (
value.device === target_value.device &&
value.device_index === target_value.device_index &&
value.device_name === target_value.device_name
) {
return parseInt(key);
}
}
return null;
};

View File

@@ -0,0 +1,106 @@
// [Fix me] Need refactor.
.mic_container {
display: flex;
flex-direction: column;
border-bottom: solid 0.1rem var(--dark_800_color);
padding-bottom: 1rem;
}
.speaker_container {
padding-top: 0rem;
}
.device_container {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
padding: 2rem;
margin-bottom: 0rem;
&.is_break_point {
flex-direction: column;
gap: 2rem;
align-items: start;
& .device_contents {
display: flex;
width: 100%;
justify-content: space-between;
padding-left: 0rem;
}
}
}
.threshold_container {
padding: 2rem;
}
.threshold_container {
display: flex;
width: 100%;
flex-direction: column;
justify-content: space-between;
align-items: center;
gap: 2rem;
}
.threshold_switch_section {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.threshold_section {
width: 100%;
}
.device_label {
font-size: 1.8rem;
}
.device_contents {
display: flex;
width: 100%;
justify-content: end;
padding-left: 2rem;
gap: 2rem;
}
.device_auto_select_wrapper {
display: flex;
flex-direction: column;
gap: 1.2rem;
justify-content: center;
align-items: center;
}
.device_dropdown_wrapper {
display: flex;
flex-direction: row;
gap: 2.8rem;
}
.device_dropdown {
display: flex;
flex-direction: column;
gap: 0.6rem;
white-space: nowrap;
max-width: 24rem;
&.is_disabled {
pointer-events: none;
}
}
.device_secondary_label {
padding-left: 0.2rem;
padding-right: 0.4rem;
font-size: 1.4rem;
color: var(--dark_500_color);
white-space: nowrap;
}

View File

@@ -0,0 +1,634 @@
import React, { useState, useEffect, useRef } from "react";
import { useI18n } from "@useI18n";
import clsx from "clsx";
import styles from "./Vr.module.scss";
import { ui_configs } from "@ui_configs";
import { Slider } from "../_components";
import {
RadioButtonContainer,
SwitchBoxContainer,
CheckboxContainer,
} from "../_templates/Templates";
import {
SectionLabelComponent,
} from "../_components";
import { ResetButton } from "@common_components";
import {
useVr,
} from "@logics_configs";
import SquareSvg from "@images/square.svg?react";
import TriangleSvg from "@images/triangle.svg?react";
import { randomIntMinMax } from "@utils";
export const Vr = () => {
const { t } = useI18n();
const [is_opened_small_settings, setIsOpenedSmallSettings] = useState(true);
const toggleIsOpenedSmallSettings = () => {
setIsOpenedSmallSettings(!is_opened_small_settings);
};
const {
currentIsEnabledOverlayLargeLog,
toggleIsEnabledOverlayLargeLog,
currentIsEnabledOverlaySmallLog,
toggleIsEnabledOverlaySmallLog,
currentOverlayLargeLogSettings,
setOverlayLargeLogSettings,
currentOverlaySmallLogSettings,
setOverlaySmallLogSettings,
} = useVr();
const restoreDefaultSettings = () => {
setOverlaySmallLogSettings(ui_configs.overlay_small_log_default_settings);
setOverlayLargeLogSettings(ui_configs.overlay_large_log_default_settings);
};
return (
<div className={styles.container}>
<div className={styles.wrapper}>
<PageSwitcherContainer
toggleFunction={toggleIsOpenedSmallSettings}
is_selected={is_opened_small_settings}
label_1={t("config_page.vr.single_line")}
label_2={t("config_page.vr.multi_lines")}
/>
{is_opened_small_settings ? (
<OverlaySettingsContainer
id="overlay_settings_small"
ui_configs={ui_configs.overlay_small_log}
default_ui_configs={ui_configs.overlay_small_log_default_settings}
current_overlay_settings={currentOverlaySmallLogSettings.data}
set_overlay_settings={setOverlaySmallLogSettings}
current_is_enabled_overlay={currentIsEnabledOverlaySmallLog}
toggle_is_enabled_overlay={toggleIsEnabledOverlaySmallLog}
/>
) : (
<OverlaySettingsContainer
id="overlay_settings_large"
ui_configs={ui_configs.overlay_large_log}
default_ui_configs={ui_configs.overlay_large_log_default_settings}
current_overlay_settings={currentOverlayLargeLogSettings.data}
set_overlay_settings={setOverlayLargeLogSettings}
current_is_enabled_overlay={currentIsEnabledOverlayLargeLog}
toggle_is_enabled_overlay={toggleIsEnabledOverlayLargeLog}
/>
)}
</div>
<CommonSettingsContainer />
<button
className={styles.restore_default_settings_button}
onClick={restoreDefaultSettings}
>
{t("config_page.vr.restore_default_settings")}
</button>
</div>
);
};
const OverlaySettingsContainer = ({
current_overlay_settings,
set_overlay_settings,
current_is_enabled_overlay,
toggle_is_enabled_overlay,
ui_configs,
default_ui_configs,
id
}) => {
const { t } = useI18n();
useEffect(() => {
setSettings(current_overlay_settings);
}, [current_overlay_settings]);
const [settings, setSettings] = useState(current_overlay_settings);
const [timeout_id, setTimeoutId] = useState(null);
const [is_opened_position_controller, setIsOpenedPositionController] = useState(true);
const togglePositionRotationController = () => {
setIsOpenedPositionController(!is_opened_position_controller);
};
const onchangeFunction = (key, value) => {
setSettings((prev) => ({ ...prev, [key]: value }));
if (timeout_id) clearTimeout(timeout_id);
const newTimeoutId = setTimeout(() => {
const new_data = { ...settings, [key]: value };
set_overlay_settings(new_data);
}, 50);
setTimeoutId(newTimeoutId);
};
const selectFunction = (key, value) => {
const new_data = { ...settings, [key]: value };
set_overlay_settings(new_data);
};
return (
<>
<SwitchBoxContainer
label={t("config_page.vr.overlay_enable")}
variable={current_is_enabled_overlay}
toggleFunction={toggle_is_enabled_overlay}
/>
<PageSwitcherContainer
toggleFunction={togglePositionRotationController}
is_selected={is_opened_position_controller}
label_1={t("config_page.vr.position")}
label_2={t("config_page.vr.rotation")}
/>
<div className={styles.position_rotation_controls_box}>
{is_opened_position_controller ? (
<PositionControls settings={settings} onchangeFunction={onchangeFunction} ui_configs={ui_configs} default_ui_configs={default_ui_configs} selectFunction={selectFunction}/>
) : (
<RotationControls settings={settings} onchangeFunction={onchangeFunction} ui_configs={ui_configs} default_ui_configs={default_ui_configs} selectFunction={selectFunction}/>
)}
<SendSampleTextToggleButton />
</div>
<OtherControls settings={settings} onchangeFunction={onchangeFunction} ui_configs={ui_configs} />
<RadioButtonContainer
label={t("config_page.vr.tracker")}
selectFunction={(value) => selectFunction("tracker", value)}
name={id}
options={[
{ id: "HMD", label: t("config_page.vr.hmd") },
{ id: "LeftHand", label: t("config_page.vr.left_hand") },
{ id: "RightHand", label: t("config_page.vr.right_hand") },
]}
checked_variable={{data: settings.tracker}}
column={true}
/>
</>
);
};
const PageSwitcherContainer = (props) => {
const toggle_button_class_names__position = clsx(styles.controller_type_switcher, {
[styles.is_selected]: props.is_selected,
});
const toggle_button_class_names__rotation = clsx(styles.controller_type_switcher, {
[styles.is_selected]: !props.is_selected,
});
return (
<div className={styles.controller_type_switch} onClick={() => props.toggleFunction()}>
<div className={toggle_button_class_names__position}>
<p className={styles.controller_switcher_label}>{props.label_1}</p>
</div>
<div className={toggle_button_class_names__rotation}>
<p className={styles.controller_switcher_label}>{props.label_2}</p>
</div>
</div>
);
};
export const PositionControls = ({ settings, onchangeFunction, selectFunction, ui_configs, default_ui_configs }) => {
const { t } = useI18n();
const {
variable_display: x_variable_display,
is_max: is_max_position_x,
is_min: is_min_position_x,
countUp: countUpPositionX,
countDown: countDownPositionX,
} = useVariableControl("x_pos", settings, onchangeFunction, ui_configs);
const {
variable_display: y_variable_display,
is_max: is_max_position_y,
is_min: is_min_position_y,
countUp: countUpPositionY,
countDown: countDownPositionY,
} = useVariableControl("y_pos", settings, onchangeFunction, ui_configs);
const {
variable_display: z_variable_display,
is_max: is_max_position_z,
is_min: is_min_position_z,
countUp: countUpPositionZ,
countDown: countDownPositionZ,
} = useVariableControl("z_pos", settings, onchangeFunction, ui_configs);
return (
<div className={styles.position_controls}>
<div className={styles.position_wrapper}>
<p className={clsx(styles.slider_label, styles.x_position_label)}>
{t("config_page.vr.x_position")}
<ResetButton onClickFunction={() => selectFunction("x_pos", default_ui_configs.x_pos)} />
</p>
<Slider
className={styles.x_position_slider}
no_padding={true}
variable={settings.x_pos}
step={ui_configs.x_pos.step}
min={ui_configs.x_pos.min}
max={ui_configs.x_pos.max}
onchangeFunction={(value) => onchangeFunction("x_pos", value)}
valueLabelDisplay={x_variable_display}
valueLabelDisplayLocation="top"
/>
<AdjustButtonContainer
wrapper_class_name={styles.x_position_button_wrapper}
is_max={is_max_position_x}
is_min={is_min_position_x}
countUp={countUpPositionX}
countDown={countDownPositionX}
/>
</div>
<div className={styles.position_wrapper}>
<p className={clsx(styles.slider_label, styles.y_position_label)}>
{t("config_page.vr.y_position")}
<ResetButton onClickFunction={() => selectFunction("y_pos", default_ui_configs.y_pos)} />
</p>
<Slider
className={styles.y_position_slider}
no_padding={true}
variable={settings.y_pos}
step={ui_configs.y_pos.step}
min={ui_configs.y_pos.min}
max={ui_configs.y_pos.max}
onchangeFunction={(value) => onchangeFunction("y_pos", value)}
orientation="vertical"
valueLabelDisplay={y_variable_display}
valueLabelDisplayLocation="right"
/>
<AdjustButtonContainer
wrapper_class_name={styles.y_position_button_wrapper}
is_max={is_max_position_y}
is_min={is_min_position_y}
countUp={countUpPositionY}
countDown={countDownPositionY}
/>
</div>
<div className={styles.position_wrapper}>
<p className={clsx(styles.slider_label, styles.z_position_label)}>
{t("config_page.vr.z_position")}
<ResetButton onClickFunction={() => selectFunction("z_pos", default_ui_configs.z_pos)} />
</p>
<Slider
className={styles.z_position_slider}
no_padding={true}
variable={settings.z_pos}
step={ui_configs.z_pos.step}
min={ui_configs.z_pos.min}
max={ui_configs.z_pos.max}
onchangeFunction={(value) => onchangeFunction("z_pos", value)}
orientation="vertical"
valueLabelDisplay={z_variable_display}
valueLabelDisplayLocation="left"
/>
<AdjustButtonContainer
wrapper_class_name={styles.z_position_button_wrapper}
is_max={is_max_position_z}
is_min={is_min_position_z}
countUp={countUpPositionZ}
countDown={countDownPositionZ}
/>
</div>
</div>
);
};
export const RotationControls = ({ settings, onchangeFunction, selectFunction, ui_configs, default_ui_configs }) => {
const { t } = useI18n();
const {
variable_display: x_variable_display,
is_max: is_max_rotation_x,
is_min: is_min_rotation_x,
countUp: countUpRotationX,
countDown: countDownRotationX,
} = useVariableControl("x_rotation", settings, onchangeFunction, ui_configs);
const {
variable_display: y_variable_display,
is_max: is_max_rotation_y,
is_min: is_min_rotation_y,
countUp: countUpRotationY,
countDown: countDownRotationY,
} = useVariableControl("y_rotation", settings, onchangeFunction, ui_configs);
const {
variable_display: z_variable_display,
is_max: is_max_rotation_z,
is_min: is_min_rotation_z,
countUp: countUpRotationZ,
countDown: countDownRotationZ,
} = useVariableControl("z_rotation", settings, onchangeFunction, ui_configs);
return (
<div className={styles.rotation_controls}>
<div className={styles.rotation_wrapper}>
<p className={clsx(styles.slider_label, styles.x_rotation_label)}>
{t("config_page.vr.x_rotation")}
<ResetButton onClickFunction={() => selectFunction("x_rotation", default_ui_configs.x_rotation)} />
</p>
<Slider
className={styles.x_rotation_slider}
no_padding={true}
variable={-settings.x_rotation}
valueLabelFormat={settings.x_rotation}
step={ui_configs.x_rotation.step}
min={ui_configs.x_rotation.min}
max={ui_configs.x_rotation.max}
onchangeFunction={(value) => onchangeFunction("x_rotation", -value)}
orientation="vertical"
valueLabelDisplay={x_variable_display}
valueLabelDisplayLocation="right"
/>
<AdjustButtonContainer
wrapper_class_name={styles.x_rotation_button_wrapper}
is_max={is_min_rotation_x}
is_min={is_max_rotation_x}
countUp={countDownRotationX}
countDown={countUpRotationX}
/>
</div>
<div className={styles.rotation_wrapper}>
<p className={clsx(styles.slider_label, styles.y_rotation_label)}>
{t("config_page.vr.y_rotation")}
<ResetButton onClickFunction={() => selectFunction("y_rotation", default_ui_configs.y_rotation)} />
</p>
<Slider
className={styles.y_rotation_slider}
no_padding={true}
variable={settings.y_rotation}
step={ui_configs.y_rotation.step}
min={ui_configs.y_rotation.min}
max={ui_configs.y_rotation.max}
onchangeFunction={(value) => onchangeFunction("y_rotation", value)}
valueLabelDisplay={y_variable_display}
valueLabelDisplayLocation="top"
/>
<AdjustButtonContainer
wrapper_class_name={styles.y_rotation_button_wrapper}
is_max={is_max_rotation_y}
is_min={is_min_rotation_y}
countUp={countUpRotationY}
countDown={countDownRotationY}
/>
</div>
<div className={styles.rotation_wrapper}>
<p className={clsx(styles.slider_label, styles.z_rotation_label)}>
{t("config_page.vr.z_rotation")}
<ResetButton onClickFunction={() => selectFunction("z_rotation", default_ui_configs.z_rotation)} />
</p>
<Slider
className={styles.z_rotation_slider}
no_padding={true}
variable={settings.z_rotation}
step={ui_configs.z_rotation.step}
min={ui_configs.z_rotation.min}
max={ui_configs.z_rotation.max}
onchangeFunction={(value) => onchangeFunction("z_rotation", value)}
orientation="vertical"
valueLabelDisplay={z_variable_display}
valueLabelDisplayLocation="left"
/>
<AdjustButtonContainer
wrapper_class_name={styles.z_rotation_button_wrapper}
is_max={is_max_rotation_z}
is_min={is_min_rotation_z}
countUp={countUpRotationZ}
countDown={countDownRotationZ}
/>
</div>
</div>
);
};
const AdjustButtonContainer = ({ wrapper_class_name, is_max, is_min, countUp, countDown }) => {
return (
<div className={wrapper_class_name}>
<div
className={clsx(
styles.button_wrapper,
{
[styles.is_disabled]: is_max,
[styles.up]: true,
}
)}
onClick={countUp}
>
<TriangleSvg className={styles.adjust_button_triangle_svg} />
</div>
<div
className={clsx(
styles.button_wrapper,
{
[styles.is_disabled]: is_min,
}
)}
onClick={countDown}
>
<TriangleSvg className={styles.adjust_button_triangle_svg} />
</div>
</div>
);
};
const OtherControls = ({settings, onchangeFunction, ui_configs}) => {
const { t } = useI18n();
const ui_variable_opacity = (settings.opacity * 100).toFixed(0);
const ui_variable_ui_scaling = (settings.ui_scaling * 100).toFixed(0);
return(
<div className={styles.other_controls}>
<div className={styles.other_controls_wrapper}>
<p className={clsx(styles.other_controls_slider_label, styles.opacity_label)}>
{t("config_page.vr.opacity")}
</p>
<Slider
className={clsx(styles.other_controls_slider, styles.opacity_slider)}
no_padding={true}
variable={settings.opacity * 100}
valueLabelFormat={`${ui_variable_opacity}%`}
step={5}
min={10}
max={100}
onchangeFunction={(value) => onchangeFunction("opacity", value / 100)}
/>
</div>
<div className={styles.other_controls_wrapper}>
<p className={clsx(styles.other_controls_slider_label, styles.ui_scaling_label)}>
{t("config_page.vr.ui_scaling")}
</p>
<Slider
className={clsx(styles.other_controls_slider, styles.ui_scaling_slider)}
no_padding={true}
variable={settings.ui_scaling * 100}
valueLabelFormat={`${ui_variable_ui_scaling}%`}
step={ui_configs.ui_scaling.step}
min={ui_configs.ui_scaling.min}
max={ui_configs.ui_scaling.max}
onchangeFunction={(value) => onchangeFunction("ui_scaling", value / 100)}
/>
</div>
<div className={styles.other_controls_wrapper}>
<p className={clsx(styles.other_controls_slider_label, styles.display_duration_label)}>{t("config_page.vr.display_duration")}</p>
<Slider
className={clsx(styles.other_controls_slider, styles.display_duration_slider)}
no_padding={true}
variable={settings.display_duration}
valueLabelFormat={`${settings.display_duration} second(s)`}
step={1}
min={1}
max={60}
onchangeFunction={(value) => onchangeFunction("display_duration", value)}
/>
</div>
<div className={styles.other_controls_wrapper}>
<p className={clsx(styles.other_controls_slider_label, styles.fadeout_duration_label)}>{t("config_page.vr.fadeout_duration")}</p>
<Slider
className={clsx(styles.other_controls_slider, styles.fadeout_duration_slider)}
no_padding={true}
variable={settings.fadeout_duration}
valueLabelFormat={`${settings.fadeout_duration} second(s)`}
step={1}
min={0}
max={5}
onchangeFunction={(value) => onchangeFunction("fadeout_duration", value)}
/>
</div>
</div>
);
};
const CommonSettingsContainer = () => {
const { t } = useI18n();
const { currentOverlayShowOnlyTranslatedMessages, toggleOverlayShowOnlyTranslatedMessages } = useVr();
return (
<div className={styles.common_container}>
<SectionLabelComponent label={t("config_page.vr.common_settings")} />
<CheckboxContainer
label={t("config_page.vr.overlay_show_only_translated_messages.label")}
variable={currentOverlayShowOnlyTranslatedMessages}
toggleFunction={toggleOverlayShowOnlyTranslatedMessages}
/>
</div>
);
};
const SendSampleTextToggleButton = () => {
const { t } = useI18n();
const { sendTextToOverlay } = useVr();
const [is_started, setIsStarted] = useState(false);
useEffect(() => {
let interval_id;
if (is_started) {
interval_id = setInterval(() => {
const text_data = Array.from(
{ length: randomIntMinMax(1, 5) },
() => t("config_page.vr.sample_text_button.sample_text")
).join(" ");
sendTextToOverlay(text_data);
}, 1000);
};
return () => {
if (interval_id) {
clearInterval(interval_id);
}
};
}, [is_started]);
const toggleFunction = () => {
setIsStarted(!is_started);
};
const label = is_started
? t("config_page.vr.sample_text_button.stop")
: t("config_page.vr.sample_text_button.start");
return (
<div className={styles.sample_text_button_wrapper}>
<button
className={clsx(styles.sample_text_button, { [styles.is_started]: is_started })}
onClick={toggleFunction}
>
{is_started ? (
<SquareSvg className={styles.sample_text_button_square_svg} />
) : (
<TriangleSvg className={styles.sample_text_button_triangle_svg} />
)}
</button>
<p className={styles.sample_text_button_label}>{label}</p>
</div>
);
};
const useVariableControl = (key, settings, onchangeFunction, ui_configs) => {
const [variable_display, setVariableDisplay] = useState("auto");
const [is_max, setIsMax] = useState(settings[key] >= ui_configs[key].max);
const [is_min, setIsMin] = useState(settings[key] <= ui_configs[key].min);
const timerRef = useRef();
useEffect(() => {
return () => {
clearTimeout(timerRef.current);
};
}, []);
const triggerDisplay = () => {
setVariableDisplay("on");
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
setVariableDisplay("auto");
}, 2000);
};
useEffect(() => {
setIsMax(settings[key] >= ui_configs[key].max);
setIsMin(settings[key] <= ui_configs[key].min);
}, [settings[key]]);
const countUp = () => {
if (is_max) return;
const step = ui_configs[key].step;
const new_value = parseFloat((settings[key] + step).toFixed(2));
onchangeFunction(key, new_value);
triggerDisplay();
};
const countDown = () => {
if (is_min) return;
const step = ui_configs[key].step;
const new_value = parseFloat((settings[key] - step).toFixed(2));
onchangeFunction(key, new_value);
triggerDisplay();
};
return {
variable_display,
is_max,
is_min,
countUp,
countDown,
};
};

View File

@@ -0,0 +1,352 @@
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
padding: 2rem;
width: 100%;
gap: 4rem;
}
.wrapper {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
max-width: 56rem;
gap: 4rem;
}
.controller_type_switch {
margin-top: 2rem;
display: flex;
border: 0.1rem solid var(--dark_600_color);
border-radius: 0.4rem;
width: 80%;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--dark_600_color);
&:hover {
color: var(--dark_400_color);
}
}
.controller_type_switcher {
width: 100%;
&.is_selected {
background-color: var(--dark_850_color);
}
&.is_selected .controller_switcher_label {
color: var(--dark_200_color);
}
}
.controller_switcher_label {
padding: 1rem;
font-size: 1.6rem;
}
.position_rotation_controls_box {
margin-top: 8rem;
position: relative;
aspect-ratio: 1 / 1;
width: 36%;
max-width: 36rem;
transform: translate(-10%);
}
.sample_text_button_wrapper {
position: absolute;
bottom: -12%;
left: -80%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
// transform: translate(-50%, -50%);
}
.sample_text_button {
background-color: var(--dark_850_color);
padding: 1.8rem;
border-radius: 50%;
&:hover {
background-color: var(--dark_800_color);
}
&:active {
background-color: var(--dark_925_color);
}
&.is_started {
background-color: var(--primary_600_color);
&:hover {
background-color: var(--primary_500_color);
}
&:active {
background-color: var(--primary_700_color);
}
}
}
.sample_text_button_triangle_svg, .sample_text_button_square_svg {
width: 2.4rem;
}
.sample_text_button_triangle_svg {
transform: translateX(10%) rotate(90deg);
}
.sample_text_button_label {
position: absolute;
left: 50%;
top: 110%;
// bottom: -2rem;
transform: translateX(-50%);
white-space: pre-wrap;
font-size: 1.2rem;
width: max-content;
text-align: center;
}
// .position_controls {
// background-color: gray;
// }
// .position_wrapper {
// background-color: gray;
// }
.slider_label {
font-size: 1.4rem;
width: 100%;
display: flex;
align-items: center;
gap: 0.6rem;
white-space: nowrap;
}
.x_position_label {
position: absolute;
bottom: -5rem;
right: -46%;
justify-content: end;
}
.y_position_label {
position: absolute;
bottom: 110%;
right: 119%;
justify-content: end;
}
.z_position_label {
position: absolute;
top: 14%;
left: 110%;
}
.x_position_slider {
position: absolute;
bottom: 0;
left: 27%;
width: 100%;
height: 0%;
}
.y_position_slider {
position: absolute;
bottom: 27%;
left: 0;
width: 0%;
height: 100%;
}
.z_position_slider {
position: absolute;
bottom: 80%;
left: 88%;
transform: translate(50%,50%) rotate(45deg);
width: 0%;
height: 100%;
}
%variable-button {
width: 3.8rem;
border-radius: 0.4rem;
aspect-ratio: 1.2 / 1;
background-color: var(--dark_850_color);
display: flex;
justify-content: center;
align-items: center;
font-size: 1.6rem;
cursor: pointer;
&:hover {
background-color: var(--primary_500_color);
}
&:active {
background-color: var(--primary_600_color);
}
&.is_disabled {
pointer-events: none;
background-color: var(--dark_875_color);
& .adjust_button_triangle_svg {
color: var(--dark_800_color);
}
}
}
@mixin variable-button-wrapper($vertical-pos, $vertical-value, $horizontal-pos, $horizontal-value, $rotate: 0deg) {
position: absolute;
#{$vertical-pos}: $vertical-value;
#{$horizontal-pos}: $horizontal-value;
display: flex;
gap: 1.6rem;
flex-direction: column;
transform: translate(-50%) rotate($rotate);
}
.button_wrapper {
@extend %variable-button;
&.up .adjust_button_triangle_svg {
transform: rotate(0deg);
}
&:not(.up) .adjust_button_triangle_svg {
transform: rotate(180deg);
}
&.is_disabled {
pointer-events: none;
color: var(--dark_875_color);
}
}
.adjust_button_triangle_svg {
width: 1.8rem;
color: var(--dark_400_color);
}
.y_position_button_wrapper {
@include variable-button-wrapper(top, 30%, left, -26%);
}
.x_position_button_wrapper {
@include variable-button-wrapper(bottom, -38%, left, 46%, 90deg);
}
.z_position_button_wrapper {
@include variable-button-wrapper(bottom, 26%, right, -4%, 45deg);
}
// .rotation_controls {
// background-color: gray;
// }
// .rotation_wrapper {
// background-color: gray;
// }
.x_rotation_label {
position: absolute;
bottom: 110%;
right: 119%;
justify-content: end;
}
.y_rotation_label {
position: absolute;
bottom: -5rem;
right: -46%;
justify-content: end;
}
.z_rotation_label {
position: absolute;
top: -20%;
right: -100%;
}
.x_rotation_slider {
position: absolute;
bottom: 27%;
left: 0;
width: 0%;
height: 100%;
}
.y_rotation_slider {
position: absolute;
bottom: 0;
left: 27%;
width: 100%;
height: 0%;
}
.z_rotation_slider {
position: absolute;
bottom: 80%;
left: 100%;
transform: translate(50%,50%) rotate(-45deg);
width: 0%;
height: 100%;
}
.x_rotation_button_wrapper {
@include variable-button-wrapper(top, 30%, left, -26%);
}
.y_rotation_button_wrapper {
@include variable-button-wrapper(bottom, -38%, left, 46%, 90deg);
}
.z_rotation_button_wrapper {
@include variable-button-wrapper(bottom, 50%, right, -60%, -45deg);
}
.other_controls {
margin-top: 6rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
width: 100%;
}
.other_controls_wrapper {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
position: relative;
}
.other_controls_slider {
// margin-left: 18rem;
// width: 60%;
}
.other_controls_slider_label {
// position: absolute;
font-size: 1.6rem;
flex-shrink: 0;
width: 30%;
}
.common_container {
width: 100%;
}
.common_label {
font-size: 1.4rem;
}
.restore_default_settings_button {
font-size: 1.2rem;
margin-top: 6rem;
padding: 0.8rem;
border-radius: 0.4rem;
&:hover {
background-color: var(--dark_775_color);
}
&:active {
background-color: var(--dark_900_color);
}
}

View File

@@ -0,0 +1,61 @@
import styles from "./SidebarSection.module.scss";
export const SidebarSection = () => {
return (
<div className={styles.container}>
<div className={styles.scroll_container}>
<div className={styles.tabs_wrapper}>
<Tab tab_id="device" />
<Tab tab_id="appearance" />
<Tab tab_id="translation" />
<Tab tab_id="transcription" />
<Tab tab_id="vr" />
<Tab tab_id="others" />
<Tab tab_id="hotkeys" />
<Tab tab_id="plugins" />
<Tab tab_id="advanced_settings" />
</div>
<div className={styles.separated_tabs_wrapper}>
<Tab tab_id="supporters" />
<Tab tab_id="about_vrct" />
</div>
</div>
</div>
);
};
import clsx from "clsx";
import { useI18n } from "@useI18n";
import { useStore_SelectedConfigTabId } from "@store";
const Tab = (props) => {
const { t } = useI18n();
const { updateSelectedConfigTabId, currentSelectedConfigTabId } = useStore_SelectedConfigTabId();
const onclickFunction = () => {
updateSelectedConfigTabId(props.tab_id);
};
const tab_container_class_names = clsx(styles["tab_container"], {
[styles["is_selected"]]: (currentSelectedConfigTabId.data === props.tab_id) ? true : false
});
const switch_indicator_class_names = clsx(styles["switch_indicator"], {
[styles["is_selected"]]: (currentSelectedConfigTabId.data === props.tab_id) ? true : false
});
const getLabel = () => {
if (props.tab_id === "vr") return "VR";
if (props.tab_id === "supporters") return (
<>Supporters<span className={styles.crown_emoji}>👑</span></>
);
if (props.tab_id === "about_vrct") return "About VRCT";
return t(`config_page.side_menu_labels.${props.tab_id}`);
};
return (
<div className={tab_container_class_names} onClick={onclickFunction}>
<p className={styles.tab_text}>{getLabel()}</p>
<div className={switch_indicator_class_names}></div>
</div>
);
};

View File

@@ -0,0 +1,79 @@
.container {
width: var(--config_page_sidebar_width);
flex-shrink: 0;
padding: 0rem 0rem 5.8rem 1.2rem;
}
.scroll_container {
display: flex;
flex-direction: column;
justify-content: space-between;
overflow-y: auto;
// overflow-x: hidden;
height: 100%;
max-height: 60rem;
}
.tabs_wrapper {
width: 100%;
display: flex;
flex-direction: column;
gap: 0.1rem;
flex: 1;
}
.tab_container {
position: relative;
width: 100%;
display: flex;
justify-content: left;
align-items: center;
padding: 0.8rem 0 0.8rem 1rem;
cursor: pointer;
&:hover {
background-color: var(--dark_800_color);
}
&:active {
background-color: var(--dark_900_color);
}
&.is_selected {
background-color: inherit;
color: var(--primary_200_color);
cursor: default;
pointer-events: none;
}
}
.switch_indicator {
display: none;
&.is_selected {
display: block;
position: absolute;
top: 50%;
left: 0rem;
transform: translate(-50%, -50%);
width: 0.2rem;
height: 2.6rem;
border-radius: 0.1rem;
background-color: var(--primary_300_color);
}
}
.tab_text {
// overflow: hidden;
font-size: 1.6rem;
// text-overflow: ellipsis;
position: relative;
}
.crown_emoji {
font-size: 1.6rem;
position: absolute;
top: 40%;
left: 100%;
transform: translateY(-50%);
padding-left: 0.4rem;
}
.separated_tabs_wrapper {
// padding-bottom: 1.2rem;
}

View File

@@ -0,0 +1,38 @@
import { useI18n } from "@useI18n";
import clsx from "clsx";
import styles from "./Topbar.module.scss";
import { useIsOpenedConfigPage } from "@logics_common";
import ArrowLeftSvg from "@images/arrow_left.svg?react";
import { TitleBox } from "./title_box/TitleBox";
import { SectionTitleBox } from "./section_title_box/SectionTitleBox";
import { CompactSwitchBox } from "./compact_switch_box/CompactSwitchBox";
export const Topbar = () => {
const { t } = useI18n();
const { currentIsOpenedConfigPage, setIsOpenedConfigPage } = useIsOpenedConfigPage();
const closeConfigPage = () => {
setIsOpenedConfigPage(false);
};
return (
<div className={clsx(styles.container, {
[styles.show_config]: currentIsOpenedConfigPage.data,
[styles.show_main]: !currentIsOpenedConfigPage.data
})}>
<div className={styles.wrapper} onClick={() => closeConfigPage()}>
<div className={styles.go_back_button}>
<ArrowLeftSvg className={styles.arrow_left_svg} />
</div>
<div className={styles.go_back_text_wrapper}>
<p className={styles.go_back_text}>{t("common.go_back_button_label")}</p>
</div>
{/* <TitleBox />
<SectionTitleBox />
<CompactSwitchBox /> */}
</div>
</div>
);
};

View File

@@ -0,0 +1,82 @@
.container {
width: 100%;
height: 0%;
transition: top 0.5s ease;
position: absolute;
top: 0;
left: 0;
}
.show_config.container {
top: 0;
}
.show_main.container {
top: 100%;
}
.wrapper {
background-color: var(--dark_850_color);
cursor: pointer;
height: 1rem;
width: 100%;
position: relative;
transition: all 0.3s ease;
&:hover {
height: 2rem;
.go_back_button {
top: -12rem;
transform: rotate(35deg);
}
.arrow_left_svg {
color: var(--dark_400_color);
}
.go_back_text {
color: var(--dark_400_color);
}
.go_back_text_wrapper {
padding-top: 8.2rem;
}
}
}
.go_back_button {
height: 16rem;
width: 16rem;
border-radius: 1.2rem;
background-color: var(--dark_850_color);
position: absolute;
top: -13.2rem;
right: 10rem;
transform: rotate(30deg);
transition: all 0.3s ease;
}
.arrow_left_svg {
height: 2.8rem;
position: absolute;
right: 1rem;
bottom: 0.4rem;
color: var(--dark_600_color);
transform: rotate(-100deg);
}
.go_back_text_wrapper {
position: absolute;
top: -4.6rem;
right: 0rem;
padding-top: 7.2rem;
padding-bottom: 1rem;
width: 15.2rem;
transition: all 0.3s ease;
}
.go_back_text {
color: var(--dark_650_color);
font-size: 1.6rem;
padding-left: 4rem;
transition: all 0.3s ease;
}

View File

@@ -0,0 +1,12 @@
import { useI18n } from "@useI18n";
import styles from "./CompactSwitchBox.module.scss";
export const CompactSwitchBox = () => {
const { t } = useI18n();
return (
<div className={styles.container}>
<p>{t("config_page.compact_mode")}</p>
</div>
);
};

View File

@@ -0,0 +1,9 @@
.container {
// flex: 0;
// width: 100%;
width: 14rem;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}

View File

@@ -0,0 +1,13 @@
import { useI18n } from "@useI18n";
import styles from "./SectionTitleBox.module.scss";
import { useStore_SelectedConfigTabId } from "@store";
export const SectionTitleBox = () => {
const { t } = useI18n();
const { currentSelectedConfigTabId } = useStore_SelectedConfigTabId();
return (
<div className={styles.container}>
<p className={styles.title}>{t(`config_page.side_menu_labels.${currentSelectedConfigTabId.data}`)}</p>
</div>
);
};

View File

@@ -0,0 +1,13 @@
.container {
flex: 1;
width: 100%;
height: 100%;
display: flex;
justify-content: left;
align-items: center;
padding-left: 2rem;
}
.title {
font-size: 2.2rem;
}

View File

@@ -0,0 +1,14 @@
import { useI18n } from "@useI18n";
import styles from "./TitleBox.module.scss";
import chato_img from "@images/chato_white.png";
export const TitleBox = () => {
const { t } = useI18n();
return (
<div className={styles.container}>
<img src={chato_img} className={styles.logo_chato} alt="VRCT logo chato" />
<p className={styles.title}>{t("config_page.config_title")}</p>
</div>
);
};

View File

@@ -0,0 +1,19 @@
.container {
// flex: 0;
width: var(--config_page_sidebar_width);
height: 100%;
display: flex;
justify-content: left;
align-items: center;
padding-left: 2.6rem;
gap: 1.4rem;
}
.logo_chato {
width: 3.2rem;
padding-top: 0.6rem;
}
.title {
font-size: 2.2rem;
}

View File

@@ -0,0 +1,48 @@
import { useI18n } from "@useI18n";
import { useState } from "react";
import clsx from "clsx";
import styles from "./VersionLabel.module.scss";
import { useSoftwareVersion, useComputeMode } from "@logics_common";
import CopySvg from "@images/copy.svg?react";
import CheckMarkSvg from "@images/check_mark.svg?react";
export const VersionLabel = () => {
const [is_copied, setIsCopied] = useState(false);
const { t } = useI18n();
const { currentSoftwareVersion } = useSoftwareVersion();
const { currentComputeMode } = useComputeMode();
const version_label = currentComputeMode.data === "cpu"
? t("config_page.common.version", { version: currentSoftwareVersion.data })
: currentComputeMode.data === "cuda"
? t("config_page.common.version", { version: currentSoftwareVersion.data }) + " CUDA"
: t("config_page.common.version", { version: currentSoftwareVersion.data });
const is_cpu = currentComputeMode.data === "cpu";
const copyToClipboard = async () => {
if (is_copied) return;
const copy_text = is_cpu ? `${currentSoftwareVersion.data}` : `${currentSoftwareVersion.data} CUDA`;
await navigator.clipboard.writeText(copy_text);
setIsCopied(true);
setTimeout(() => {
setIsCopied(false);
}, 1000);
};
return (
<div className={styles.container}>
<div className={clsx(styles.wrapper, {[styles.is_copied]: is_copied})} onClick={copyToClipboard}>
<p className={styles.version_label}>{version_label}</p>
{is_copied
? <CheckMarkSvg className={styles.check_mark_svg}/>
: <CopySvg className={styles.copy_svg}/>
}
</div>
</div>
);
};

View File

@@ -0,0 +1,33 @@
.container {
position: absolute;
bottom: 1.2rem;
left: 1.4rem;
font-size: 1.2rem;
color: var(--dark_400_color);
}
.wrapper {
position: relative;
display: flex;
align-items: center;
gap: 0.6rem;
cursor: pointer;
&.is_copied {
cursor: default;
}
}
.version_label {
font-size: 1.2rem;
color: var(--dark_400_color);
}
.copy_svg {
width: 1.4rem;
color: var(--dark_500_color);
}
.check_mark_svg {
width: 1.4rem;
color: var(--primary_300_color);
}