[Update] Combine in one window. Rename Main/Config Window to Main/Config Page accordingly.

For now, I put Config Page to below the main page temporary. Open Config Button does not work.
This commit is contained in:
Sakamoto Shiina
2024-08-09 12:23:17 +09:00
parent af1cdd2d7b
commit 3ed3d5eb40
118 changed files with 175 additions and 192 deletions

35
src-ui/app/App.jsx Normal file
View File

@@ -0,0 +1,35 @@
import { getCurrent } from "@tauri-apps/api/window";
import { useEffect, useRef } from "react";
import { useStartPython } from "@logics/useStartPython";
import { useConfig } from "@logics/useConfig";
import { MainPage } from "./main_page/MainPage";
import { ConfigPage } from "./config_page/ConfigPage";
export const App = () => {
const { asyncStartPython } = useStartPython();
const hasRunRef = useRef(false);
const main_page = getCurrent();
const { getSoftwareVersion } = useConfig();
useEffect(() => {
main_page.setDecorations(true);
if (!hasRunRef.current) {
asyncStartPython().then((result) => {
getSoftwareVersion();
}).catch((err) => {
});
}
return () => hasRunRef.current = true;
}, []);
return (
<>
<MainPage/>
<ConfigPage/>
</>
);
};

View File

@@ -0,0 +1,29 @@
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 { useSoftwareVersion } from "@store";
import { useTranslation } from "react-i18next";
// import { useConfig } from "@logics/useConfig";
export const ConfigPage = () => {
const { currentSoftwareVersion, updateSoftwareVersion } = useSoftwareVersion();
const { t } = useTranslation();
return (
<div className={styles.container}>
<Topbar />
<div className={styles.main_container}>
<SidebarSection />
<SettingSection />
</div>
<p className={styles.software_version}>
{
t("config_page.version", {version: currentSoftwareVersion})
}
</p>
</div>
);
};

View File

@@ -0,0 +1,24 @@
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: var(--dark_950_color);
overflow: hidden;
position: relative;
}
.main_container {
width: 100%;
height: 100%;
display: flex;
padding-top: var(--config_page_topbar_height);
}
.software_version {
position: absolute;
bottom: 0.8rem;
left: 1.2rem;
font-size: 1.4rem;
}

View File

@@ -0,0 +1,12 @@
import styles from "./SettingSection.module.scss";
import { SettingBox } from "./setting_box/SettingBox";
export const SettingSection = () => {
return (
<div className={styles.scroll_container}>
<div className={styles.container}>
<SettingBox />
</div>
</div>
);
};

View File

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

View File

@@ -0,0 +1,17 @@
import { useSelectedConfigTabId } from "@store";
import { Appearance } from "./appearance/Appearance";
import { AboutVrct } from "./about_vrct/AboutVrct";
export const SettingBox = () => {
const { currentSelectedConfigTabId } = useSelectedConfigTabId();
switch (currentSelectedConfigTabId) {
case "appearance":
return <Appearance />;
case "about_vrct":
return <AboutVrct />;
default:
return null;
}
};

View File

@@ -0,0 +1,135 @@
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 contributors_members from "@images/about_vrct/contributors_members.png";
import localization_section_title from "@images/about_vrct/localization_section_title.png";
import localization_members from "@images/about_vrct/localization_members.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 vrchat_disclaimer from "@images/about_vrct/vrchat_disclaimer.png";
import clsx from "clsx";
import { useTranslation } from "react-i18next";
import { useUiLanguageStatus } from "@store";
import { PosterShowcaseContents } from "./poster_showcase_contents/PosterShowcaseContents";
export const AboutVrct = () => {
const { t } = useTranslation();
const { currentUiLanguageStatus } = useUiLanguageStatus();
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}>
<img src={vrct_logo_for_about_vrct} className={styles.about_vrct_logo} />
<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}>
<img src={contributors_members} className={clsx(styles.contributors_img, styles.contributors)} />
<OpenLinkContainer className={styles.contributors_done_san_x} href_id="contributors_done_san_x" />
<OpenLinkContainer className={styles.contributors_iya_x} href_id="contributors_iya_x" />
<OpenLinkContainer className={styles.contributors_rera_x} href_id="contributors_rera_x" />
<OpenLinkContainer className={styles.contributors_rera_github} href_id="contributors_rera_github" />
<OpenLinkContainer className={styles.contributors_poposuke_x} href_id="contributors_poposuke_x" />
<OpenLinkContainer className={styles.contributors_kumaguma_x} href_id="contributors_kumaguma_x" />
</div>
</div>
<div className={styles.localization_section}>
<img src={localization_section_title} className={clsx(styles.section_title, styles.localization)} />
<img src={localization_members} className={clsx(styles.localization_members_img, styles.localization)} />
</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} />
{
currentUiLanguageStatus === "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}>
<img src={vrchat_disclaimer} className={styles.vrchat_disclaimer} />
</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" },
};
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,171 @@
.container {
display: flex;
gap: 2.2rem;
flex-direction: column;
width: 72rem;
// background-color: gray;
}
.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;
justify-content: space-between;
}
.dev_card_wrapper {
width: 34.6rem;
position: relative;
}
.dev_card_img {
width: 100%;
}
@mixin dev_sns_styles($right) {
position: absolute;
right: $right;
bottom: 1.2rem;
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(3rem);
}
.project_links_and_logo_section {
display: flex;
flex-direction: row;
justify-content: space-between;
text-align: center;
padding: 0 5.5rem;
}
.about_vrct_logo {
width: 20rem;
object-fit: contain;
}
.project_links_wrapper {
display: flex;
flex-direction: column;
gap: 0.2rem;
align-items: start;
}
.project_link {
height: 2.6rem;
padding: 0.4rem 1rem;
border-radius: 0.4rem;
&:hover {
background-color: var(--dark_850_color);
}
&:active {
background-color: var(--dark_900_color)
}
}
.contributors_img_wrapper {
position: relative;
}
.contributors_img {
width: 100%;
}
@mixin contributors_sns_styles($top, $left) {
position: absolute;
left: $left;
top: $top;
width: 2.4rem;
padding: 0.4rem;
border-radius: 0.4rem;
&:hover {
background-color: var(--dark_825_color);
}
&:active {
background-color: var(--dark_888_color)
}
}
$first_line_top: 6.2rem;
.contributors_done_san_x {
@include contributors_sns_styles($first_line_top, 2rem);
}
.contributors_iya_x {
@include contributors_sns_styles($first_line_top, 27.6rem);
}
.contributors_rera_x {
@include contributors_sns_styles($first_line_top, 52.2rem);
}
.contributors_rera_github {
@include contributors_sns_styles($first_line_top, 55rem);
}
$second_line_top: 16.6rem;
.contributors_poposuke_x {
@include contributors_sns_styles($second_line_top, 14.8rem);
}
.contributors_kumaguma_x {
@include contributors_sns_styles($second_line_top, 40.8rem);
}
.localization_section {
display: flex;
flex-direction: column;
}
.localization_members_img {
width: 100%;
}
.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;
}
.vrchat_disclaimer {
width: 100%;
margin-top: 8rem;
}

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,6 @@
.container {
display: flex;
justify-content: space-between;
align-items: start;
gap: 2rem;
}

View File

@@ -0,0 +1,91 @@
import clsx from "clsx";
import styles from "./PosterShowcaseWorldsContents.module.scss";
import { usePosterShowcaseWorldPageIndex } 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.includes(file_name));
return imagePath ? images[imagePath]?.default : null;
};
import poster_showcase_worlds_settings from "./poster_showcase_worlds_settings";
import { chunkArray } from "@utils/chunkArray";
export const PosterShowcaseWorldsContents = () => {
const { currentPosterShowcaseWorldPageIndex } = usePosterShowcaseWorldPageIndex();
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];
return (
<div className={styles.container}>
<div className={styles.poster_showcase_world_container}>
{target_poster_showcase_world_images.map((poster, index) => {
let 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) {
content = (
<a href={`https://twitter.com/Shiina_12siy/status/${poster.x_post_num}`} target="_blank" rel="noreferrer" >
{content}
</a>
);
}
const class_names = clsx(styles.poster_showcase_world_wrapper, {
[styles.clickable]: (poster.x_post_num !== null)
});
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/randomIntMinMax";
const PosterShowcaseWorldsPagination = ({ page_length }) => {
const { currentPosterShowcaseWorldPageIndex, updatePosterShowcaseWorldPageIndex } = usePosterShowcaseWorldPageIndex();
useEffect(() => {
updatePosterShowcaseWorldPageIndex(randomIntMinMax(page_length -1));
},[page_length]);
const setPage = (index) => {
updatePosterShowcaseWorldPageIndex(index);
};
const getClassNames = (index, baseClass) => clsx(baseClass, {
[styles.is_active]: (currentPosterShowcaseWorldPageIndex === 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,126 @@
.container {
display: flex;
flex-direction: column;
flex: 1;
gap: 1rem;
justify-content: space-between;
}
$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;
}
.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,63 @@
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: null },
// 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_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: null },
// 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: null },
// 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: null },
// 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: null },
// MiMi_Sorahana # VRC日韓交流会 (KRJPEX.1355)
{ image_file_name: "kr_jp_exchange", x_post_num: null },
// Einアイン
{ image_file_name: "smokerz_guild_v2", 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 { useUiLanguageStatus } from "@store";
import { useVrctPosterIndex } 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 } = useVrctPosterIndex();
const { currentUiLanguageStatus } = useUiLanguageStatus();
const updateIndex = (delta) => {
const newIndex = (currentVrctPosterIndex + delta + poster_images.length) % poster_images.length;
updateVrctPosterIndex(newIndex);
};
const current_poster = poster_images[currentVrctPosterIndex];
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>
{
currentUiLanguageStatus === "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,46 @@
.poster_pagination_container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.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,68 @@
import { useTranslation } from "react-i18next";
import FolderOpenSvg from "@images/folder_open.svg?react";
import { useSettingBox } from "../components/useSettingBox";
import { useSelectedMicDeviceStatus, useMicDeviceListStatus } from "@store";
export const Appearance = () => {
const { t } = useTranslation();
const { currentSelectedMicDeviceStatus, updateSelectedMicDeviceStatus } = useSelectedMicDeviceStatus();
const { currentMicDeviceListStatus } = useMicDeviceListStatus();
const {
DropdownMenuContainer,
SliderContainer,
CheckboxContainer,
SwitchboxContainer,
EntryContainer,
ThresholdContainer,
RadioButtonContainer,
DeeplAuthKeyContainer,
MessageFormatContainer,
WordFilterContainer,
ActionButtonContainer,
} = useSettingBox();
const selectFunction = (selected_data) => {
const asyncFunction = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(selected_data.selected_id);
}, 3000);
});
};
updateSelectedMicDeviceStatus(asyncFunction);
};
return (
<>
<DropdownMenuContainer dropdown_id="mic_host" label="Mic Host/Driver" desc="description" selected_id="b" list={{a: "A", b: "B", c: "C"}} />
<DropdownMenuContainer dropdown_id="mic_device" label="Mic Device" desc="description" selected_id={currentSelectedMicDeviceStatus.data} list={currentMicDeviceListStatus} selectFunction={selectFunction} state={currentSelectedMicDeviceStatus.state} />
<SliderContainer label="Transparent" desc="description" min="0" max="3000"/>
<CheckboxContainer label="Transparent" desc="description" checkbox_id="checkbox_id_1"/>
<SwitchboxContainer label="Transparent" desc="description" switchbox_id="switchbox_id_1"/>
<RadioButtonContainer label="Transparent" desc="description" switchbox_id="radiobutton_id_1"/>
<EntryContainer width="20rem" label="Transparent" desc="description" switchbox_id="entry_id_1"/>
<ThresholdContainer label="Transparent" desc="description" id="mic_threshold" min="0" max="3000"/>
<DeeplAuthKeyContainer label={t(`config_page.deepl_auth_key.label`)} desc={t(`config_page.deepl_auth_key.desc`)}/>
<MessageFormatContainer label={t(`config_page.send_message_format.label`)} desc={t(`config_page.send_message_format.desc`)} id="send"/>
<MessageFormatContainer label={t(`config_page.send_message_format_with_t.label`)} desc={t(`config_page.send_message_format_with_t.desc`)} id="send_with_t"/>
<MessageFormatContainer label={t(`config_page.send_message_format.label`)} desc={t(`config_page.send_message_format.desc`)} id="received"/>
<MessageFormatContainer label={t(`config_page.send_message_format_with_t.label`)} desc={t(`config_page.send_message_format_with_t.desc`)} id="received_with_t"/>
<WordFilterContainer label={t(`config_page.mic_word_filter.label`)} desc={t(`config_page.mic_word_filter.desc`)}/>
<ActionButtonContainer label={t(`config_page.open_config_filepath.label`)} IconComponent={FolderOpenSvg} OnclickFunction={()=>{}}/>
</>
);
};

View File

@@ -0,0 +1,40 @@
import React, { useState, useRef, forwardRef, useImperativeHandle } from "react";
import styles from "./_Entry.module.scss";
const _Entry = forwardRef(({ width, onChange, initialValue = "" }, ref) => {
const [input_value, setInputValue] = useState(initialValue);
const inputRef = useRef();
const onChangeFunction = (e) => {
setInputValue(e.currentTarget.value);
if (onChange) {
onChange(e);
}
};
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return (
<div className={styles.entry_container}>
<div
className={styles.entry_wrapper}
style={{ width }}
>
<input
ref={inputRef}
className={styles.entry_input_area}
value={input_value}
onChange={onChangeFunction}
/>
</div>
</div>
);
});
_Entry.displayName = "_Entry";
export { _Entry };

View File

@@ -0,0 +1,19 @@
.entry_container {
width: 100%;
}
.entry_wrapper {
width: 10rem;
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;
}

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,14 @@
.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;
}

View File

@@ -0,0 +1,28 @@
import React, { useState } from 'react';
import styles from "./Checkbox.module.scss";
export const Checkbox = (props) => {
const [isChecked, setIsChecked] = useState(false);
const handleCheckboxChange = () => {
setIsChecked(!isChecked);
};
return (
<div className={styles.checkbox_container}>
<label className={styles.checkbox_wrapper} htmlFor={props.checkbox_id}>
<input
type="checkbox"
id={props.checkbox_id}
checked={isChecked}
onChange={handleCheckboxChange}
/>
<span className={styles.cbx}>
<svg viewBox="0 0 12 12">
<polyline points="1 6.29411765 4.5 10 11 1"></polyline>
</svg>
</span>
</label>
</div>
);
};

View File

@@ -0,0 +1,53 @@
.checkbox_container {
width: 100%;
height: 100%;
display: flex;
justify-content: end;
align-items: center;
}
.checkbox_wrapper {
display: inline-block;
cursor: pointer;
padding: 2rem;
&:hover {
& .cbx {
border: var(--primary_600_color) solid 0.2rem;
}
}
}
.checkbox_wrapper .cbx {
display: block;
width: 2.8rem;
height: 2.8rem;
border-radius: 0.4rem;
border: var(--dark_700_color) solid 0.2rem;
transition: all 0.15s ease;
padding: 0.4rem;
}
.checkbox_wrapper .cbx svg {
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
stroke: var(--dark_basic_text_color);
stroke-width: 0.2rem;
stroke-dasharray: 1.7rem;
stroke-dashoffset: 1.7rem;
}
.checkbox_wrapper input[type="checkbox"] {
display: none;
visibility: hidden;
}
.checkbox_wrapper input[type="checkbox"]:checked + .cbx {
background-color: var(--primary_600_color);
border: none;
}
.checkbox_wrapper input[type="checkbox"]:checked + .cbx svg {
stroke-dashoffset: 0;
transition: all 0.15s ease;
}

View File

@@ -0,0 +1,53 @@
import styles from "./DeeplAuthKey.module.scss";
import { useTranslation } from "react-i18next";
import clsx from "clsx";
import ExternalLink from "@images/external_link.svg?react";
import { _Entry } from "../_atoms/_entry/_Entry";
import { useState, useRef } from "react";
export const DeeplAuthKey = () => {
const { t } = useTranslation();
const [is_editable, seIsEditable] = useState(false);
const [input_value, seInputValue] = useState("");
const entryRef = useRef(null);
const revealEditAuthKey = () => {
seIsEditable(true);
entryRef.current.focus();
};
const onchangeEntryAuthKey = (e) => {
seInputValue(e.target.value);
};
const saveAuthKey = () => {
console.log(input_value);
};
return (
<div className={styles.container}>
<div className={styles.entry_section_wrapper}>
<_Entry ref={entryRef} width="30rem" onChange={onchangeEntryAuthKey}/>
<button className={styles.save_button} onClick={saveAuthKey}>Save</button>
{is_editable
? null
:
<div className={styles.entry_edit_cover} onClick={revealEditAuthKey}>
<button className={styles.edit_button}>Edit</button>
</div>
}
</div>
</div>
);
};
export const OpenWebpage_DeeplAuthKey = () => {
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}>Open DeepL Account Webpage</p>
<ExternalLink className={styles.external_link_svg} />
</a>
</div>
);
};

View File

@@ -0,0 +1,90 @@
.container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1rem;
}
.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: (#00000044);
backdrop-filter: blur(4rem);
border: solid 0.1rem var(--dark_700_color);
&:hover {
background-color: (#00000088);
}
&:active {
backdrop-filter: blur(1.4rem);
}
}
.edit_button {
padding: 0.8rem 1.2rem;
color: var(--dark_basic_text_color);
height: 100%;
width: 100%;
font-size: 1.4rem;
text-align: center;
}
.save_button {
padding: 0.8rem 1.2rem;
background-color: var(--primary_600_color);
color: var(--dark_basic_text_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);
}
}
.open_webpage_button_wrapper {
display: flex;
justify-content: center;
align-items: center;
}
.open_webpage_button {
padding: 0.6rem 3.2rem;
display: flex;
gap: 1rem;
justify-content: center;
align-items: center;
border-radius: 0.4rem;
cursor: pointer;
&:hover {
background-color: var(--dark_825_color);
}
&:active {
background-color: var(--dark_900_color);
}
}
.open_webpage_text {
font-size: 1.2rem;
color: var(--dark_basic_text_color);
}
.external_link_svg {
color: var(--dark_500_color);
width: 1.6rem;
}

View File

@@ -0,0 +1,65 @@
import styles from "./DropdownMenu.module.scss";
import clsx from "clsx";
import ArrowLeftSvg from "@images/arrow_left.svg?react";
import { useIsOpenedDropdownMenu } from "@store";
export const DropdownMenu = (props) => {
const { updateIsOpenedDropdownMenu, currentIsOpenedDropdownMenu } = useIsOpenedDropdownMenu();
const toggleDropdownMenu = () => {
if (currentIsOpenedDropdownMenu === props.dropdown_id) {
updateIsOpenedDropdownMenu("");
} else {
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 === props.dropdown_id) ? true : false
});
const dropdown_toggle_button_class_name = clsx(styles["dropdown_toggle_button"], {
[styles["is_loading"]]: (props.state === "loading") ? true : false
});
const arrow_class_names = clsx(styles["arrow_left_svg"], {
[styles["is_opened"]]: (currentIsOpenedDropdownMenu === props.dropdown_id) ? true : false
});
return (
<div className={styles.container}>
<div className={dropdown_toggle_button_class_name} onClick={toggleDropdownMenu}>
{(props.state === "loading")
? <p className={styles.dropdown_selected_text}>Loading...</p>
: <p className={styles.dropdown_selected_text}>{props.list[props.selected_id]}</p>
}
{(props.state === "loading")
? <span className={styles.loader}></span>
: <ArrowLeftSvg className={arrow_class_names} />
}
</div>
<div className={dropdown_content_wrapper_class_name}>
<div className={styles.dropdown_content}>
{
Object.entries(props.list).map(([key, value]) => {
return (
<div key={key} className={styles.value_button} onClick={() => selectValue(key)}>
<p className={styles.value_text} >{value}</p>
</div>
);
})
}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,78 @@
@import "@scss_mixins";
.container {
position: relative;
}
.dropdown_toggle_button {
position: relative;
background-color: var(--dark_925_color);
min-width: 20rem;
padding: 0.6rem 1rem;
cursor: pointer;
border-radius: 0.4rem;
&:hover {
background-color: var(--dark_850_color);
}
&:active {
background-color: var(--dark_950_color);
}
&.is_loading {
pointer-events: none;
}
}
.dropdown_selected_text {
font-size: 1.4rem;
}
.dropdown_content_wrapper {
display: none;
position: absolute;
top: 100%; // Position it below the toggle button
left: 0;
width: 100%;
z-index: 1;
&.is_opened {
display: block;
}
}
.dropdown_content {
background-color: var(--dark_900_color);
border: 0.1rem solid var(--dark_600_color);
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.value_button {
background-color: var(--dark_875_color);
padding: 0.8rem;
cursor: pointer;
&:hover {
background-color: var(--dark_800_color);
}
&:active {
background-color: var(--dark_900_color);
}
}
.value_text {
font-size: 1.2rem;
}
.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,15 @@
import styles from "./Entry.module.scss";
import { _Entry } from "../_atoms/_entry/_Entry";
export const Entry = ({width}) => {
const handleInputChange = (e) => {
console.log(e.currentTarget.value);
};
return (
<div className={styles.entry_container}>
<_Entry width={width} onChange={handleInputChange} initialValue="" />
</div>
);
};

View File

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

View File

@@ -0,0 +1,17 @@
.label_component {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.label {
font-size: 1.6rem;
font-weight: 400;
color: var(--dark_basic_text_color);
}
.desc {
font-size: 1.4rem;
font-weight: 300;
color: var(--dark_500_color);
}

View File

@@ -0,0 +1,180 @@
import styles from "./MessageFormat.module.scss";
import { useTranslation } from "react-i18next";
import {
useUiLanguageStatus,
useSendMessageFormat,
useSendMessageFormatWithT,
useReceivedMessageFormat,
useReceivedMessageFormatWithT,
} from "@store";
import { _Entry } from "../_atoms/_entry/_Entry";
import SwapImg from "@images/swap_icon.png";
export const MessageFormat = (props) => {
const { currentSendMessageFormat, updateSendMessageFormat } = useSendMessageFormat();
const { currentSendMessageFormatWithT, updateSendMessageFormatWithT } = useSendMessageFormatWithT();
const { currentReceivedMessageFormat, updateReceivedMessageFormat } = useReceivedMessageFormat();
const { currentReceivedMessageFormatWithT, updateReceivedMessageFormatWithT } = useReceivedMessageFormatWithT();
let atoms = [];
switch (props.id) {
case "send":
atoms = [currentSendMessageFormat, updateSendMessageFormat];
break;
case "send_with_t":
atoms = [currentSendMessageFormatWithT, updateSendMessageFormatWithT];
break;
case "received":
atoms = [currentReceivedMessageFormat, updateReceivedMessageFormat];
break;
case "received_with_t":
atoms = [currentReceivedMessageFormatWithT, updateReceivedMessageFormatWithT];
break;
default:
break;
}
return (
<div className={styles.container}>
<ExampleComponent {...props} current_format={atoms[0]} />
<InputComponent {...props} atoms={atoms} />
{["send_with_t", "received_with_t"].includes(props.id) && <SwapButton atoms={atoms} />}
</div>
);
};
const ExampleComponent = ({ id, current_format }) => {
const { t } = useTranslation();
const { currentUiLanguageStatus } = useUiLanguageStatus();
const createExampleMessage = () => {
const originalLangMessage = t("config_page.send_message_format.example_text");
let format = current_format;
if (["send_with_t", "received_with_t"].includes(id)) {
const translationLocale = currentUiLanguageStatus === "en" ? "ja" : "en";
const translatedLangMessage = t("config_page.send_message_format.example_text", { lng: translationLocale });
return format.is_message_first
? `${format.before}${originalLangMessage}${format.between}${translatedLangMessage}${format.after}`
: `${format.before}${translatedLangMessage}${format.between}${originalLangMessage}${format.after}`;
} else {
return `${format.before}${originalLangMessage}${format.after}`;
}
};
return (
<div className={styles.example_container}>
<p className={styles.example_text}>{createExampleMessage()}</p>
</div>
);
};
const InputComponent = ({ id, atoms }) => {
const [current, updater] = atoms;
const handleChange = (key) => (e) => {
updater({ ...current, [key]: e.target.value });
};
return (
<div className={styles.input_wrapper}>
<_Entry width="100%" onChange={handleChange("before")} />
{["send_with_t", "received_with_t"].includes(id) ? (
<>
<p className={styles.preset_text}>{current.is_message_first ? "[message]" : "[translation]"}</p>
<_Entry width="100%" onChange={handleChange("between")} />
<p className={styles.preset_text}>{current.is_message_first ? "[translation]" : "[message]"}</p>
</>
) : (
<p className={styles.preset_text}>[message]</p>
)}
<_Entry width="100%" onChange={handleChange("after")} />
</div>
);
};
const SwapButton = ({ atoms }) => {
const [current, updater] = atoms;
const swapMessageAndTranslate = () => {
updater({ ...current, is_message_first: !current.is_message_first });
};
return (
<div className={styles.swap_button_container}>
<div className={styles.swap_button_wrapper} onClick={swapMessageAndTranslate}>
<p className={styles.swap_text}>{current.is_message_first ? "[message]" : "[translation]"}</p>
<img className={styles.swap_img} src={SwapImg} alt="Swap Icon" />
<p className={styles.swap_text}>{current.is_message_first ? "[translation]" : "[message]"}</p>
</div>
</div>
);
};
// const extractMessageFormat = (text) => {
// const split_result = text.split("[message]");
// let result_data = {
// before: split_result[0],
// after: split_result[1]
// };
// return result_data;
// };
// const extractMessageFormatWithT = (text) => {
// const message_index = text.indexOf("[message]");
// const translation_index = text.indexOf("[translation]");
// let result_data = {
// is_message_first: true,
// before: "",
// between: "",
// after: ""
// };
// if (message_index < translation_index) {
// const text_before_message = text.slice(0, message_index);
// result_data.before = text_before_message;
// const match = text.match(/\[message\](.*?)\[translation\]/);
// if (match) {
// const extracted_text = match[1];
// result_data.between = extracted_text;
// } else {
// throw new Error("Invalid Message Format");
// }
// const text_after_translation = text.slice(translation_index + "[translation]".length);
// result_data.after = text_after_translation;
// } else if (translation_index < message_index) {
// result_data.is_message_first = false;
// const text_before_translation = text.slice(0, translation_index);
// result_data.before = text_before_translation;
// const match = text.match(/\[translation\](.*?)\[message\]/);
// if (match) {
// const extracted_text = match[1];
// result_data.between = extracted_text;
// } else {
// throw new Error("Invalid Message Format");
// }
// const text_after_message = text.slice(message_index + "[message]".length);
// result_data.after = text_after_message;
// } else {
// throw new Error("Invalid Message Format");
// }
// return result_data;
// };

View File

@@ -0,0 +1,65 @@
.container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
gap: 1.6rem;
}
.example_container {
padding: 1rem;
background-color: #3A4554;
border-radius: 1.4rem;
max-width: 30rem;
text-align: center;
}
.example_text {
font-size: 1.4rem;
}
.input_wrapper {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
.preset_text {
font-size: 1.6rem;
width: 40rem;
text-align: center;
}
.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;
}

View File

@@ -0,0 +1,30 @@
import clsx from "clsx";
import { useState } from "react";
import styles from "./RadioButton.module.scss";
export const RadioButton = (props) => {
const options = [
{ radio_button_id: "1", label: "AAAAAAAA" },
{ radio_button_id: "2", label: "BBBBBB" }
];
const changeValue = (e) => {
console.log(e.target.value);
};
return (
<div className={styles.container}>
{options.map((option) => (
<label key={option.radio_button_id} className={styles.radio_button_wrapper}>
<input
type="radio"
name="radio"
value={option.radio_button_id}
onChange={changeValue}
/>
<p className={styles.radio_button_label}>{option.label}</p>
</label>
))}
</div>
);
};

View File

@@ -0,0 +1,41 @@
.container {
display: flex;
flex-direction: column;
justify-content: center;
gap: 0.4rem;
}
.radio_button_wrapper {
display: flex;
align-items: center;
cursor: pointer;
gap: 1rem;
padding: 0.6rem 0.8rem;
border-radius: 0.4rem;
&:hover {
background-color: var(--dark_800_color);
}
&:active {
background-color: var(--dark_850_color);
}
}
input[type="radio"] {
appearance: none;
margin: 0;
width: 2.4rem;
height: 2.4rem;
border: 0.3rem solid var(--dark_600_color);
border-radius: 50%;
transition: border-color .1s ease, border-width .1s ease;
cursor: inherit;
&:checked {
border-color: var(--primary_400_color);
border-width: 0.6rem;
}
}
.radio_button_label {
font-size: 1.4rem;
font-weight: 300;
}

View File

@@ -0,0 +1,55 @@
import React, { useState, useEffect } from "react";
import styles from "./Slider.module.scss";
import MUI_Slider from "@mui/material/Slider";
export const Slider = ({ min, max }) => {
const [baseColor, setBaseColor] = useState("");
const [activeColor, setActiveColor] = useState("");
const [toolTipColor, setToolTipColor] = useState("");
useEffect(() => {
const baseColor = getComputedStyle(document.documentElement).getPropertyValue("--dark_700_color");
const activeColor = getComputedStyle(document.documentElement).getPropertyValue("--primary_600_color");
const toolTipColor = getComputedStyle(document.documentElement).getPropertyValue("--dark_800_color");
setBaseColor(baseColor.trim());
setActiveColor(activeColor.trim());
setToolTipColor(toolTipColor.trim());
}, []);
const showSliderValue = (_e, value) => {
console.log(value);
};
return (
<div className={styles.container}>
<MUI_Slider className={styles.range_slider} defaultValue={50} aria-label="Default" valueLabelDisplay="auto"
step={1}
min={Number(min)}
max={Number(max)}
onChange={showSliderValue}
sx={{
color: baseColor,
"& .MuiSlider-thumb": {
backgroundColor: activeColor,
"&:hover, &.Mui-focusVisible, &.Mui-active": {
boxShadow: "0 0 0 0.8rem" + activeColor + "44",
},
"& .MuiSlider-valueLabel": {
fontSize: "1.4rem",
backgroundColor: toolTipColor,
padding: "0.6rem 1rem",
lineHeight: "1.15",
top: "-1.4rem",
"&::before": {
left: "30%",
width: "1rem",
height: "1rem",
clipPath: "polygon(50% 0, 100% 100%, 0 100%)",
}
}
},
}}
/>
</div>
);
};

View File

@@ -0,0 +1,11 @@
.container {
display: flex;
flex-direction: column;
align-items: end;
justify-content: center;
width: 100%;
}
.range_slider {
max-width: 40rem;
}

View File

@@ -0,0 +1,43 @@
import clsx from "clsx";
import { useState } from "react";
import styles from "./Switchbox.module.scss";
export const Switchbox = (props) => {
const [is_turned_on, setIsTurnedOn] = useState(false);
const [is_hovered, setIsHovered] = useState(false);
const [is_mouse_down, setIsMouseDown] = useState(false);
const getClassNames = (baseClass) => clsx(baseClass, {
[styles.is_active]: (is_turned_on === true),
// [styles.is_loading]: (currentState.state === "loading"),
[styles.is_hovered]: is_hovered,
[styles.is_mouse_down]: is_mouse_down,
});
const onMouseEnter = () => setIsHovered(true);
const onMouseLeave = () => setIsHovered(false);
const onMouseDown = () => setIsMouseDown(true);
const onMouseUp = () => setIsMouseDown(false);
const toggleFunction = () => {
setIsTurnedOn(!is_turned_on);
};
return (
<div className={styles.switchbox_container}>
<div className={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>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,25 @@
@import "@scss_mixins";
.switchbox_container {
width: 100%;
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%;
}
.toggle_control {
@include toggle_control_styles;
display: flex;
justify-content: center;
align-items: center;
}

View File

@@ -0,0 +1,14 @@
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";
export const ThresholdComponent = (props) => {
return (
<div className={styles.container}>
<VolumeCheckButton {...props}/>
<SliderAndMeter {...props}/>
<ThresholdEntry/>
</div>
);
};

View File

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

View File

@@ -0,0 +1,48 @@
import { useState, useEffect } from "react";
import styles from "./SliderAndMeter.module.scss";
export const SliderAndMeter = (props) => {
const [volume, setVolume] = useState(0);
const [threshold, setThreshold] = useState(props.max / 2);
const updateVolume = () => {
setVolume(Math.random());
};
// useEffect(() => {
// const intervalId = setInterval(updateVolume, 200);
// return () => clearInterval(intervalId);
// }, []);
return (
<div className={styles.container}>
<div className={styles.meter_container}>
<div
className={styles.volume_meter}
style={{
width: `${(volume * 100)}%`,
backgroundColor: volume < (threshold / props.max) ? "var(--primary_750_color)" : "var(--primary_400_color)"
}}
/>
<input
type="range"
min={props.min}
max={props.max}
value={threshold}
onChange={(e) => setThreshold(e.target.value)}
className={styles.threshold_slider}
/>
</div>
<div className={styles.dev_info_box}>
<p>dev</p>
<button onClick={updateVolume}>Update Volume</button>
<div className={styles.volume_info}>
<span>Current Volume: {(volume * props.max).toFixed(2)}</span>
</div>
<div className={styles.threshold_info}>
<span>Threshold: {threshold}</span>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,76 @@
.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;
}
}
.volume_info, .threshold_info {
font-size: 1.2rem;
}
// for dev
.dev_info_box {
position: absolute;
top: -4rem;
display: flex;
gap: 2rem
}

View File

@@ -0,0 +1,22 @@
import { useState, useEffect } from "react";
import styles from "./ThresholdEntry.module.scss";
export const ThresholdEntry = () => {
const [input_value, setInputValue] = useState();
const onChangeFunction = (e) => {
setInputValue(e.currentTarget.value);
};
return (
<div className={styles.container}>
<div className={styles.entry_wrapper}>
<input
className={styles.entry_input_area}
onChange={onChangeFunction}
/>
</div>
</div>
);
};

View File

@@ -0,0 +1,19 @@
.container {
background-color: green;
}
.entry_wrapper {
width: 10rem;
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;
}

View File

@@ -0,0 +1,24 @@
import styles from "./VolumeCheckButton.module.scss";
import MicSvg from "@images/mic.svg?react";
import HeadphonesSvg from "@images/headphones.svg?react";
import clsx from "clsx";
export const VolumeCheckButton = (props) => {
const SvgComponent = props.id === "mic_threshold" ? MicSvg : HeadphonesSvg;
const getClassNames = (baseClass) => clsx(baseClass, {
// [styles.is_active]: (currentState.data === true),
// [styles.is_loading]: (currentState.state === "loading"),
// [styles.is_hovered]: is_hovered,
// [styles.is_mouse_down]: is_mouse_down,
});
return (
<div className={styles.container}>
<div className={getClassNames(styles.button_button)}>
<SvgComponent className={getClassNames(styles.button_svg)} />
</div>
</div>
);
};

View File

@@ -0,0 +1,14 @@
.button_button {
width: 100%;
background-color: var(--dark_800_color);
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
border-radius: 50%;
}
.button_svg {
width: 2.6rem;
color: var(--dark_350_color);
}

View File

@@ -0,0 +1,159 @@
import styles from "./useSettingBox.module.scss";
import { useIsOpenedDropdownMenu } from "@store";
import clsx from "clsx";
import { LabelComponent } from "./label_component/LabelComponent";
import { DropdownMenu } from "./dropdown_menu/DropdownMenu";
import { Slider } from "./slider/Slider";
import { Checkbox } from "./checkbox/Checkbox";
import { Switchbox } from "./switchbox/Switchbox";
import { Entry } from "./entry/Entry";
import { ThresholdComponent } from "./threshold_component/ThresholdComponent";
import { RadioButton } from "./radio_button/RadioButton";
import { OpenWebpage_DeeplAuthKey, DeeplAuthKey } from "./deepl_auth_key/DeeplAuthKey";
import { MessageFormat } from "./message_format/MessageFormat";
import { ActionButton } from "./action_button/ActionButton";
import { WordFilter, WordFilterListToggleComponent } from "./word_filter/WordFilter";
export const useSettingBox = () => {
const { updateIsOpenedDropdownMenu } = useIsOpenedDropdownMenu();
const DropdownMenuContainer = (props) => {
const onMouseLeaveFunction = () => {
updateIsOpenedDropdownMenu("");
};
return (
<div className={styles.container} onMouseLeave={onMouseLeaveFunction}>
<LabelComponent label={props.label} desc={props.desc} />
<DropdownMenu {...props}/>
</div>
);
};
const SliderContainer = (props) => {
return (
<div className={styles.container}>
<LabelComponent label={props.label} desc={props.desc} />
<Slider {...props}/>
</div>
);
};
const CheckboxContainer = (props) => {
return (
<div className={styles.container}>
<LabelComponent label={props.label} desc={props.desc} />
<Checkbox {...props}/>
</div>
);
};
const SwitchboxContainer = (props) => {
return (
<div className={styles.container}>
<LabelComponent label={props.label} desc={props.desc} />
<Switchbox {...props}/>
</div>
);
};
const EntryContainer = (props) => {
return (
<div className={styles.container}>
<LabelComponent label={props.label} desc={props.desc} />
<Entry {...props}/>
</div>
);
};
const RadioButtonContainer = (props) => {
return (
<div className={styles.container}>
<LabelComponent label={props.label} desc={props.desc} />
<RadioButton {...props}/>
</div>
);
};
const ThresholdContainer = (props) => {
return (
<div className={styles.threshold_container}>
<div className={styles.threshold_switch_section}>
<LabelComponent label={props.label} desc={props.desc} />
<Switchbox {...props}/>
</div>
<div className={styles.threshold_section}>
<ThresholdComponent {...props}/>
</div>
</div>
);
};
const DeeplAuthKeyContainer = (props) => {
return (
<div className={styles.container}>
<div className={styles.deepl_auth_key_label_section}>
<LabelComponent label={props.label} desc={props.desc} />
<OpenWebpage_DeeplAuthKey />
</div>
<DeeplAuthKey {...props}/>
</div>
);
};
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>
);
};
const ActionButtonContainer = (props) => {
return (
<div className={styles.container}>
<LabelComponent label={props.label} desc={props.desc} />
<ActionButton {...props}/>
</div>
);
};
const WordFilterContainer = (props) => {
return (
<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>
);
};
return {
DropdownMenuContainer,
SliderContainer,
CheckboxContainer,
SwitchboxContainer,
EntryContainer,
ThresholdContainer,
RadioButtonContainer,
DeeplAuthKeyContainer,
MessageFormatContainer,
WordFilterContainer,
ActionButtonContainer,
};
};

View File

@@ -0,0 +1,77 @@
.container {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
background-color: var(--dark_888_color);
padding: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
gap: 2rem;
&.flex_column {
flex-direction: column;
}
}
.label_only_section {
width: 100%;
}
.threshold_container {
display: flex;
width: 100%;
flex-direction: column;
justify-content: space-between;
align-items: center;
gap: 2rem;
background-color: var(--dark_888_color);
padding: 2rem;
}
.threshold_switch_section {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
}
.threshold_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;
background-color: var(--dark_888_color);
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,139 @@
import styles from "./WordFilter.module.scss";
import { _Entry } from "../_atoms/_entry/_Entry";
import { useState } from "react";
import { useWordFilterList, useIsOpenedWordFilterList } from "@store";
export const WordFilter = () => {
const [input_value, setInputValue] = useState();
const { currentWordFilterList, updateWordFilterList } = useWordFilterList();
const { currentIsOpenedWordFilterList, updateIsOpenedWordFilterList } = useIsOpenedWordFilterList();
const onChangeEntry = (e) => {
setInputValue(e.target.value);
};
const addWords = () => {
if (input_value === undefined) return;
const input_value_array = input_value.split(",");
let updated_list = [...currentWordFilterList];
for (let each_input_value of input_value_array) {
each_input_value = each_input_value.trim();
if (each_input_value) {
const target_item = updated_list.find((item) => item.value === each_input_value);
if (target_item === undefined) {
// Add
updated_list = [...updated_list, { value: each_input_value, is_redoable: false }];
} else {
// Update
updated_list = updated_list.map(item =>
item.value === each_input_value ? { ...item, is_redoable: false } : item
);
}
}
}
updateWordFilterList(updated_list);
updateIsOpenedWordFilterList(true);
};
const updateRedoable = (target_item_value, is_redoable) => {
updateWordFilterList((prev_list) =>
prev_list.map(item =>
item.value === target_item_value ? { ...item, is_redoable: is_redoable } : item
)
);
};
const deleteAction = (target_item_value) => {
updateRedoable(target_item_value, true);
};
const redoAction = (target_item_value) => {
updateRedoable(target_item_value, false);
};
return (
<div className={styles.container}>
{ currentIsOpenedWordFilterList &&
<div className={styles.list_section_wrapper}>
{
currentWordFilterList.map((item, index) => {
return <WordFilterItem value={item.value} key={index} is_redoable={item.is_redoable} deleteAction={deleteAction} redoAction={redoAction}/>;
})
}
</div>
}
<div className={styles.entry_section_wrapper}>
<_Entry width="30rem" onChange={onChangeEntry}/>
<button className={styles.add_button} onClick={addWords}>Add</button>
</div>
</div>
);
};
import DeleteSvg from "@images/cancel.svg?react";
import RedoSvg from "@images/redo.svg?react";
import clsx from "clsx";
const WordFilterItem = (props) => {
const item_wrapper_class_names = clsx(styles["item_wrapper"], {
[styles["is_redoable"]]: props.is_redoable
});
const item_text_class_names = clsx(styles["item_text"], {
[styles["is_redoable"]]: props.is_redoable
});
const target_item_value = props.value;
return (
<div className={item_wrapper_class_names}>
<p className={item_text_class_names}>{target_item_value}</p>
{props.is_redoable
?
<button className={clsx(styles.action_button, styles.redo)} onClick={() => props.redoAction(target_item_value)}>
<RedoSvg className={styles.redo_svg}/>
</button>
:
<button className={clsx(styles.action_button, styles.delete)} onClick={() => props.deleteAction(target_item_value)}>
<DeleteSvg className={styles.delete_svg}/>
</button>
}
</div>
);
};
import { useTranslation } from "react-i18next";
import ArrowLeftSvg from "@images/arrow_left.svg?react";
export const WordFilterListToggleComponent = (props) => {
const { t } = useTranslation();
const { currentIsOpenedWordFilterList, updateIsOpenedWordFilterList } = useIsOpenedWordFilterList();
const { currentWordFilterList } = useWordFilterList();
const svg_class_names = clsx(styles["arrow_left_svg"], {
[styles.to_down]: !currentIsOpenedWordFilterList,
[styles.to_up]: currentIsOpenedWordFilterList
});
const OnclickFunction = () => {
updateIsOpenedWordFilterList(!currentIsOpenedWordFilterList);
};
const word_filter_list_length = currentWordFilterList.filter(item => item.is_redoable === false).length;
return (
<div className={styles.toggle_button_container}>
<p className={styles.words_count_text}>{t("config_page.mic_word_filter.count_desc", {count: word_filter_list_length} )}</p>
<button className={styles.toggle_button_wrapper} onClick={OnclickFunction}>
<ArrowLeftSvg className={svg_class_names}/>
</button>
</div>
);
};

View File

@@ -0,0 +1,125 @@
.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;
&.is_redoable {
background-color: var(--dark_850_color);
}
}
.item_text {
font-size: 1.4rem;
font-weight: 300;
color: var(--dark_basic_text_color);
&.is_redoable {
text-decoration: line-through;
}
}
.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: #bb4448;
}
.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);
color: var(--dark_basic_text_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,46 @@
import styles from "./SidebarSection.module.scss";
export const SidebarSection = () => {
return (
<div className={styles.container}>
<div className={styles.tabs_wrapper}>
<Tab tab_id="appearance" />
<Tab tab_id="translation" />
<Tab tab_id="transcription" />
<Tab tab_id="vr" />
<Tab tab_id="others" />
<Tab tab_id="advanced_settings" />
</div>
<div className={styles.separated_tabs_wrapper}>
<Tab tab_id="about_vrct" />
</div>
</div>
);
};
import clsx from "clsx";
import { useTranslation } from "react-i18next";
import { useSelectedConfigTabId } from "@store";
const Tab = (props) => {
const { t } = useTranslation();
const { updateSelectedConfigTabId, currentSelectedConfigTabId } = useSelectedConfigTabId();
const onclickFunction = () => {
updateSelectedConfigTabId(props.tab_id);
};
const tab_container_class_names = clsx(styles["tab_container"], {
[styles["is_selected"]]: (currentSelectedConfigTabId === props.tab_id) ? true : false
});
const switch_indicator_class_names = clsx(styles["switch_indicator"], {
[styles["is_selected"]]: (currentSelectedConfigTabId === props.tab_id) ? true : false
});
return (
<div className={tab_container_class_names} onClick={onclickFunction}>
<p className={styles.tab_text}>{t(`config_page.side_menu_labels.${props.tab_id}`)}</p>
<div className={switch_indicator_class_names}></div>
</div>
);
};

View File

@@ -0,0 +1,63 @@
.container {
width: var(--config_page_sidebar_width);
flex-shrink: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 5.8rem 0rem 5.8rem 2.8rem;
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;
color: var(--dark_basic_text_color);
padding: 0.8rem 0 0.8rem 1.8rem;
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 {
font-size: 1.8rem;
}
.separated_tabs_wrapper {
// padding-bottom: 1.2rem;
}

View File

@@ -0,0 +1,17 @@
import styles from "./Topbar.module.scss";
import { TitleBox } from "./title_box/TitleBox";
import { SectionTitleBox } from "./section_title_box/SectionTitleBox";
import { CompactSwitchBox } from "./compact_switch_box/CompactSwitchBox";
export const Topbar = () => {
return (
<div className={styles.container}>
<div className={styles.wrapper}>
<TitleBox />
<SectionTitleBox />
<CompactSwitchBox />
</div>
</div>
);
};

View File

@@ -0,0 +1,12 @@
.container {
width: 100%;
height: 0%;
}
.wrapper {
height: var(--config_page_topbar_height);
background-color: var(--dark_850_color);
display: flex;
justify-content: space-between;
flex-shrink: 0;
}

View File

@@ -0,0 +1,12 @@
import { useTranslation } from "react-i18next";
import styles from "./CompactSwitchBox.module.scss";
export const CompactSwitchBox = () => {
const { t } = useTranslation();
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 { useTranslation } from "react-i18next";
import styles from "./SectionTitleBox.module.scss";
import { useSelectedConfigTabId } from "@store";
export const SectionTitleBox = () => {
const { t } = useTranslation();
const { currentSelectedConfigTabId } = useSelectedConfigTabId();
return (
<div className={styles.container}>
<p className={styles.title}>{t(`config_page.side_menu_labels.${currentSelectedConfigTabId}`)}</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 { useTranslation } from "react-i18next";
import styles from "./TitleBox.module.scss";
import chato_img from "@images/chato_white.png";
export const TitleBox = () => {
const { t } = useTranslation();
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;
}

12
src-ui/app/index.jsx Normal file
View File

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

View File

@@ -0,0 +1,39 @@
import styles from "./MainPage.module.scss";
import { SidebarSection } from "./sidebar_section/SidebarSection";
import { MainSection } from "./main_section/MainSection";
export const MainPage = () => {
return (
<div className={styles.container}>
<SidebarSection />
<MainSection />
{/* <MainPageCover /> */}
</div>
);
};
// import { useTranslation } from "react-i18next";
// import { useIsOpenedConfigPage } from "@store";
// import { useWindow } from "@logics/useWindow";
// export const MainPageCover = () => {
// const { t } = useTranslation();
// const { currentIsOpenedConfigPage } = useIsOpenedConfigPage();
// const { closeConfigPage } = useWindow();
// if ( currentIsOpenedConfigPage === false) return null;
// const closeSettingsWindow = () => closeConfigPage();
// return (
// <div className={styles.main_page_cover}>
// <p className={styles.cover_message}>{t("main_page.cover_message")}</p>
// <button
// className={styles.close_settings_window_button}
// onClick={closeSettingsWindow}
// >
// {t("main_page.close_settings_window")}
// </button>
// </div>
// );
// };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,28 @@
import { useTranslation } from "react-i18next";
import styles from "./TranslatorSelectorOpenButton.module.scss";
import { TranslatorSelector } from "./translator_selector/TranslatorSelector";
import { useTranslatorListStatus, useSelectedTranslatorIdStatus, useIsOpenedTranslatorSelector } from "@store";
export const TranslatorSelectorOpenButton = () => {
const { t } = useTranslation();
const { currentSelectedTranslatorIdStatus } = useSelectedTranslatorIdStatus();
const { currentTranslatorListStatus } = useTranslatorListStatus();
const currentTranslator = currentTranslatorListStatus.find(
translator_data => translator_data.translator_key === currentSelectedTranslatorIdStatus
);
const { currentIsOpenedTranslatorSelector, updateIsOpenedTranslatorSelector} = useIsOpenedTranslatorSelector();
const openTranslatorSelector = () => updateIsOpenedTranslatorSelector(!currentIsOpenedTranslatorSelector);
return (
<div className={styles.container}>
<div className={styles.translator_selector_button} onClick={openTranslatorSelector}>
<p className={styles.label}>{t("main_page.translator")}</p>
<p className={styles.label}>{currentTranslator?.translator_name}</p>
</div>
{currentIsOpenedTranslatorSelector && <TranslatorSelector />}
</div>
);
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More