[Refactor] Move to src-ui/views and src-ui/logics structure.
This commit is contained in:
82
src-ui/views/app/App.jsx
Normal file
82
src-ui/views/app/App.jsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { useI18n } from "@useI18n";
|
||||
|
||||
import {
|
||||
KeyEventController,
|
||||
StartPythonController,
|
||||
GlobalHotKeyController,
|
||||
UiLanguageController,
|
||||
ConfigPageCloseTriggerController,
|
||||
UiSizeController,
|
||||
FontFamilyController,
|
||||
TransparencyController,
|
||||
CornerRadiusController,
|
||||
PluginsController,
|
||||
} from "./_app_controllers";
|
||||
|
||||
import styles from "./App.module.scss";
|
||||
|
||||
import { MainPage } from "./main_page/MainPage";
|
||||
import { ConfigPage } from "./config_page/ConfigPage";
|
||||
|
||||
import {
|
||||
WindowTitleBar,
|
||||
SplashComponent,
|
||||
UpdatingComponent,
|
||||
ModalController,
|
||||
SnackbarController,
|
||||
AppErrorBoundary,
|
||||
} from "./others";
|
||||
|
||||
import { useIsBackendReady, useIsSoftwareUpdating, useIsVrctAvailable, useWindow } from "@logics_common";
|
||||
|
||||
export const App = () => {
|
||||
const { currentIsVrctAvailable } = useIsVrctAvailable();
|
||||
const { currentIsBackendReady } = useIsBackendReady();
|
||||
const { i18n } = useI18n();
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<AppErrorBoundary >
|
||||
<KeyEventController />
|
||||
<StartPythonController />
|
||||
<GlobalHotKeyController />
|
||||
<UiLanguageController />
|
||||
<ConfigPageCloseTriggerController />
|
||||
<UiSizeController />
|
||||
<FontFamilyController />
|
||||
<TransparencyController />
|
||||
<CornerRadiusController />
|
||||
|
||||
{(currentIsBackendReady.data === false || currentIsVrctAvailable.data === false)
|
||||
? <SplashComponent />
|
||||
: <Contents key={i18n.language} />
|
||||
}
|
||||
|
||||
<SnackbarController />
|
||||
</AppErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Contents = () => {
|
||||
const { WindowGeometryController } = useWindow();
|
||||
const { currentIsSoftwareUpdating } = useIsSoftwareUpdating();
|
||||
return (
|
||||
<>
|
||||
<WindowGeometryController />
|
||||
<PluginsController />
|
||||
|
||||
<WindowTitleBar />
|
||||
{currentIsSoftwareUpdating.data === false
|
||||
?
|
||||
<div className={styles.pages_wrapper}>
|
||||
<ConfigPage />
|
||||
<MainPage />
|
||||
<ModalController />
|
||||
</div>
|
||||
:
|
||||
<UpdatingComponent />
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
18
src-ui/views/app/App.module.scss
Normal file
18
src-ui/views/app/App.module.scss
Normal file
@@ -0,0 +1,18 @@
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
background-color: var(--dark_888_color);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.pages_wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import {
|
||||
useVolume,
|
||||
useIsOpenedConfigPage,
|
||||
} from "@logics_common";
|
||||
|
||||
import {
|
||||
useMainFunction,
|
||||
} from "@logics_main";
|
||||
|
||||
import { useHotkeys } from "@logics_configs";
|
||||
|
||||
import { useStore_MainFunctionsStateMemory } from "@store";
|
||||
|
||||
export const ConfigPageCloseTriggerController = () => {
|
||||
const { currentIsOpenedConfigPage } = useIsOpenedConfigPage();
|
||||
const {
|
||||
currentMainFunctionsStateMemory,
|
||||
updateMainFunctionsStateMemory,
|
||||
} = useStore_MainFunctionsStateMemory();
|
||||
|
||||
const {
|
||||
currentTranslationStatus,
|
||||
setTranslation,
|
||||
pendingTranslationStatus,
|
||||
currentTranscriptionSendStatus,
|
||||
setTranscriptionSend,
|
||||
pendingTranscriptionSendStatus,
|
||||
currentTranscriptionReceiveStatus,
|
||||
setTranscriptionReceive,
|
||||
pendingTranscriptionReceiveStatus,
|
||||
} = useMainFunction();
|
||||
const {
|
||||
currentMicThresholdCheckStatus,
|
||||
volumeCheckStop_Mic,
|
||||
currentSpeakerThresholdCheckStatus,
|
||||
volumeCheckStop_Speaker,
|
||||
} = useVolume();
|
||||
|
||||
const { registerShortcuts, unregisterAll } = useHotkeys();
|
||||
|
||||
|
||||
const memorizeLatestMainFunctionsState = () => {
|
||||
updateMainFunctionsStateMemory({
|
||||
translation: currentTranslationStatus.data,
|
||||
transcription_send: currentTranscriptionSendStatus.data,
|
||||
transcription_receive: currentTranscriptionReceiveStatus.data,
|
||||
});
|
||||
};
|
||||
|
||||
const restoreMainFunctionState = () => {
|
||||
// First, set loading status all before waiting a backend process.
|
||||
if (currentMainFunctionsStateMemory.data.translation === true) pendingTranslationStatus();
|
||||
if (currentMainFunctionsStateMemory.data.transcription_send === true) pendingTranscriptionSendStatus();
|
||||
if (currentMainFunctionsStateMemory.data.transcription_receive === true) pendingTranscriptionReceiveStatus();
|
||||
|
||||
// Then, restore them.
|
||||
if (currentMainFunctionsStateMemory.data.translation === true) setTranslation(true);
|
||||
if (currentMainFunctionsStateMemory.data.transcription_send === true) setTranscriptionSend(true);
|
||||
if (currentMainFunctionsStateMemory.data.transcription_receive === true) setTranscriptionReceive(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (currentIsOpenedConfigPage.data === true) { // When config page is opened.
|
||||
memorizeLatestMainFunctionsState();
|
||||
unregisterAll();
|
||||
if (currentTranslationStatus.data === true) setTranslation(false);
|
||||
if (currentTranscriptionSendStatus.data === true) setTranscriptionSend(false);
|
||||
if (currentTranscriptionReceiveStatus.data === true) setTranscriptionReceive(false);
|
||||
} else if (currentIsOpenedConfigPage.data === false) { // When config page is closed.
|
||||
registerShortcuts();
|
||||
if (currentMicThresholdCheckStatus.data === true) volumeCheckStop_Mic();
|
||||
if (currentSpeakerThresholdCheckStatus.data === true) volumeCheckStop_Speaker();
|
||||
restoreMainFunctionState();
|
||||
}
|
||||
}, [currentIsOpenedConfigPage.data]);
|
||||
return null;
|
||||
};
|
||||
54
src-ui/views/app/_app_controllers/CornerRadiusController.jsx
Normal file
54
src-ui/views/app/_app_controllers/CornerRadiusController.jsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { store } from "@store";
|
||||
|
||||
export const CornerRadiusController = () => {
|
||||
const [is_win11, setIsWin11] = useState(false);
|
||||
const [is_maximized, setIsMaximized] = useState(false);
|
||||
|
||||
const appWindow = store.appWindow;
|
||||
|
||||
// OS 判定(Win11 なら platformVersion の major ≥13)
|
||||
useEffect(() => {
|
||||
if (navigator.userAgentData?.getHighEntropyValues) {
|
||||
navigator.userAgentData
|
||||
.getHighEntropyValues(["platformVersion"])
|
||||
.then(({ platformVersion }) => {
|
||||
const major = parseInt(platformVersion.split(".")[0], 10);
|
||||
setIsWin11(major >= 13);
|
||||
})
|
||||
.catch(() => {
|
||||
setIsWin11(false);
|
||||
})
|
||||
} else {
|
||||
// フォールバックで Win10 扱い
|
||||
setIsWin11(false);
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
let unlisten;
|
||||
const setup = async () => {
|
||||
// 初期状態取得
|
||||
setIsMaximized(await appWindow.isMaximized());
|
||||
// リサイズ時にも再取得
|
||||
const updateMax = () => appWindow.isMaximized().then(setIsMaximized);
|
||||
unlisten = await appWindow.listen("tauri://resize", updateMax);
|
||||
}
|
||||
setup();
|
||||
|
||||
return () => {
|
||||
if (unlisten) {
|
||||
unlisten();
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 角丸の適用
|
||||
useEffect(() => {
|
||||
const radius = is_win11 && !is_maximized ? "10px" : "0";
|
||||
document.body.style.borderRadius = radius;
|
||||
}, [is_win11, is_maximized]);
|
||||
|
||||
return null;;
|
||||
}
|
||||
11
src-ui/views/app/_app_controllers/FontFamilyController.jsx
Normal file
11
src-ui/views/app/_app_controllers/FontFamilyController.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useEffect } from "react";
|
||||
import { useAppearance } from "@logics_configs";
|
||||
|
||||
export const FontFamilyController = () => {
|
||||
const { currentSelectedFontFamily } = useAppearance();
|
||||
useEffect(() => {
|
||||
document.documentElement.style.setProperty("--font_family", currentSelectedFontFamily.data);
|
||||
}, [currentSelectedFontFamily.data]);
|
||||
|
||||
return null;
|
||||
};
|
||||
25
src-ui/views/app/_app_controllers/GlobalHotKeyController.jsx
Normal file
25
src-ui/views/app/_app_controllers/GlobalHotKeyController.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useEffect } from "react";
|
||||
import { useHotkeys } from "@logics_configs";
|
||||
import { useIsBackendReady, useIsSoftwareUpdating, useIsVrctAvailable } from "@logics_common";
|
||||
|
||||
export const GlobalHotKeyController = () => {
|
||||
const { currentIsBackendReady } = useIsBackendReady();
|
||||
const { currentIsSoftwareUpdating } = useIsSoftwareUpdating();
|
||||
const { registerShortcuts, unregisterAll } = useHotkeys();
|
||||
|
||||
const { currentIsVrctAvailable } = useIsVrctAvailable();
|
||||
|
||||
useEffect(() => {
|
||||
const is_backend_ready = currentIsBackendReady.data;
|
||||
const is_software_updating = currentIsSoftwareUpdating.data;
|
||||
const is_vrct_available = currentIsVrctAvailable.data;
|
||||
|
||||
if (is_vrct_available && is_backend_ready && !is_software_updating) {
|
||||
registerShortcuts();
|
||||
} else {
|
||||
unregisterAll();
|
||||
}
|
||||
}, [currentIsBackendReady.data, currentIsSoftwareUpdating.data, currentIsVrctAvailable.data]);
|
||||
|
||||
return null;
|
||||
};
|
||||
31
src-ui/views/app/_app_controllers/KeyEventController.jsx
Normal file
31
src-ui/views/app/_app_controllers/KeyEventController.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const KeyEventController = () => {
|
||||
useEffect(() => {
|
||||
const handleKeydown = (event) => {
|
||||
if (
|
||||
event.key === "F5" || // Page reload
|
||||
event.key === "F10" || // Focus thw window menu (maybe)
|
||||
event.key === "F12" || // Open dev tool
|
||||
(event.ctrlKey && event.key === "r") ||
|
||||
(event.metaKey && event.key === "r")
|
||||
) {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const handleContextmenu = (event) => {
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeydown);
|
||||
document.addEventListener("contextmenu", handleContextmenu);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeydown);
|
||||
document.removeEventListener("contextmenu", handleContextmenu);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
24
src-ui/views/app/_app_controllers/PluginsController.jsx
Normal file
24
src-ui/views/app/_app_controllers/PluginsController.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
import * as reactI18next from "react-i18next";
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.React = React;
|
||||
window.clsx = clsx;
|
||||
window.reactI18next = reactI18next;
|
||||
}
|
||||
|
||||
import { LoadPluginsController } from "./plugins_controllers/LoadPluginsController";
|
||||
import { FetchLatestPluginsDataController } from "./plugins_controllers/FetchLatestPluginsDataController";
|
||||
import { MergePluginsController } from "./plugins_controllers/MergePluginsController";
|
||||
|
||||
export const PluginsController = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<MergePluginsController />
|
||||
<LoadPluginsController />
|
||||
<FetchLatestPluginsDataController />
|
||||
</>
|
||||
);
|
||||
};
|
||||
81
src-ui/views/app/_app_controllers/StartPythonController.jsx
Normal file
81
src-ui/views/app/_app_controllers/StartPythonController.jsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { Command } from "@tauri-apps/plugin-shell";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { useStdoutToPython } from "@useStdoutToPython";
|
||||
import { useReceiveRoutes } from "@useReceiveRoutes";
|
||||
import { store, useStore_SelectableFontFamilyList } from "@store";
|
||||
import { arrayToObject } from "@utils";
|
||||
|
||||
import {
|
||||
useNotificationStatus,
|
||||
} from "@logics_common";
|
||||
|
||||
export const StartPythonController = () => {
|
||||
const { asyncStartPython } = useStartPython();
|
||||
const hasRunRef = useRef(false);
|
||||
const { asyncFetchFonts } = useAsyncFetchFonts();
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasRunRef.current) {
|
||||
asyncStartPython().then(() => {
|
||||
startFeedingToWatchDogController();
|
||||
asyncFetchFonts();
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
return () => hasRunRef.current = true;
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const useStartPython = () => {
|
||||
const { receiveRoutes } = useReceiveRoutes();
|
||||
const { showNotification_Success, showNotification_Error } = useNotificationStatus();
|
||||
|
||||
const asyncStartPython = async () => {
|
||||
const command = Command.sidecar("bin/VRCT-sidecar");
|
||||
command.on("error", error => console.error(`error: "${error}"`));
|
||||
command.stdout.on("data", (line) => {
|
||||
let parsed_data = "";
|
||||
try {
|
||||
parsed_data = JSON.parse(line);
|
||||
receiveRoutes(parsed_data);
|
||||
} catch (error) {
|
||||
console.log(error, line);
|
||||
}
|
||||
});
|
||||
command.stderr.on("data", line => {
|
||||
showNotification_Error(
|
||||
`An error occurred. Please restart VRCT or contact the developers. The last line:${JSON.stringify(line)}`, { hide_duration: null });
|
||||
console.error("stderr", line);
|
||||
});
|
||||
const backend_subprocess = await command.spawn();
|
||||
store.backend_subprocess = backend_subprocess;
|
||||
};
|
||||
|
||||
return { asyncStartPython };
|
||||
};
|
||||
|
||||
const useAsyncFetchFonts = () => {
|
||||
const { updateSelectableFontFamilyList } = useStore_SelectableFontFamilyList();
|
||||
const asyncFetchFonts = async () => {
|
||||
try {
|
||||
let fonts = await invoke("get_font_list");
|
||||
fonts = fonts.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
|
||||
updateSelectableFontFamilyList(arrayToObject(fonts));
|
||||
} catch (error) {
|
||||
console.error("Error fetching fonts:", error);
|
||||
}
|
||||
};
|
||||
return { asyncFetchFonts };
|
||||
};
|
||||
|
||||
const startFeedingToWatchDogController = () => {
|
||||
const { asyncStdoutToPython } = useStdoutToPython();
|
||||
setInterval(() => {
|
||||
asyncStdoutToPython("/run/feed_watchdog");
|
||||
}, 20000); // 20000ミリ秒 = 20秒
|
||||
};
|
||||
11
src-ui/views/app/_app_controllers/TransparencyController.jsx
Normal file
11
src-ui/views/app/_app_controllers/TransparencyController.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { useEffect } from "react";
|
||||
import { useAppearance } from "@logics_configs";
|
||||
|
||||
export const TransparencyController = () => {
|
||||
const { currentTransparency } = useAppearance();
|
||||
useEffect(() => {
|
||||
document.documentElement.style.setProperty("opacity", `${currentTransparency.data / 100}`);
|
||||
}, [currentTransparency.data]);
|
||||
|
||||
return null;
|
||||
};
|
||||
14
src-ui/views/app/_app_controllers/UiLanguageController.jsx
Normal file
14
src-ui/views/app/_app_controllers/UiLanguageController.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useI18n } from "@useI18n";
|
||||
import { useAppearance } from "@logics_configs";
|
||||
|
||||
export const UiLanguageController = () => {
|
||||
const { currentUiLanguage } = useAppearance();
|
||||
const { i18n } = useI18n();
|
||||
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(currentUiLanguage.data);
|
||||
}, [currentUiLanguage.data]);
|
||||
return null;
|
||||
};
|
||||
13
src-ui/views/app/_app_controllers/UiSizeController.jsx
Normal file
13
src-ui/views/app/_app_controllers/UiSizeController.jsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useEffect } from "react";
|
||||
import { useAppearance } from "@logics_configs";
|
||||
|
||||
export const UiSizeController = () => {
|
||||
const { currentUiScaling } = useAppearance();
|
||||
const font_size = 62.5 * currentUiScaling.data / 100;
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.style.setProperty("font-size", `${font_size}%`);
|
||||
}, [currentUiScaling.data]);
|
||||
|
||||
return null;
|
||||
};
|
||||
10
src-ui/views/app/_app_controllers/index.js
Normal file
10
src-ui/views/app/_app_controllers/index.js
Normal file
@@ -0,0 +1,10 @@
|
||||
export { KeyEventController } from "./KeyEventController";
|
||||
export { StartPythonController } from "./StartPythonController";
|
||||
export { GlobalHotKeyController } from "./GlobalHotKeyController";
|
||||
export { UiLanguageController } from "./UiLanguageController";
|
||||
export { ConfigPageCloseTriggerController } from "./ConfigPageCloseTriggerController";
|
||||
export { UiSizeController } from "./UiSizeController";
|
||||
export { FontFamilyController } from "./FontFamilyController";
|
||||
export { TransparencyController } from "./TransparencyController";
|
||||
export { PluginsController } from "./PluginsController";
|
||||
export { CornerRadiusController } from "./CornerRadiusController";
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useEffect } from "react";
|
||||
import { usePlugins } from "@logics_configs";
|
||||
|
||||
export const FetchLatestPluginsDataController = () => {
|
||||
const {
|
||||
asyncFetchPluginsInfo,
|
||||
isAnyPluginEnabled_Init,
|
||||
} = usePlugins();
|
||||
|
||||
useEffect(() => {
|
||||
if (isAnyPluginEnabled_Init()) {
|
||||
asyncFetchPluginsInfo();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useEffect } from "react";
|
||||
import { usePlugins } from "@logics_configs";
|
||||
import { store } from "@store";
|
||||
|
||||
export const LoadPluginsController = () => {
|
||||
const {
|
||||
asyncLoadAllPlugins,
|
||||
} = usePlugins();
|
||||
|
||||
const asyncInitLoadPlugins = async () => {
|
||||
try {
|
||||
await asyncLoadAllPlugins();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!store.is_initialized_load_plugin) {
|
||||
asyncInitLoadPlugins();
|
||||
store.is_initialized_load_plugin = true;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,205 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useI18n } from "@useI18n";
|
||||
import { store } from "@store";
|
||||
import { usePlugins } from "@logics_configs";
|
||||
import { useSoftwareVersion } from "@logics_common";
|
||||
import { useNotificationStatus } from "@logics_common";
|
||||
|
||||
export const MergePluginsController = () => {
|
||||
const { t } = useI18n();
|
||||
const {
|
||||
currentLoadedPlugins,
|
||||
updatePluginsData,
|
||||
currentPluginsData,
|
||||
currentFetchedPluginsInfo,
|
||||
currentSavedPluginsStatus,
|
||||
downloadAndExtractPlugin,
|
||||
setTargetSavedPluginsStatus_Init,
|
||||
} = usePlugins();
|
||||
const { checkVrctVerCompatibility } = useSoftwareVersion();
|
||||
const { showNotification_Success, showNotification_Error } = useNotificationStatus();
|
||||
|
||||
// downloaded, fetched, saved の各情報をまとめてマージ
|
||||
useEffect(() => {
|
||||
const mergePluginData = () => {
|
||||
updatePluginsData(prev => {
|
||||
// downloaded, fetched, 保存済み状態のMapをそれぞれ作成(plugin_id をキー)
|
||||
const downloaded_map = new Map(
|
||||
currentLoadedPlugins.data.map(info => [info.plugin_id, info])
|
||||
);
|
||||
const fetched_map = new Map(
|
||||
currentFetchedPluginsInfo.data.map(info => [info.plugin_id, info])
|
||||
);
|
||||
const saved_map = new Map(
|
||||
currentSavedPluginsStatus.data.map(saved => [saved.plugin_id, saved])
|
||||
);
|
||||
const prev_map = new Map(
|
||||
prev.data.map(item => [item.plugin_id, item])
|
||||
);
|
||||
|
||||
// union_keys: Saved以外の情報に対して重複なくキーを取得する
|
||||
const union_keys = new Set([
|
||||
...downloaded_map.keys(),
|
||||
...fetched_map.keys(),
|
||||
...prev_map.keys(),
|
||||
]);
|
||||
|
||||
const new_data = [];
|
||||
for (const id of union_keys) {
|
||||
const downloaded = downloaded_map.get(id);
|
||||
const fetched = fetched_map.get(id);
|
||||
const prev_plugin = prev_map.get(id);
|
||||
let plugin = {};
|
||||
|
||||
if (downloaded) {
|
||||
// ダウンロード済み情報に対してサポート確認
|
||||
const { is_plugin_supported, is_plugin_supported_latest_vrct } =
|
||||
checkVrctVerCompatibility(
|
||||
downloaded.min_supported_vrct_version,
|
||||
downloaded.max_supported_vrct_version
|
||||
);
|
||||
plugin = {
|
||||
// prevの情報があれば引き継ぎつつ上書き
|
||||
...(prev_plugin || {}),
|
||||
plugin_id: downloaded.plugin_id,
|
||||
component: downloaded.component,
|
||||
is_downloaded: true,
|
||||
downloaded_plugin_info: {
|
||||
...downloaded,
|
||||
is_plugin_supported,
|
||||
is_plugin_supported_latest_vrct,
|
||||
},
|
||||
};
|
||||
|
||||
if (fetched) {
|
||||
const is_latest_version_available =
|
||||
(downloaded.plugin_version !== fetched.plugin_version && fetched.is_plugin_supported);
|
||||
plugin = {
|
||||
...plugin,
|
||||
is_outdated: false,
|
||||
latest_plugin_info: { ...fetched },
|
||||
is_latest_version_available:
|
||||
plugin.is_downloaded && is_latest_version_available,
|
||||
is_latest_version_already:
|
||||
downloaded.plugin_version === fetched.plugin_version,
|
||||
};
|
||||
} else {
|
||||
// フェッチ情報がない場合の初期状態
|
||||
plugin = {
|
||||
...plugin,
|
||||
is_latest_version_available: false,
|
||||
is_latest_version_already: true,
|
||||
};
|
||||
}
|
||||
} else if (fetched) {
|
||||
// フェッチ情報のみの場合は、ダウンロードしていない初期状態
|
||||
plugin = {
|
||||
...(prev_plugin || {}),
|
||||
plugin_id: fetched.plugin_id,
|
||||
is_downloaded: false,
|
||||
is_latest_version_available: fetched.is_plugin_supported,
|
||||
is_latest_version_already: false,
|
||||
is_outdated: false,
|
||||
latest_plugin_info: { ...fetched },
|
||||
};
|
||||
} else if (prev_plugin) {
|
||||
// 既存情報のみ存在する場合は outdated フラグを付与
|
||||
plugin = { ...prev_plugin, is_outdated: true };
|
||||
}
|
||||
// いずれかの情報がある場合のみ new_data に追加
|
||||
if (plugin.plugin_id) {
|
||||
new_data.push(plugin);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存済み状態(currentSavedPluginsStatus)のマージ
|
||||
// ・new_dataに存在する各プラグインに対して、保存済みの is_enabled を上書き
|
||||
new_data.forEach(plugin => {
|
||||
if (saved_map.has(plugin.plugin_id)) {
|
||||
plugin.is_enabled = saved_map.get(plugin.plugin_id).is_enabled;
|
||||
}
|
||||
});
|
||||
// ・prev.data には存在せず、保存済み情報にのみある場合は追加
|
||||
for (const [id, saved] of saved_map.entries()) {
|
||||
if (!new_data.some(item => item.plugin_id === id)) {
|
||||
new_data.push({ plugin_id: saved.plugin_id, is_enabled: saved.is_enabled });
|
||||
}
|
||||
}
|
||||
|
||||
return new_data;
|
||||
});
|
||||
};
|
||||
|
||||
mergePluginData();
|
||||
}, [currentFetchedPluginsInfo.data, currentLoadedPlugins.data, currentSavedPluginsStatus]);
|
||||
|
||||
|
||||
|
||||
// --- 自動アップデート(ダウンロード処理)---
|
||||
// ※downloadAndExtractPlugin の重複実行を防ぐため、実行中の plugin_id を useRef で管理
|
||||
const downloadingRef = useRef(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentPluginsData.data.length) return;
|
||||
// マージ結果の currentPluginsData.data を元にダウンロード処理をチェック
|
||||
currentPluginsData.data.forEach(plugin => {
|
||||
if (plugin.is_downloaded &&
|
||||
plugin.is_enabled &&
|
||||
!plugin.downloaded_plugin_info.is_plugin_supported &&
|
||||
!plugin.is_latest_version_already &&
|
||||
plugin.is_latest_version_available
|
||||
) {
|
||||
if (!downloadingRef.current.has(plugin.plugin_id)) {
|
||||
showNotification_Success(t("plugin_notifications.updating"));
|
||||
downloadingRef.current.add(plugin.plugin_id);
|
||||
const target_plugin_id = plugin.plugin_id;
|
||||
downloadAndExtractPlugin(plugin)
|
||||
.then(() => {
|
||||
console.log(`Plugin ${target_plugin_id} updated successfully`);
|
||||
downloadingRef.current.delete(target_plugin_id);
|
||||
showNotification_Success(t("plugin_notifications.updated_success"));
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`Plugin ${target_plugin_id} update failed`, error);
|
||||
downloadingRef.current.delete(target_plugin_id);
|
||||
showNotification_Error(t("plugin_notifications.updated_error"));
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}, [currentPluginsData.data]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
// ダウンロード済みかつ有効なプラグインで、サポート対象でない場合は無効化
|
||||
if (store.is_initialized_fetched_plugin_info) {
|
||||
updatePluginsData(prev => {
|
||||
prev.data.forEach(plugin => {
|
||||
if (plugin.is_downloaded && plugin.is_enabled) {
|
||||
if (
|
||||
!plugin.downloaded_plugin_info.is_plugin_supported &&
|
||||
plugin.latest_plugin_info &&
|
||||
!plugin.latest_plugin_info?.is_plugin_supported
|
||||
) {
|
||||
showNotification_Error(t("plugin_notifications.disabled_out_of_support"));
|
||||
plugin.is_enabled = false;
|
||||
setTargetSavedPluginsStatus_Init(plugin.plugin_id, false);
|
||||
}
|
||||
|
||||
if (
|
||||
!plugin.downloaded_plugin_info.is_plugin_supported &&
|
||||
plugin.is_outdated
|
||||
) {
|
||||
showNotification_Error(t("plugin_notifications.disabled_out_of_support"));
|
||||
plugin.is_enabled = false;
|
||||
setTargetSavedPluginsStatus_Init(plugin.plugin_id, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
return prev.data;
|
||||
});
|
||||
}
|
||||
}, [store.is_initialized_fetched_plugin_info]);
|
||||
|
||||
return null;
|
||||
};
|
||||
418
src-ui/views/app/_index_css/reset.css
Normal file
418
src-ui/views/app/_index_css/reset.css
Normal file
@@ -0,0 +1,418 @@
|
||||
/*! destyle.css v4.0.1 | MIT License | https://github.com/nicolas-cusan/destyle.css */
|
||||
|
||||
/* Reset box-model and set borders */
|
||||
/* ============================================ */
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Document */
|
||||
/* ============================================ */
|
||||
|
||||
/**
|
||||
* 1. Correct the line height in all browsers.
|
||||
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
* 3. Remove gray overlay on links for iOS.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.15; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
-webkit-tap-highlight-color: transparent; /* 3*/
|
||||
}
|
||||
|
||||
/* Sections */
|
||||
/* ============================================ */
|
||||
|
||||
/**
|
||||
* Remove the margin in all browsers.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the `main` element consistently in IE.
|
||||
*/
|
||||
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Vertical rhythm */
|
||||
/* ============================================ */
|
||||
|
||||
p,
|
||||
table,
|
||||
blockquote,
|
||||
address,
|
||||
pre,
|
||||
iframe,
|
||||
form,
|
||||
figure,
|
||||
dl {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Headings */
|
||||
/* ============================================ */
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Lists (enumeration) */
|
||||
/* ============================================ */
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
/* Lists (definition) */
|
||||
/* ============================================ */
|
||||
|
||||
dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* Grouping content */
|
||||
/* ============================================ */
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in Firefox.
|
||||
* 2. Show the overflow in Edge and IE.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box; /* 1 */
|
||||
height: 0; /* 1 */
|
||||
overflow: visible; /* 2 */
|
||||
border-top-width: 1px;
|
||||
margin: 0;
|
||||
clear: both;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: inherit; /* 2 */
|
||||
}
|
||||
|
||||
address {
|
||||
font-style: inherit;
|
||||
}
|
||||
|
||||
/* Text-level semantics */
|
||||
/* ============================================ */
|
||||
|
||||
/**
|
||||
* Remove the gray background on active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Remove the bottom border in Chrome 57-
|
||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
text-decoration: underline dotted; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||
* all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/* Replaced content */
|
||||
/* ============================================ */
|
||||
|
||||
/**
|
||||
* Prevent vertical alignment issues.
|
||||
*/
|
||||
|
||||
svg,
|
||||
img,
|
||||
embed,
|
||||
object,
|
||||
iframe {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
/* ============================================ */
|
||||
|
||||
/**
|
||||
* Reset form fields to make them styleable.
|
||||
* 1. Make form elements stylable across systems iOS especially.
|
||||
* 2. Inherit text-transform from parent.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
-webkit-appearance: none; /* 1 */
|
||||
appearance: none;
|
||||
vertical-align: middle;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
text-align: inherit;
|
||||
text-transform: inherit; /* 2 */
|
||||
|
||||
/* Customize */
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct cursors for clickable elements.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:disabled,
|
||||
[type="button"]:disabled,
|
||||
[type="reset"]:disabled,
|
||||
[type="submit"]:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improve outlines for Firefox and unify style with input elements & buttons.
|
||||
*/
|
||||
|
||||
:-moz-focusring {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
select:disabled {
|
||||
opacity: inherit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove padding
|
||||
*/
|
||||
|
||||
option {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to invisible
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the default vertical scrollbar in IE 10+.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||
*/
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type="search"] {
|
||||
outline-offset: -2px; /* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||
* 2. Fix font inheritance.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix appearance for Firefox
|
||||
*/
|
||||
[type="number"] {
|
||||
appearance: textfield;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clickable labels
|
||||
*/
|
||||
|
||||
label[for] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Interactive */
|
||||
/* ============================================ */
|
||||
|
||||
/*
|
||||
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||
*/
|
||||
|
||||
details {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the correct display in all browsers.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/*
|
||||
* Remove outline for editable content.
|
||||
*/
|
||||
|
||||
[contenteditable]:focus {
|
||||
outline: auto;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
/* ============================================ */
|
||||
|
||||
/**
|
||||
1. Correct table border color inheritance in all Chrome and Safari.
|
||||
*/
|
||||
|
||||
table {
|
||||
border-color: inherit; /* 1 */
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
vertical-align: top;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
}
|
||||
51
src-ui/views/app/_index_css/root.css
Normal file
51
src-ui/views/app/_index_css/root.css
Normal file
@@ -0,0 +1,51 @@
|
||||
@import "./reset.css";
|
||||
@import "./variables.css";
|
||||
|
||||
:root {
|
||||
font-size: 62.5%;
|
||||
color: var(--dark_basic_text_color);
|
||||
}
|
||||
|
||||
* {
|
||||
user-select: none;
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.8rem;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: var(--dark_925_color);
|
||||
border-radius: 0.4rem;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--dark_800_color);
|
||||
border-radius: 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
font-family: var(--font_family); /* If not found the font family where 'root:' that is selected by user*/
|
||||
border-radius: 10px; /* fixed by px */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
#root {
|
||||
height: 100%;
|
||||
|
||||
/* For controlling a whole window transparency */
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* SVG内のすべての要素にfillを適用 (colorの調整をcssでするため) */
|
||||
svg {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
p {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
img {
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
75
src-ui/views/app/_index_css/variables.css
Normal file
75
src-ui/views/app/_index_css/variables.css
Normal file
@@ -0,0 +1,75 @@
|
||||
:root {
|
||||
--primary_100_color: #b7ded8;
|
||||
--primary_150_color: #a1d4cc;
|
||||
--primary_200_color: #8acac0;
|
||||
--primary_250_color: #76bfb4;
|
||||
--primary_300_color: #61b4a7;
|
||||
--primary_350_color: #55ac9e;
|
||||
--primary_400_color: #48a495;
|
||||
--primary_450_color: #429c8c;
|
||||
--primary_500_color: #3b9483;
|
||||
--primary_550_color: #398E7D;
|
||||
--primary_600_color: #368777;
|
||||
--primary_650_color: #347f6f;
|
||||
--primary_700_color: #317767;
|
||||
--primary_750_color: #2f6f60;
|
||||
--primary_800_color: #2c6759;
|
||||
--primary_900_color: #214b3f;
|
||||
|
||||
--primary_600_color_44: #36877744;
|
||||
|
||||
/* primary_300_color 61b4a7 as standard */
|
||||
--sent_400_color: #6197b4;
|
||||
--received_300_color: #a861b4;
|
||||
|
||||
--error_bc_color: #bb4448;
|
||||
--error_bc_active_color: #9c3938;
|
||||
--success_bc_color: var(--primary_600_color);
|
||||
--warning_color: #cb944f;
|
||||
--warning_bc_color: #cf7b1b;
|
||||
|
||||
--dark_basic_text_color: #f2f2f2;
|
||||
--dark_100_color: #f5f7fb;
|
||||
--dark_200_color: #f1f2f6;
|
||||
--dark_300_color: #e9eaee;
|
||||
--dark_350_color: #d8d9dd;
|
||||
--dark_400_color: #c7c8cc;
|
||||
--dark_450_color: #b8b9bd;
|
||||
--dark_500_color: #a9aaae;
|
||||
--dark_550_color: #949599;
|
||||
--dark_600_color: #7f8084;
|
||||
--dark_650_color: #75767a;
|
||||
--dark_700_color: #6a6c6f;
|
||||
--dark_725_color: #636467;
|
||||
--dark_750_color: #5b5c5f;
|
||||
--dark_775_color: #535457;
|
||||
--dark_800_color: #4b4c4f;
|
||||
--dark_825_color: #434447;
|
||||
--dark_850_color: #3a3b3e;
|
||||
--dark_863_color: #36373a;
|
||||
--dark_875_color: #323336;
|
||||
--dark_888_color: #2e2f32;
|
||||
--dark_900_color: #292a2d;
|
||||
--dark_925_color: #242528;
|
||||
--dark_950_color: #1f2022;
|
||||
--dark_975_color: #1a1b1d;
|
||||
--dark_1000_color: #151517;
|
||||
|
||||
--dark_550_color_22: #94959922;
|
||||
--dark_825_color_cc: #434447cc;
|
||||
--dark_1000_color_66: #15151766;
|
||||
--dark_1000_color_aa: #151517aa;
|
||||
--dark_1000_color_dd: #151517dd;
|
||||
|
||||
/*sent_400_color + #000 10% = */
|
||||
--supporters_color_fuwa: #5788a2;
|
||||
|
||||
--title_bar_height: 2rem;
|
||||
--main_page_topbar_height: 4.8rem;
|
||||
--config_page_sidebar_width: 16.8rem;
|
||||
--config_page_topbar_height: 8rem;
|
||||
|
||||
--font_family: "Yu Gothic UI";
|
||||
}
|
||||
/* https://m2.material.io/design/color/the-color-system.html#tools-for-picking-colors */
|
||||
/* https://meyerweb.com/eric/tools/color-blend/#::1:hex */
|
||||
21
src-ui/views/app/config_page/ConfigPage.jsx
Normal file
21
src-ui/views/app/config_page/ConfigPage.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import styles from "./ConfigPage.module.scss";
|
||||
|
||||
import { Topbar } from "./topbar/Topbar.jsx";
|
||||
import { SidebarSection } from "./sidebar_section/SidebarSection.jsx";
|
||||
import { SettingSection } from "./setting_section/SettingSection.jsx";
|
||||
import { VersionLabel } from "./version_label/VersionLabel.jsx";
|
||||
|
||||
export const ConfigPage = () => {
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<div className={styles.container}>
|
||||
<Topbar />
|
||||
<div className={styles.main_container}>
|
||||
<SidebarSection />
|
||||
<SettingSection />
|
||||
</div>
|
||||
<VersionLabel />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
26
src-ui/views/app/config_page/ConfigPage.module.scss
Normal file
26
src-ui/views/app/config_page/ConfigPage.module.scss
Normal file
@@ -0,0 +1,26 @@
|
||||
.page {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
background-color: var(--dark_900_color);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main_container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
padding-top: var(--config_page_topbar_height);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useRef, useLayoutEffect, useEffect } from "react";
|
||||
|
||||
import styles from "./SettingSection.module.scss";
|
||||
import { SettingBox } from "./setting_box/SettingBox";
|
||||
import { store, useStore_SelectedConfigTabId } from "@store";
|
||||
import { useSettingBoxScrollPosition } from "@logics_configs";
|
||||
|
||||
export const SettingSection = () => {
|
||||
const { currentSelectedConfigTabId } = useStore_SelectedConfigTabId();
|
||||
const { resetScrollPosition } = useSettingBoxScrollPosition();
|
||||
const scrollContainerRef = useRef(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
store.setting_box_scroll_container = scrollContainerRef;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
resetScrollPosition();
|
||||
}, [currentSelectedConfigTabId.data]);
|
||||
|
||||
return (
|
||||
<div ref={scrollContainerRef} className={styles.scroll_container}>
|
||||
<div className={styles.container}>
|
||||
<SettingBox />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
.scroll_container {
|
||||
width: 100%;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0rem 4rem 16rem 0.6rem;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useStore_SelectedConfigTabId } from "@store";
|
||||
|
||||
import {
|
||||
Device,
|
||||
Appearance,
|
||||
Translation,
|
||||
Transcription,
|
||||
Others,
|
||||
AdvancedSettings,
|
||||
Vr,
|
||||
Hotkeys,
|
||||
Plugins,
|
||||
Supporters,
|
||||
AboutVrct,
|
||||
} from "@setting_box";
|
||||
|
||||
export const SettingBox = () => {
|
||||
const { currentSelectedConfigTabId } = useStore_SelectedConfigTabId();
|
||||
switch (currentSelectedConfigTabId.data) {
|
||||
case "device":
|
||||
return <Device />;
|
||||
case "appearance":
|
||||
return <Appearance />;
|
||||
case "translation":
|
||||
return <Translation />;
|
||||
case "transcription":
|
||||
return <Transcription />;
|
||||
case "others":
|
||||
return <Others />;
|
||||
case "vr":
|
||||
return <Vr />;
|
||||
case "hotkeys":
|
||||
return <Hotkeys />;
|
||||
case "advanced_settings":
|
||||
return <AdvancedSettings />;
|
||||
case "plugins":
|
||||
return <Plugins />;
|
||||
case "supporters":
|
||||
return <Supporters />;
|
||||
case "about_vrct":
|
||||
return <AboutVrct />;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useI18n } from "@useI18n";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import styles from "./_DownloadButton.module.scss";
|
||||
|
||||
export const _DownloadButton = ({option, ...props}) => {
|
||||
const { t } = useI18n();
|
||||
|
||||
const renderContent = () => {
|
||||
const circular_progress = Math.floor(option.progress / 10) * 10;
|
||||
|
||||
switch (true) {
|
||||
case option.progress !== null:
|
||||
return (
|
||||
<>
|
||||
<CircularProgress
|
||||
variant={(option.progress === 100) ? "indeterminate" : "determinate"}
|
||||
value={circular_progress}
|
||||
size="3rem"
|
||||
sx={{ color: "var(--primary_300_color)" }}
|
||||
/>
|
||||
<p className={styles.progress_label}>{`${Math.round(option.progress)}%`}</p>
|
||||
</>
|
||||
);
|
||||
case option.is_pending:
|
||||
return <CircularProgress size="3rem" sx={{ color: "var(--dark_600_color)" }}/>;
|
||||
case !option.is_downloaded:
|
||||
return (
|
||||
<button
|
||||
className={styles.download_button}
|
||||
onClick={() => props.downloadStartFunction(option.id)}
|
||||
>
|
||||
<p className={styles.download_button_label}>{t("config_page.common.model_download_button_label")}</p>
|
||||
</button>
|
||||
);
|
||||
case option.update_button:
|
||||
return (
|
||||
<button
|
||||
className={styles.update_button}
|
||||
onClick={() => props.downloadStartFunction(option.id)}
|
||||
>
|
||||
<p className={styles.download_button_label}>Update</p>
|
||||
</button>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return <div className={styles.download_container}>{renderContent()}</div>;
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
@import "@scss_mixins";
|
||||
|
||||
.download_container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
max-width: 8rem;
|
||||
}
|
||||
|
||||
.download_button {
|
||||
pointer-events: auto;
|
||||
background-color: var(--dark_800_color);
|
||||
padding: 0.8rem;
|
||||
flex-shrink: 0;
|
||||
border-radius: 0.2rem;
|
||||
&:hover {
|
||||
background-color: var(--dark_750_color);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--dark_800_color);
|
||||
}
|
||||
}
|
||||
.download_button_label {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.progress_label {
|
||||
position: absolute;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.update_button {
|
||||
pointer-events: auto;
|
||||
background-color: var(--primary_400_color);
|
||||
padding: 0.8rem;
|
||||
flex-shrink: 0;
|
||||
border-radius: 0.2rem;
|
||||
&:hover {
|
||||
background-color: var(--primary_450_color);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--primary_500_color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import clsx from "clsx";
|
||||
import React, { useRef, forwardRef, useImperativeHandle } from "react";
|
||||
import styles from "./_Entry.module.scss";
|
||||
|
||||
const _Entry = forwardRef((props, ref) => {
|
||||
const inputRef = useRef();
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
inputRef.current.focus();
|
||||
},
|
||||
blur: () => {
|
||||
inputRef.current.blur();
|
||||
}
|
||||
}));
|
||||
const input_class_names = clsx(styles.entry_input_area, {
|
||||
[styles.is_disabled]: props.is_disabled,
|
||||
});
|
||||
const input_wrapper_class_names = clsx(styles.entry_wrapper, {
|
||||
[styles.is_activated]: props.is_activated,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.entry_container}
|
||||
style={{width: props.width || "100%" }}
|
||||
>
|
||||
<div className={input_wrapper_class_names}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
text={props.text ? props.text : "text"}
|
||||
placeholder={props.placeholder ? props.placeholder : ""}
|
||||
className={input_class_names}
|
||||
value={props.ui_variable === null ? "" : props.ui_variable}
|
||||
onChange={(e) => props.onChange?.(e)}
|
||||
onFocus={(e) => props.onFocus?.(e)}
|
||||
onBlur={(e) => props.onBlur?.(e)}
|
||||
onKeyDown={(e) => props.onKeyDown?.(e)}
|
||||
onKeyUp={(e) => props.onKeyUp?.(e)}
|
||||
readOnly={props.readOnly === true ? true : false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
_Entry.displayName = "_Entry";
|
||||
|
||||
export { _Entry };
|
||||
@@ -0,0 +1,26 @@
|
||||
.entry_container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.entry_wrapper {
|
||||
height: 100%;
|
||||
padding: 0.6rem;
|
||||
background-color: var(--dark_875_color);
|
||||
border: 0.1rem solid var(--dark_750_color);
|
||||
border-radius: 0.4rem;
|
||||
&.is_activated {
|
||||
border: 0.1rem solid var(--primary_400_color);
|
||||
}
|
||||
}
|
||||
|
||||
.entry_input_area {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 1.4rem;
|
||||
resize: none;
|
||||
font-family: Arial, sans-serif;
|
||||
&.is_disabled {
|
||||
color: var(--dark_500_color);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
.button_wrapper {
|
||||
padding: 1.6rem;
|
||||
border-radius: 0.4rem;
|
||||
&:hover {
|
||||
background-color: var(--dark_825_color);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--dark_900_color);
|
||||
}
|
||||
}
|
||||
|
||||
.button_svg {
|
||||
width: 2.4rem;
|
||||
color: var(--dark_400_color);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import styles from "./ComputeDevice.module.scss";
|
||||
import { DropdownMenu } from "../dropdown_menu/DropdownMenu";
|
||||
import { ActionButton } from "../action_button/ActionButton";
|
||||
import HelpSvg from "@images/help.svg?react";
|
||||
import { useStore_OpenedQuickSetting } from "@store"
|
||||
|
||||
export const ComputeDevice = (props) => {
|
||||
const { updateOpenedQuickSetting } = useStore_OpenedQuickSetting();
|
||||
|
||||
const onClickFunction = () => {
|
||||
updateOpenedQuickSetting("update_software");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<DropdownMenu
|
||||
{...props}
|
||||
is_disabled={true}
|
||||
/>
|
||||
<ActionButton
|
||||
{...props}
|
||||
IconComponent={HelpSvg}
|
||||
onclickFunction={onClickFunction}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import styles from "./DeeplAuthKey.module.scss";
|
||||
import { useI18n } from "@useI18n";
|
||||
import clsx from "clsx";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import ExternalLink from "@images/external_link.svg?react";
|
||||
import { _Entry } from "../_atoms/_entry/_Entry";
|
||||
import { useState, useRef } from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const DeeplAuthKey = (props) => {
|
||||
const { t } = useI18n();
|
||||
const [is_editable, seIsEditable] = useState(false);
|
||||
const entryRef = useRef(null);
|
||||
|
||||
const revealEditAuthKey = () => {
|
||||
seIsEditable(true);
|
||||
entryRef.current.focus();
|
||||
};
|
||||
|
||||
const onchangeEntryAuthKey = (e) => {
|
||||
props.onChangeFunction(e.target.value);
|
||||
};
|
||||
const saveAuthKey = () => {
|
||||
props.saveFunction();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (props.variable === "" || props.variable === null) {
|
||||
seIsEditable(true);
|
||||
}
|
||||
}, [props.variable]);
|
||||
|
||||
const is_disabled = props.state === "pending";
|
||||
|
||||
const save_button_class_names = clsx(styles.save_button, {
|
||||
[styles.is_disabled]: is_disabled
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.entry_section_wrapper}>
|
||||
<_Entry ref={entryRef} width="30rem" onChange={onchangeEntryAuthKey} ui_variable={props.variable} is_disabled={is_disabled}/>
|
||||
<button className={save_button_class_names} onClick={saveAuthKey}>
|
||||
{is_disabled
|
||||
? <CircularProgress size="1.4rem" sx={{ color: "var(--dark_basic_text_color)" }}/>
|
||||
: <p className={styles.save_button_label}>{t("config_page.translation.deepl_auth_key.save")}</p>
|
||||
}
|
||||
</button>
|
||||
{is_editable
|
||||
? null
|
||||
:
|
||||
<div className={styles.entry_edit_cover} onClick={revealEditAuthKey}>
|
||||
<button className={styles.edit_button}>{t("config_page.translation.deepl_auth_key.edit")}</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export const OpenWebpage_DeeplAuthKey = () => {
|
||||
const { t } = useI18n();
|
||||
return (
|
||||
<div className={styles.open_webpage_button_wrapper}>
|
||||
<a className={styles.open_webpage_button} href="https://www.deepl.com/ja/your-account/keys" target="_blank" rel="noreferrer" >
|
||||
<p className={styles.open_webpage_text}>{t("config_page.translation.deepl_auth_key.open_auth_key_webpage")}</p>
|
||||
<ExternalLink className={styles.external_link_svg} />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.entry_section_wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.entry_edit_cover {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
border-radius: 0.4rem;
|
||||
background-color: var(--dark_1000_color_66);
|
||||
backdrop-filter: blur(4rem);
|
||||
border: solid 0.1rem var(--dark_700_color);
|
||||
&:hover {
|
||||
background-color: var(--dark_1000_color_aa);
|
||||
}
|
||||
&:active {
|
||||
backdrop-filter: blur(1.4rem);
|
||||
}
|
||||
}
|
||||
|
||||
.edit_button {
|
||||
padding: 0.8rem 1.2rem;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
font-size: 1.4rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.save_button {
|
||||
padding: 0.8rem 1.2rem;
|
||||
background-color: var(--primary_600_color);
|
||||
border-radius: 0.4rem;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
min-width: 5.4rem;
|
||||
&:hover {
|
||||
background-color: var(--primary_500_color);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--primary_700_color);
|
||||
}
|
||||
&.is_disabled {
|
||||
pointer-events: none;
|
||||
background-color: var(--primary_800_color);
|
||||
}
|
||||
}
|
||||
|
||||
.save_button_label {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.open_webpage_button_wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.open_webpage_button {
|
||||
padding: 0.6rem 2.8rem;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 0.4rem;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
&:hover {
|
||||
background-color: var(--dark_825_color);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--dark_900_color);
|
||||
}
|
||||
}
|
||||
|
||||
.open_webpage_text {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.external_link_svg {
|
||||
color: var(--dark_500_color);
|
||||
width: 1.6rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
RadioButton,
|
||||
} from "../index";
|
||||
import { _DownloadButton } from "../_atoms/_download_button/_DownloadButton";
|
||||
|
||||
export const DownloadModels = (props) => {
|
||||
const options = props.options.map(item => ({
|
||||
...item,
|
||||
disabled: !item.is_downloaded
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<RadioButton
|
||||
selectFunction={props.selectFunction}
|
||||
name={props.name}
|
||||
options={options}
|
||||
checked_variable={props.checked_variable}
|
||||
column={true}
|
||||
ChildComponent={_DownloadButton}
|
||||
downloadStartFunction={props.downloadStartFunction}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import styles from "./DropdownMenu.module.scss";
|
||||
import clsx from "clsx";
|
||||
import ArrowLeftSvg from "@images/arrow_left.svg?react";
|
||||
import { useStore_IsOpenedDropdownMenu } from "@store";
|
||||
|
||||
export const DropdownMenu = (props) => {
|
||||
const { updateIsOpenedDropdownMenu, currentIsOpenedDropdownMenu } = useStore_IsOpenedDropdownMenu();
|
||||
|
||||
const toggleDropdownMenu = () => {
|
||||
if (currentIsOpenedDropdownMenu.data === props.dropdown_id) {
|
||||
updateIsOpenedDropdownMenu("");
|
||||
} else {
|
||||
if (props.openListFunction !== undefined) props.openListFunction();
|
||||
updateIsOpenedDropdownMenu(props.dropdown_id);
|
||||
}
|
||||
};
|
||||
|
||||
const selectValue = (key) => {
|
||||
updateIsOpenedDropdownMenu("");
|
||||
props.selectFunction({
|
||||
dropdown_id: props.dropdown_id,
|
||||
selected_id: key,
|
||||
});
|
||||
};
|
||||
|
||||
const dropdown_content_wrapper_class_name = clsx(styles["dropdown_content_wrapper"], {
|
||||
[styles.is_opened]: (currentIsOpenedDropdownMenu.data === props.dropdown_id) ? true : false,
|
||||
[styles.is_disabled]: props.is_disabled,
|
||||
});
|
||||
|
||||
const dropdown_toggle_button_class_name = clsx(styles["dropdown_toggle_button"], {
|
||||
[styles.is_pending]: (props.state === "pending") ? true : false,
|
||||
[styles.is_disabled]: props.is_disabled,
|
||||
});
|
||||
|
||||
const arrow_class_names = clsx(styles["arrow_left_svg"], {
|
||||
[styles.is_opened]: (currentIsOpenedDropdownMenu.data === props.dropdown_id) ? true : false
|
||||
});
|
||||
|
||||
const getSelectedText = () => {
|
||||
if (props.state !== "ok") return;
|
||||
if (props.list[props.selected_id] === undefined) return props.selected_id; // [Fix me]
|
||||
|
||||
return props.list[props.selected_id];
|
||||
};
|
||||
const list = (props.list === undefined) ? {} : props.list;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={dropdown_toggle_button_class_name} onClick={toggleDropdownMenu} style={props.style}>
|
||||
{(props.state === "pending")
|
||||
? <p className={styles.dropdown_selected_text}>Loading...</p>
|
||||
: <p className={styles.dropdown_selected_text}>{getSelectedText()}</p>
|
||||
}
|
||||
{(props.state === "pending")
|
||||
? <span className={styles.loader}></span>
|
||||
: <ArrowLeftSvg className={arrow_class_names} />
|
||||
}
|
||||
</div>
|
||||
<div className={dropdown_content_wrapper_class_name}>
|
||||
<div className={styles.dropdown_content}>
|
||||
{(props.state === "ok")
|
||||
? Object.entries(list).map(([key, value]) => {
|
||||
return (
|
||||
<div key={key} className={styles.value_button} onClick={() => selectValue(key)}>
|
||||
<p className={styles.value_text}>{value}</p>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
@import "@scss_mixins";
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown_toggle_button {
|
||||
position: relative;
|
||||
background-color: var(--dark_950_color);
|
||||
min-width: 20rem;
|
||||
padding: 0.8rem 1.4rem;
|
||||
cursor: pointer;
|
||||
border-radius: 0.4rem;
|
||||
&:hover {
|
||||
background-color: var(--dark_925_color);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--dark_975_color);
|
||||
}
|
||||
&.is_pending {
|
||||
pointer-events: none;
|
||||
}
|
||||
&.is_disabled {
|
||||
pointer-events: none;
|
||||
.dropdown_selected_text, .arrow_left_svg {
|
||||
color: var(--dark_550_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown_selected_text {
|
||||
font-size: 1.4rem;
|
||||
padding-right: 2.8rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dropdown_content_wrapper {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%; // Position it below the toggle button
|
||||
right: 0;
|
||||
min-width: 20rem;
|
||||
z-index: 1;
|
||||
&.is_opened {
|
||||
display: block;
|
||||
}
|
||||
&.is_disabled {
|
||||
pointer-events: none;
|
||||
.value_text {
|
||||
color: var(--dark_550_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown_content {
|
||||
background-color: var(--dark_900_color);
|
||||
border: 0.1rem solid var(--dark_600_color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
white-space: nowrap;
|
||||
max-height: 20rem;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.value_button {
|
||||
background-color: var(--dark_875_color);
|
||||
padding: 1.2rem;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: var(--dark_800_color);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--dark_900_color);
|
||||
}
|
||||
}
|
||||
|
||||
.value_text {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.loader {
|
||||
@include loader(2rem, 0.2rem, right, 0);
|
||||
}
|
||||
|
||||
.arrow_left_svg {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0;
|
||||
transform: translate(-50%, -50%) rotate(-90deg);
|
||||
width: 1.4rem;
|
||||
&.is_opened {
|
||||
transform: translate(-50%, -50%) rotate(90deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import styles from "./Entry.module.scss";
|
||||
import { _Entry } from "../_atoms/_entry/_Entry";
|
||||
|
||||
export const Entry = (props) => {
|
||||
return (
|
||||
<div className={styles.entry_container}>
|
||||
<_Entry {...props} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
import styles from "./EntryWithSaveButton.module.scss";
|
||||
import { _Entry } from "../_atoms/_entry/_Entry";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import { useI18n } from "@useI18n";
|
||||
import { clsx } from "clsx";
|
||||
|
||||
export const EntryWithSaveButton = (props) => {
|
||||
const { t } = useI18n();
|
||||
const onChangeFunction = (e) => {
|
||||
props.onChangeFunction?.(e.target.value);
|
||||
};
|
||||
const saveFunction = () => {
|
||||
props.saveFunction();
|
||||
};
|
||||
const is_disabled = props.state === "pending";
|
||||
|
||||
const save_button_class_names = clsx(styles.save_button, {
|
||||
[styles.is_disabled]: is_disabled
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<_Entry width={props.width} onChange={onChangeFunction} ui_variable={props.variable} is_disabled={is_disabled}/>
|
||||
<button className={save_button_class_names} onClick={saveFunction}>
|
||||
{is_disabled
|
||||
? <CircularProgress size="1.4rem" sx={{ color: "var(--dark_basic_text_color)" }}/>
|
||||
: <p className={styles.save_button_label}>{t("config_page.translation.deepl_auth_key.save")}</p>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.save_button {
|
||||
padding: 0.8rem 1.2rem;
|
||||
background-color: var(--primary_600_color);
|
||||
border-radius: 0.4rem;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
min-width: 5.4rem;
|
||||
&:hover {
|
||||
background-color: var(--primary_500_color);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--primary_700_color);
|
||||
}
|
||||
&.is_disabled {
|
||||
pointer-events: none;
|
||||
background-color: var(--primary_800_color);
|
||||
}
|
||||
}
|
||||
|
||||
.save_button_label {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import styles from "./HotkeysEntry.module.scss";
|
||||
import { _Entry } from "../_atoms/_entry/_Entry";
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import DeleteSvg from "@images/cancel.svg?react";
|
||||
import clsx from "clsx";
|
||||
|
||||
export const HotkeysEntry = (props) => {
|
||||
const [isAcceptingInput, setIsAcceptingInput] = useState(false);
|
||||
const [displayValue, setDisplayValue] = useState("");
|
||||
const lastKeyRef = useRef(null);
|
||||
const isModifierOnlyRef = useRef(false);
|
||||
const entryRef = useRef(null);
|
||||
const pressedKeys = useRef(new Set());
|
||||
const keysRef = useRef([]);
|
||||
|
||||
useEffect(() => {
|
||||
const init_display_value = props.value[props.hotkey_id] ? props.value[props.hotkey_id].join(" + ") : "";
|
||||
setDisplayValue(init_display_value);
|
||||
}, []);
|
||||
|
||||
const updateHotkeys = (keys) => {
|
||||
entryRef.current.blur();
|
||||
const result = props.setHotkeys({ [props.hotkey_id]: keys });
|
||||
if (result === false) setDisplayValue("");
|
||||
};
|
||||
|
||||
const processKey = (key) => {
|
||||
if (/^[a-zA-Z]$/.test(key)) return key.toUpperCase();
|
||||
if (key === "Meta") return "Super";
|
||||
return key;
|
||||
};
|
||||
|
||||
const handleKeyInput = (event) => {
|
||||
const keys = [];
|
||||
const nonModifierKeys = [];
|
||||
|
||||
["Ctrl", "Shift", "Alt", "Meta"].forEach((modKey) => {
|
||||
if (event[`${modKey.toLowerCase()}Key`] && !keys.includes(modKey)) {
|
||||
let register_mod_key = (modKey === "Meta") ? "Super" : modKey;
|
||||
keys.push(register_mod_key);
|
||||
}
|
||||
});
|
||||
|
||||
const key = processKey(event.key);
|
||||
if (!["Control", "Shift", "Alt", "Meta"].includes(event.key)) {
|
||||
keys.push(key);
|
||||
nonModifierKeys.push(key);
|
||||
}
|
||||
|
||||
if (!pressedKeys.current.has(key)) {
|
||||
pressedKeys.current.add(key);
|
||||
}
|
||||
|
||||
keysRef.current = keys;
|
||||
setDisplayValue(keys.join(" + "));
|
||||
isModifierOnlyRef.current = nonModifierKeys.length === 0;
|
||||
};
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
event.preventDefault();
|
||||
if (lastKeyRef.current === event.key) return;
|
||||
|
||||
lastKeyRef.current = event.key;
|
||||
handleKeyInput(event);
|
||||
};
|
||||
|
||||
const handleKeyUp = (event) => {
|
||||
lastKeyRef.current = null;
|
||||
|
||||
const key = processKey(event.key);
|
||||
pressedKeys.current.delete(key);
|
||||
|
||||
if (isModifierOnlyRef.current) {
|
||||
setDisplayValue("");
|
||||
}
|
||||
|
||||
if (pressedKeys.current.size === 0) {
|
||||
const hasNonModifierKeys = keysRef.current.some(
|
||||
(key) => !["Ctrl", "Shift", "Alt", "Super"].includes(key)
|
||||
);
|
||||
if (hasNonModifierKeys) {
|
||||
updateHotkeys(keysRef.current);
|
||||
} else {
|
||||
const display_value = props.value[props.hotkey_id] ? props.value[props.hotkey_id].join(" + ") : "";
|
||||
setDisplayValue(display_value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
setIsAcceptingInput(false);
|
||||
pressedKeys.current.clear();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
updateHotkeys(null);
|
||||
setDisplayValue("");
|
||||
};
|
||||
|
||||
const is_pending = props.state === "pending";
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{is_pending && <span className={styles.loader}></span>}
|
||||
<_Entry
|
||||
ref={entryRef}
|
||||
type="text"
|
||||
onFocus={() => setIsAcceptingInput(true)}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={handleKeyUp}
|
||||
ui_variable={displayValue}
|
||||
width="20rem"
|
||||
is_activated={isAcceptingInput}
|
||||
is_disabled={is_pending}
|
||||
readOnly
|
||||
/>
|
||||
<button className={clsx(styles.delete_button, { [styles.is_pending]: is_pending })} onClick={handleDelete}>
|
||||
<DeleteSvg className={styles.delete_svg}/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
@import "@scss_mixins";
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.delete_button {
|
||||
padding: 0.4rem;
|
||||
font-size: 1.4rem;
|
||||
// background-color: var(--dark_800_color);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 0.2rem;
|
||||
&:hover {
|
||||
background-color: var(--dark_850_color);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--dark_900_color);
|
||||
}
|
||||
&.is_pending {
|
||||
pointer-events: none;
|
||||
& .delete_svg {
|
||||
color: var(--dark_600_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delete_svg {
|
||||
width: 2.2rem;
|
||||
color: var(--error_bc_color);
|
||||
}
|
||||
|
||||
.loader {
|
||||
@include loader(2rem, 0.2rem, left, -2.2rem);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export { ActionButton } from "./action_button/ActionButton";
|
||||
export { ComputeDevice } from "./compute_device/ComputeDevice";
|
||||
export { DeeplAuthKey, OpenWebpage_DeeplAuthKey } from "./deepl_auth_key/DeeplAuthKey";
|
||||
export { DropdownMenu } from "./dropdown_menu/DropdownMenu";
|
||||
export { Entry } from "./entry/Entry";
|
||||
export { EntryWithSaveButton } from "./entry_with_save_button/EntryWithSaveButton";
|
||||
export { HotkeysEntry } from "./hotkeys_entry/HotkeysEntry";
|
||||
export { LabelComponent } from "./label_component/LabelComponent";
|
||||
export { RadioButton } from "./radio_button/RadioButton";
|
||||
export { SectionLabelComponent } from "./section_label_component/SectionLabelComponent";
|
||||
export { Slider } from "./slider/Slider";
|
||||
export { SwitchBox } from "./switch_box/SwitchBox";
|
||||
export { ThresholdComponent } from "./threshold_component/ThresholdComponent";
|
||||
export { WordFilter, WordFilterListToggleComponent } from "./word_filter/WordFilter";
|
||||
export { DownloadModels } from "./download_models/DownloadModels";
|
||||
export { MessageFormat } from "./message_format/MessageFormat";
|
||||
@@ -0,0 +1,13 @@
|
||||
import styles from "./LabelComponent.module.scss";
|
||||
|
||||
export const LabelComponent = (props) => {
|
||||
return (
|
||||
<div className={styles.label_component}>
|
||||
<p className={styles.label}>{props.label}</p>
|
||||
{props.desc
|
||||
? <p className={styles.desc}>{props.desc}</p>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
.label_component {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
// flex-shrink: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 300;
|
||||
color: var(--dark_500_color);
|
||||
max-width: 38rem;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
import styles from "./MessageFormat.module.scss";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { _Entry } from "../_atoms/_entry/_Entry";
|
||||
import SwapImg from "@images/swap_icon.png";
|
||||
import ArrowLeftSvg from "@images/arrow_left.svg?react";
|
||||
import {
|
||||
useStore_IsBreakPoint,
|
||||
useStore_MessageFormat_ExampleViewFilter,
|
||||
} from "@store";
|
||||
import { useAppearance } from "@logics_configs";
|
||||
import { ui_configs } from "@ui_configs";
|
||||
import { ResetButton } from "@common_components";
|
||||
|
||||
const ENTRY_WIDTH = "8rem";
|
||||
|
||||
const EXAMPLE_TEXTS = {
|
||||
en: "Hello",
|
||||
ja: "こんにちは",
|
||||
ko: "안녕하세요",
|
||||
fr: "Bonjour",
|
||||
};
|
||||
|
||||
export const MessageFormat = (props) => {
|
||||
const { currentIsBreakPoint } = useStore_IsBreakPoint();
|
||||
const message_format_container_class = clsx(styles.container, {
|
||||
[styles.is_break_point]: currentIsBreakPoint.data,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={message_format_container_class}>
|
||||
<ExampleComponent
|
||||
format={props.variable.data}
|
||||
format_id={props.format_id}
|
||||
/>
|
||||
<div className={styles.border}></div>
|
||||
<InputComponent
|
||||
variable={props.variable.data}
|
||||
setFunction={props.setFunction}
|
||||
format_id={props.format_id}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ExampleComponent = ({ format, format_id }) => {
|
||||
const { currentUiLanguage } = useAppearance();
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
currentMessageFormat_ExampleViewFilter,
|
||||
updateMessageFormat_ExampleViewFilter,
|
||||
} = useStore_MessageFormat_ExampleViewFilter();
|
||||
|
||||
const locale_base_path = "config_page.others.message_format_common.example_view.";
|
||||
|
||||
const label_title = t(locale_base_path + "title");
|
||||
|
||||
const label_original_translated = t(locale_base_path + "original_translated");
|
||||
const label_original_translated_multi = t(locale_base_path + "original_translated_multi");
|
||||
const label_translated_only_multi = t(locale_base_path + "translated_only_multi");
|
||||
const label_translated_only = t(locale_base_path + "translated_only");
|
||||
const label_original_only = t(locale_base_path + "original_only");
|
||||
|
||||
const createExampleMessage = (id) => {
|
||||
// 言語順序を決定
|
||||
let example_text_order = [];
|
||||
switch (currentUiLanguage.data) {
|
||||
case "ja":
|
||||
example_text_order = ["ja", "en", "ko", "fr"];
|
||||
break;
|
||||
case "ko":
|
||||
example_text_order = ["ko", "ja", "en", "fr"];
|
||||
break;
|
||||
default: // en
|
||||
example_text_order = ["en", "ja", "ko", "fr"];
|
||||
break;
|
||||
}
|
||||
|
||||
const original = EXAMPLE_TEXTS[example_text_order[0]];
|
||||
const translations = example_text_order.slice(1).map(lang => EXAMPLE_TEXTS[lang]);
|
||||
|
||||
const originalPart = `${format.message.prefix}${original}${format.message.suffix}`;
|
||||
const translationSingle = `${format.translation.prefix}${translations[0]}${format.translation.suffix}`;
|
||||
const translationMulti = `${format.translation.prefix}${translations.join(format.translation.separator)}${format.translation.suffix}`;
|
||||
|
||||
switch (id) {
|
||||
case "original_translated":
|
||||
return format.translation_first
|
||||
? `${translationSingle}${format.separator}${originalPart}`
|
||||
: `${originalPart}${format.separator}${translationSingle}`;
|
||||
|
||||
case "original_only":
|
||||
return originalPart;
|
||||
|
||||
case "translated_only":
|
||||
return translationSingle;
|
||||
|
||||
case "translated_only_multi":
|
||||
return translationMulti;
|
||||
|
||||
case "original_translated_multi":
|
||||
return format.translation_first
|
||||
? `${translationMulti}${format.separator}${originalPart}`
|
||||
: `${originalPart}${format.separator}${translationMulti}`;
|
||||
|
||||
default:
|
||||
throw new Error(`Unexpected id: ${id}`);
|
||||
}
|
||||
};
|
||||
|
||||
const ExampleBox = ({label, example_text_id}) => {
|
||||
return (
|
||||
<div className={styles.example_wrapper}>
|
||||
<p className={styles.example_label}>{label}</p>
|
||||
<div className={styles.example_chatbox}>
|
||||
<p className={styles.example_text}>{createExampleMessage(example_text_id)}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
const svg_class_names = clsx(styles.arrow_left_svg, {
|
||||
[styles.to_down]: currentMessageFormat_ExampleViewFilter.data[format_id] === "Simplified",
|
||||
[styles.to_up]: currentMessageFormat_ExampleViewFilter.data[format_id] === "All"
|
||||
});
|
||||
|
||||
|
||||
const FilteredExampleBox = ({format_id, id}) => {
|
||||
if (format_id === "send" && id === "Simplified") {
|
||||
return (
|
||||
<>
|
||||
<ExampleBox label={label_original_translated} example_text_id="original_translated" />
|
||||
<ExampleBox label={label_original_translated_multi} example_text_id="original_translated_multi" />
|
||||
</>
|
||||
);
|
||||
} else if ( format_id === "send" && id === "All") {
|
||||
return (
|
||||
<>
|
||||
<ExampleBox label={label_original_translated} example_text_id="original_translated" />
|
||||
<ExampleBox label={label_original_translated_multi} example_text_id="original_translated_multi" />
|
||||
<ExampleBox label={label_translated_only_multi} example_text_id="translated_only_multi" />
|
||||
<ExampleBox label={label_translated_only} example_text_id="translated_only" />
|
||||
<ExampleBox label={label_original_only} example_text_id="original_only" />
|
||||
</>
|
||||
);
|
||||
|
||||
} else if (format_id === "received") {
|
||||
return (
|
||||
<>
|
||||
<ExampleBox label={label_original_translated} example_text_id="original_translated" />
|
||||
<ExampleBox label={label_original_only} example_text_id="original_only" />
|
||||
<ExampleBox label={label_translated_only} example_text_id="translated_only" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const exampleViewFilterToggleFunction = (format_id) => {
|
||||
if (["send", "received"].includes(format_id) === false) return console.error(`format_id should be small case 'send' or 'received'. got format_id: ${format_id}`);
|
||||
|
||||
updateMessageFormat_ExampleViewFilter({
|
||||
...currentMessageFormat_ExampleViewFilter.data,
|
||||
[format_id]: currentMessageFormat_ExampleViewFilter.data[format_id] === "Simplified"
|
||||
? "All"
|
||||
: "Simplified"
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.example_container}>
|
||||
<p className={styles.section_title}>{label_title}</p>
|
||||
<div className={styles.example_view_container}>
|
||||
<FilteredExampleBox format_id={format_id} id={currentMessageFormat_ExampleViewFilter.data[format_id]} />
|
||||
</div>
|
||||
{ format_id === "send" &&
|
||||
<div className={styles.show_more_container} onClick={() => exampleViewFilterToggleFunction(format_id)}>
|
||||
<ArrowLeftSvg className={svg_class_names}/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const InputComponent = ({id, variable, setFunction, format_id }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const locale_base_path = "config_page.others.message_format_common.settings.";
|
||||
const label_title = t(locale_base_path + "title");
|
||||
|
||||
const LABEL_ORIGINAL = t(locale_base_path + "original");
|
||||
const LABEL_TRANSLATED = t(locale_base_path + "translated");
|
||||
const LABEL_FOR_MULTI_TRANSLATION = t(locale_base_path + "for_multi_translation");
|
||||
|
||||
const replaceValue = (value) => {
|
||||
if (value === "") return "";
|
||||
|
||||
const replaced = value.replace(/\\n/g, "\n");
|
||||
return replaced;
|
||||
};
|
||||
|
||||
const handleChange = (parent_key, child_key) => (e) => {
|
||||
const rawValue = e.target.value;
|
||||
const parsedValue = replaceValue(rawValue);
|
||||
|
||||
if (child_key !== undefined) {
|
||||
setFunction({
|
||||
...variable,
|
||||
[parent_key]: {
|
||||
...variable[parent_key],
|
||||
[child_key]: parsedValue
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setFunction({
|
||||
...variable,
|
||||
[parent_key]: parsedValue
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const toUiValue = (v) => {
|
||||
if (typeof v === "string") {
|
||||
return v.replace(/\n/g, "\\n");
|
||||
}
|
||||
console.log("Empty");
|
||||
|
||||
return v ?? "";
|
||||
};
|
||||
|
||||
const resetFunction = () => {
|
||||
if (format_id === "send") {
|
||||
setFunction(ui_configs.send_message_format_parts);
|
||||
} else if (format_id === "received") {
|
||||
setFunction(ui_configs.received_message_format_parts);
|
||||
}
|
||||
};
|
||||
|
||||
const SwapButton = ({ variable, setFunction }) => {
|
||||
const swapMessageAndTranslate = () => {
|
||||
setFunction({ ...variable, translation_first: !variable.translation_first });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.swap_button_wrapper} onClick={swapMessageAndTranslate}>
|
||||
<p className={styles.swap_text}>{variable.translation_first ? LABEL_TRANSLATED : LABEL_ORIGINAL}</p>
|
||||
<img className={styles.swap_img} src={SwapImg} alt="Swap Icon" />
|
||||
<p className={styles.swap_text}>{variable.translation_first ? LABEL_ORIGINAL : LABEL_TRANSLATED}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.message_format_settings_container}>
|
||||
<p className={styles.section_title}>{label_title}</p>
|
||||
<div className={styles.message_format_settings_wrapper}>
|
||||
<div className={styles.swap_button_container}>
|
||||
<SwapButton variable={variable} setFunction={setFunction} />
|
||||
</div>
|
||||
{ !variable.translation_first ?
|
||||
<div className={styles.input_wrapper}>
|
||||
<div className={styles.input_contents}>
|
||||
<_Entry ui_variable={toUiValue(variable.message.prefix)} width={ENTRY_WIDTH} onChange={handleChange("message", "prefix")} />
|
||||
<p className={styles.preset_text}>{LABEL_ORIGINAL}</p>
|
||||
<_Entry ui_variable={toUiValue(variable.message.suffix)} width={ENTRY_WIDTH} onChange={handleChange("message", "suffix")} />
|
||||
</div>
|
||||
<div className={styles.input_contents}>
|
||||
<_Entry ui_variable={toUiValue(variable.separator)} width={ENTRY_WIDTH} onChange={handleChange("separator")} />
|
||||
</div>
|
||||
<div className={styles.input_contents}>
|
||||
<_Entry ui_variable={toUiValue(variable.translation.prefix)} width={ENTRY_WIDTH} onChange={handleChange("translation", "prefix")} />
|
||||
<p className={styles.preset_text}>{LABEL_TRANSLATED}</p>
|
||||
<_Entry ui_variable={toUiValue(variable.translation.suffix)} width={ENTRY_WIDTH} onChange={handleChange("translation", "suffix")} />
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div className={styles.input_wrapper}>
|
||||
<div className={styles.input_contents}>
|
||||
<_Entry ui_variable={toUiValue(variable.translation.prefix)} width={ENTRY_WIDTH} onChange={handleChange("translation", "prefix")} />
|
||||
<p className={styles.preset_text}>{LABEL_TRANSLATED}</p>
|
||||
<_Entry ui_variable={toUiValue(variable.translation.suffix)} width={ENTRY_WIDTH} onChange={handleChange("translation", "suffix")} />
|
||||
</div>
|
||||
<div className={styles.input_contents}>
|
||||
<_Entry ui_variable={toUiValue(variable.separator)} width={ENTRY_WIDTH} onChange={handleChange("separator")} />
|
||||
</div>
|
||||
<div className={styles.input_contents}>
|
||||
<_Entry ui_variable={toUiValue(variable.message.prefix)} width={ENTRY_WIDTH} onChange={handleChange("message", "prefix")} />
|
||||
<p className={styles.preset_text}>{LABEL_ORIGINAL}</p>
|
||||
<_Entry ui_variable={toUiValue(variable.message.suffix)} width={ENTRY_WIDTH} onChange={handleChange("message", "suffix")} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{ format_id === "send" &&
|
||||
<div className={styles.multi_translation_input_wrapper}>
|
||||
<p className={styles.multi_translation_title}>{LABEL_FOR_MULTI_TRANSLATION}</p>
|
||||
<div className={styles.input_contents}>
|
||||
<p className={styles.preset_text}>{LABEL_TRANSLATED}</p>
|
||||
<_Entry ui_variable={toUiValue(variable.translation.separator)} width={ENTRY_WIDTH} onChange={handleChange("translation", "separator")} />
|
||||
<p className={styles.preset_text}>{LABEL_TRANSLATED}</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className={styles.reset_button_wrapper}>
|
||||
<ResetButton onClickFunction={resetFunction}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,200 @@
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
gap: 2.6rem;
|
||||
padding-bottom: 2rem;
|
||||
margin: 1rem 0;
|
||||
&.is_break_point {
|
||||
flex-direction: column;
|
||||
gap: 2.2rem;
|
||||
align-items: center;
|
||||
.border {
|
||||
height: 0.1rem;
|
||||
width: 60%;
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.show_more_container {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.example_container {
|
||||
gap: 0;
|
||||
}
|
||||
.message_format_settings_container {
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.border {
|
||||
height: auto;
|
||||
width: 0.1rem;
|
||||
background-color: var(--dark_800_color);
|
||||
margin: 3.2rem 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.section_title {
|
||||
font-size: 1.4rem;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.example_container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
min-width: 14rem;
|
||||
max-width: 34rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.example_view_container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
|
||||
.example_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
.example_label {
|
||||
font-size: 1.4rem;
|
||||
text-align: start;
|
||||
width: 100%;
|
||||
color: var(--dark_basic_text_color);
|
||||
}
|
||||
.example_chatbox {
|
||||
padding: 0.6rem;
|
||||
background-color: #3A4554;
|
||||
border-radius: 1rem;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.example_text {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.show_more_container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0.6rem 0;
|
||||
cursor: pointer;
|
||||
border-radius: 0.6rem;
|
||||
&:hover {
|
||||
background-color: var(--dark_850_color);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--dark_875_color);
|
||||
}
|
||||
}
|
||||
|
||||
.arrow_left_svg {
|
||||
width: 2rem;
|
||||
color: var(--dark_450_color);
|
||||
&.to_down {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
&.to_up {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.message_format_settings_container {
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.message_format_settings_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3.8rem;
|
||||
width: 34rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.swap_button_container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.swap_button_wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
padding: 0.6rem 1.2rem;
|
||||
border-radius: 0.4rem;
|
||||
background-color: var(--dark_850_color);
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: var(--dark_800_color);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--dark_900_color);
|
||||
}
|
||||
}
|
||||
|
||||
.swap_text {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.swap_img {
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
|
||||
.input_wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1.8rem;
|
||||
}
|
||||
|
||||
.input_contents {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
color: var(--dark_basic_text_color);
|
||||
}
|
||||
|
||||
.preset_text {
|
||||
font-size: 1.6rem;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
color: var(--dark_basic_text_color);
|
||||
}
|
||||
|
||||
.multi_translation_input_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
color: var(--dark_basic_text_color);
|
||||
}
|
||||
|
||||
.multi_translation_title {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.reset_button_wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import styles from "./RadioButton.module.scss";
|
||||
import clsx from "clsx";
|
||||
|
||||
export const RadioButton = (props) => {
|
||||
const containerClass = clsx(styles.container, {
|
||||
[styles.column]: props.column === true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={containerClass}>
|
||||
{props.checked_variable.state === "pending" && <span className={styles.loader}></span>}
|
||||
{props.options.map((option) => {
|
||||
const radioWrapperClass = clsx(styles.radio_button_container, {
|
||||
[styles.is_selected]: props.checked_variable.data === option.id,
|
||||
});
|
||||
|
||||
const labelClass = clsx(styles.radio_button_wrapper, {
|
||||
[styles.is_selected]: props.checked_variable.data === option.id,
|
||||
[styles.disabled]: option.disabled === true || props.checked_variable.state === "pending",
|
||||
});
|
||||
|
||||
return (
|
||||
<div key={option.id} className={radioWrapperClass}>
|
||||
<label className={labelClass}>
|
||||
<input
|
||||
className={styles.radio_button_input}
|
||||
type="radio"
|
||||
name={props.name}
|
||||
value={option.id}
|
||||
onChange={() => props.selectFunction(option.id)}
|
||||
checked={props.checked_variable.data === option.id}
|
||||
disabled={option.disabled === true || props.checked_variable.state === "pending"}
|
||||
/>
|
||||
<p className={styles.radio_button_label}>{option.label}</p>
|
||||
</label>
|
||||
{props.ChildComponent && <props.ChildComponent option={option} {...props} />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
@import "@scss_mixins";
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
max-width: 70%;
|
||||
&.column {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.radio_button_container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 2rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
.radio_button_wrapper {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
gap: 1rem;
|
||||
padding: 0.6rem 0.8rem;
|
||||
border-radius: 0.4rem;
|
||||
&:hover {
|
||||
background-color: var(--dark_850_color);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--dark_925_color);
|
||||
}
|
||||
&.is_selected {
|
||||
pointer-events: none;
|
||||
}
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
color: var(--dark_600_color);
|
||||
}
|
||||
}
|
||||
|
||||
.radio_button_input {
|
||||
appearance: none;
|
||||
margin: 0;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: 0.3rem solid var(--dark_600_color);
|
||||
border-radius: 50%;
|
||||
transition: border-color .1s ease, border-width .1s ease;
|
||||
flex-shrink: 0;
|
||||
cursor: inherit;
|
||||
&:checked {
|
||||
border-color: var(--primary_400_color);
|
||||
border-width: 0.6rem;
|
||||
}
|
||||
&:disabled {
|
||||
border-color: var(--dark_825_color);
|
||||
}
|
||||
}
|
||||
|
||||
.radio_button_label {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 400;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.loader {
|
||||
@include loader(2rem, 0.2rem, left, -1.6rem);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import styles from "./SectionLabelComponent.module.scss";
|
||||
import clsx from "clsx";
|
||||
|
||||
export const SectionLabelComponent = (props) => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<label className={styles.section_label}>{props.label}</label>
|
||||
<div className={styles.section_line}></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
.section_label {
|
||||
font-size: 2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.section_line {
|
||||
width: 100%;
|
||||
height: 0.1rem;
|
||||
background: linear-gradient(90deg, var(--dark_400_color) 0%, var(--dark_600_color) 35%, var(--dark_800_color) 100%);
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import React from "react";
|
||||
import styles from "./Slider.module.scss";
|
||||
import MUI_Slider from "@mui/material/Slider";
|
||||
import clsx from "clsx";
|
||||
|
||||
export const Slider = (props) => {
|
||||
const location = props.valueLabelDisplayLocation || "top";
|
||||
|
||||
const sliderSx = {
|
||||
color: "var(--dark_700_color)",
|
||||
"& .MuiSlider-thumb": {
|
||||
backgroundColor: "var(--primary_600_color)",
|
||||
"&:hover, &.Mui-focusVisible, &.Mui-active": {
|
||||
boxShadow: `0 0 0 0.8rem var(--primary_600_color_44)`,
|
||||
},
|
||||
"& .MuiSlider-valueLabel": {
|
||||
position: "absolute",
|
||||
backgroundColor: "var(--dark_800_color)",
|
||||
width: "fit-content",
|
||||
minWidth: "4.8rem",
|
||||
padding: "0.4rem 0.8rem",
|
||||
lineHeight: "1.15",
|
||||
"& .MuiSlider-valueLabelLabel": {
|
||||
fontSize: "1.4rem",
|
||||
},
|
||||
...(location === "top" && {
|
||||
top: "-110%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%) scale(0)",
|
||||
transformOrigin: "bottom center",
|
||||
"&.MuiSlider-valueLabelOpen": {
|
||||
transform: "translate(-50%, -50%) scale(1)",
|
||||
},
|
||||
"&::before": {
|
||||
bottom: "0%",
|
||||
left: "50%",
|
||||
},
|
||||
}),
|
||||
...(location === "right" && {
|
||||
top: "50%",
|
||||
left: "150%",
|
||||
transform: "translate(0, -50%) scale(0)",
|
||||
transformOrigin: "left center",
|
||||
"&.MuiSlider-valueLabelOpen": {
|
||||
transform: "translate(0, -50%) scale(1)",
|
||||
},
|
||||
"&::before": {
|
||||
bottom: "50%",
|
||||
left: "0",
|
||||
},
|
||||
}),
|
||||
...(location === "left" && {
|
||||
// top: "50%",
|
||||
// right: "50%",
|
||||
// transform: "translate(-50%, -50%) scale(0)",
|
||||
// transformOrigin: "bottom center",
|
||||
// "&.MuiSlider-valueLabelOpen": {
|
||||
// transform: "translate(-50%, -50%) scale(1)",
|
||||
// },
|
||||
// "&::before": {
|
||||
// bottom: "50%",
|
||||
// left: "100%",
|
||||
// },
|
||||
}),
|
||||
},
|
||||
},
|
||||
"& .MuiSlider-markLabel": {
|
||||
fontSize: "1.4rem",
|
||||
color: "var(--dark_550_color)",
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
"& .MuiSlider-markLabelActive": {
|
||||
color: "var(--primary_300_color)",
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
styles.container,
|
||||
props.className,
|
||||
{ [styles.no_padding]: props.no_padding || props.is_break_point }
|
||||
)}
|
||||
>
|
||||
<MUI_Slider
|
||||
aria-label="Default"
|
||||
// valueLabelDisplay="on"
|
||||
valueLabelDisplay={props.valueLabelDisplay ? props.valueLabelDisplay : "auto"}
|
||||
value={props.variable}
|
||||
step={props.step}
|
||||
min={Number(props.min)}
|
||||
max={Number(props.max)}
|
||||
onChange={(_e, value) => props.onchangeFunction(value)}
|
||||
onChangeCommitted={(_e, value) =>
|
||||
props.onchangeCommittedFunction ? props.onchangeCommittedFunction(value) : null
|
||||
}
|
||||
onMouseEnter={(event) =>
|
||||
props.onMouseEnterFunction ? props.onMouseEnterFunction(event) : null
|
||||
}
|
||||
onMouseLeave={(event) =>
|
||||
props.onMouseLeaveFunction ? props.onMouseLeaveFunction(event) : null
|
||||
}
|
||||
marks={props.marks}
|
||||
track={props.track}
|
||||
orientation={props.orientation}
|
||||
valueLabelFormat={`${props.valueLabelFormat ? props.valueLabelFormat : props.variable}`}
|
||||
sx={sliderSx}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding-left: 4rem;
|
||||
&.no_padding {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import clsx from "clsx";
|
||||
import { useState } from "react";
|
||||
import styles from "./SwitchBox.module.scss";
|
||||
|
||||
export const SwitchBox = (props) => {
|
||||
const [is_hovered, setIsHovered] = useState(false);
|
||||
const [is_mouse_down, setIsMouseDown] = useState(false);
|
||||
|
||||
const is_pending = (props.variable.state === "pending");
|
||||
|
||||
const getClassNames = (base_class) => clsx(base_class, {
|
||||
[styles.is_active]: (props.variable.data === true),
|
||||
[styles.is_pending]: is_pending,
|
||||
[styles.is_hovered]: is_hovered,
|
||||
[styles.is_mouse_down]: is_mouse_down,
|
||||
});
|
||||
|
||||
const onMouseEnter = () => setIsHovered(true);
|
||||
const onMouseLeave = () => setIsHovered(false);
|
||||
const onMouseDown = () => setIsMouseDown(true);
|
||||
const onMouseUp = () => setIsMouseDown(false);
|
||||
|
||||
const toggleFunction = () => {
|
||||
props.toggleFunction();
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className={styles.switchbox_container}>
|
||||
<div className={getClassNames(styles.switchbox_wrapper)}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseUp={onMouseUp}
|
||||
onClick={toggleFunction}
|
||||
>
|
||||
<div className={getClassNames(styles.toggle_control)}>
|
||||
<span className={getClassNames(styles.control)}></span>
|
||||
{is_pending && <span className={styles.loader}></span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
@import "@scss_mixins";
|
||||
|
||||
.switchbox_container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.switchbox_wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
padding: 2rem;
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
&.is_pending {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle_control {
|
||||
position: relative;
|
||||
@include toggle_control_styles;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loader {
|
||||
@include loader(2rem, 0.2rem, right, -4rem);
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import styles from "./ThresholdComponent.module.scss";
|
||||
import { SliderAndMeter } from "./slider_and_meter/SliderAndMeter";
|
||||
import { ThresholdEntry } from "./threshold_entry/ThresholdEntry";
|
||||
import { VolumeCheckButton } from "./volume_check_button/VolumeCheckButton";
|
||||
import { useVolume } from "@logics_common";
|
||||
import MicSvg from "@images/mic.svg?react";
|
||||
import HeadphonesSvg from "@images/headphones.svg?react";
|
||||
import {
|
||||
useDevice,
|
||||
} from "@logics_configs";
|
||||
|
||||
export const ThresholdComponent = (props) => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{props.id === "mic_threshold"
|
||||
? <MicComponent {...props} />
|
||||
: <SpeakerComponent {...props} />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const MicComponent = (props) => {
|
||||
const {
|
||||
currentMicThreshold,
|
||||
setMicThreshold,
|
||||
currentEnableAutomaticMicThreshold,
|
||||
} = useDevice();
|
||||
const [ui_threshold, setUiThreshold] = useState(currentMicThreshold.data);
|
||||
const {
|
||||
volumeCheckStart_Mic,
|
||||
volumeCheckStop_Mic,
|
||||
currentMicThresholdCheckStatus,
|
||||
} = useVolume();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (currentEnableAutomaticMicThreshold.data === true) {
|
||||
setUiThreshold("Auto");
|
||||
} else {
|
||||
setUiThreshold(currentMicThreshold.data);
|
||||
}
|
||||
}, [currentMicThreshold.data, currentEnableAutomaticMicThreshold]);
|
||||
|
||||
const setUiThresholdFunction = (payload_ui_threshold) => {
|
||||
setUiThreshold(payload_ui_threshold);
|
||||
};
|
||||
const setThresholdFunction = (payload_threshold) => {
|
||||
setMicThreshold(payload_threshold);
|
||||
};
|
||||
|
||||
const is_disable = currentEnableAutomaticMicThreshold.data === true ? true : false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<VolumeCheckButton
|
||||
{...props}
|
||||
SvgComponent={MicSvg}
|
||||
startFunction={volumeCheckStart_Mic}
|
||||
stopFunction={volumeCheckStop_Mic}
|
||||
isChecking={currentMicThresholdCheckStatus}
|
||||
/>
|
||||
<SliderAndMeter
|
||||
{...props}
|
||||
ui_threshold={ui_threshold}
|
||||
setUiThresholdFunction={setUiThresholdFunction}
|
||||
setThresholdFunction={setThresholdFunction}
|
||||
/>
|
||||
<ThresholdEntry
|
||||
{...props}
|
||||
ui_threshold={ui_threshold}
|
||||
setUiThresholdFunction={setUiThresholdFunction}
|
||||
setThresholdFunction={setThresholdFunction}
|
||||
is_disable={is_disable}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SpeakerComponent = (props) => {
|
||||
const {
|
||||
currentSpeakerThreshold,
|
||||
setSpeakerThreshold,
|
||||
currentEnableAutomaticSpeakerThreshold,
|
||||
} = useDevice();
|
||||
const [ui_threshold, setUiThreshold] = useState(currentSpeakerThreshold.data);
|
||||
const {
|
||||
volumeCheckStart_Speaker,
|
||||
volumeCheckStop_Speaker,
|
||||
currentSpeakerThresholdCheckStatus,
|
||||
} = useVolume();
|
||||
|
||||
useEffect(() => {
|
||||
if (currentEnableAutomaticSpeakerThreshold.data === true) {
|
||||
setUiThreshold("Auto");
|
||||
} else {
|
||||
setUiThreshold(currentSpeakerThreshold.data);
|
||||
}
|
||||
}, [currentSpeakerThreshold.data, currentEnableAutomaticSpeakerThreshold]);
|
||||
|
||||
const setUiThresholdFunction = (payload_ui_threshold) => {
|
||||
setUiThreshold(payload_ui_threshold);
|
||||
};
|
||||
const setThresholdFunction = (payload_threshold) => {
|
||||
setSpeakerThreshold(payload_threshold);
|
||||
};
|
||||
|
||||
const is_disable = currentEnableAutomaticSpeakerThreshold.data === true ? true : false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<VolumeCheckButton
|
||||
{...props}
|
||||
SvgComponent={HeadphonesSvg}
|
||||
startFunction={volumeCheckStart_Speaker}
|
||||
stopFunction={volumeCheckStop_Speaker}
|
||||
isChecking={currentSpeakerThresholdCheckStatus}
|
||||
/>
|
||||
<SliderAndMeter
|
||||
{...props}
|
||||
ui_threshold={ui_threshold}
|
||||
setUiThresholdFunction={setUiThresholdFunction}
|
||||
setThresholdFunction={setThresholdFunction}
|
||||
/>
|
||||
<ThresholdEntry
|
||||
{...props}
|
||||
ui_threshold={ui_threshold}
|
||||
setUiThresholdFunction={setUiThresholdFunction}
|
||||
setThresholdFunction={setThresholdFunction}
|
||||
is_disable={is_disable}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
.container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import styles from "./SliderAndMeter.module.scss";
|
||||
import {
|
||||
useStore_MicVolume,
|
||||
useStore_SpeakerVolume,
|
||||
} from "@store";
|
||||
import {
|
||||
useDevice,
|
||||
} from "@logics_configs";
|
||||
|
||||
export const SliderAndMeter = (props) => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.meter_container}>
|
||||
{props.id === "mic_threshold"
|
||||
? <ThresholdVolumeMeter_Mic {...props}/>
|
||||
: <ThresholdVolumeMeter_Speaker {...props}/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ThresholdVolumeMeter_Mic = (props) => {
|
||||
const { currentMicVolume } = useStore_MicVolume();
|
||||
|
||||
const { currentEnableAutomaticMicThreshold } = useDevice();
|
||||
|
||||
const currentVolumeVariable = Math.min(currentMicVolume.data, props.max);
|
||||
const volume_width_percentage = (currentVolumeVariable / props.max) * 100;
|
||||
|
||||
return (
|
||||
<>
|
||||
<VolumeMeter volume_width_percentage={volume_width_percentage} volume={currentVolumeVariable} threshold={props.ui_threshold}/>
|
||||
{currentEnableAutomaticMicThreshold.data === false &&
|
||||
<input
|
||||
type="range"
|
||||
min={props.min}
|
||||
max={props.max}
|
||||
value={props.ui_threshold}
|
||||
onChange={(e) => props.setUiThresholdFunction(e.target.value)}
|
||||
onMouseUp={(e) => props.setThresholdFunction(e.target.value)}
|
||||
className={styles.threshold_slider}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ThresholdVolumeMeter_Speaker = (props) => {
|
||||
const { currentSpeakerVolume } = useStore_SpeakerVolume();
|
||||
|
||||
const { currentEnableAutomaticSpeakerThreshold } = useDevice();
|
||||
|
||||
const currentVolumeVariable = Math.min(currentSpeakerVolume.data, props.max);
|
||||
const volume_width_percentage = (currentVolumeVariable / props.max) * 100;
|
||||
|
||||
return (
|
||||
<>
|
||||
<VolumeMeter volume_width_percentage={volume_width_percentage} volume={currentVolumeVariable} threshold={props.ui_threshold} />
|
||||
{currentEnableAutomaticSpeakerThreshold.data === false &&
|
||||
<input
|
||||
type="range"
|
||||
min={props.min}
|
||||
max={props.max}
|
||||
value={props.ui_threshold}
|
||||
onChange={(e) => props.setUiThresholdFunction(e.target.value)}
|
||||
onMouseUp={(e) => props.setThresholdFunction(e.target.value)}
|
||||
className={styles.threshold_slider}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const VolumeMeter = ({ volume_width_percentage, volume, threshold }) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.volume_meter}
|
||||
style={{
|
||||
width: `${volume_width_percentage}%`,
|
||||
backgroundColor: (volume < threshold) ? "var(--primary_750_color)" : "var(--primary_400_color)"
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
flex: 1;
|
||||
// width: 100%;
|
||||
position: relative; // for dev
|
||||
}
|
||||
|
||||
.meter_container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 0.8rem;
|
||||
background: var(--dark_800_color);
|
||||
border-radius: 0.4rem;
|
||||
}
|
||||
|
||||
.volume_meter {
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
transition: width 0.1s ease, background-color 0.1s ease;
|
||||
}
|
||||
|
||||
.threshold_slider {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: all;
|
||||
|
||||
&::-webkit-slider-runnable-track {
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 0.4rem;
|
||||
height: 4rem;
|
||||
background: var(--primary_600_color);
|
||||
border-radius: 0.2rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover::-webkit-slider-thumb{
|
||||
background: var(--primary_500_color);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import clsx from "clsx";
|
||||
import styles from "./ThresholdEntry.module.scss";
|
||||
|
||||
export const ThresholdEntry = (props) => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.entry_wrapper}>
|
||||
{props.id === "mic_threshold"
|
||||
? <ThresholdEntry_Mic {...props}/>
|
||||
: <ThresholdEntry_Speaker {...props}/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ThresholdEntry_Mic = (props) => {
|
||||
const onChangeFunction = (e) => {
|
||||
if (e.currentTarget.value === "") {
|
||||
props.setThresholdFunction("0");
|
||||
} else {
|
||||
props.setThresholdFunction(e.currentTarget.value);
|
||||
}
|
||||
};
|
||||
|
||||
const class_names = clsx(styles.entry_input_area, {
|
||||
[styles.is_disable]: props.is_disable
|
||||
});
|
||||
|
||||
return (
|
||||
<input
|
||||
className={class_names}
|
||||
onChange={onChangeFunction}
|
||||
value={props.ui_threshold}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ThresholdEntry_Speaker = (props) => {
|
||||
const onChangeFunction = (e) => {
|
||||
if (e.currentTarget.value === "") {
|
||||
props.setThresholdFunction("0");
|
||||
} else {
|
||||
props.setThresholdFunction(e.currentTarget.value);
|
||||
}
|
||||
};
|
||||
|
||||
const class_names = clsx(styles.entry_input_area, {
|
||||
[styles.is_disable]: props.is_disable
|
||||
});
|
||||
|
||||
return (
|
||||
<input
|
||||
className={class_names}
|
||||
onChange={onChangeFunction}
|
||||
value={props.ui_threshold}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
.container {
|
||||
|
||||
}
|
||||
|
||||
.entry_wrapper {
|
||||
width: 6rem;
|
||||
height: 100%;
|
||||
padding: 0.6rem;
|
||||
background-color: var(--dark_875_color);
|
||||
border: 0.1rem solid var(--dark_750_color);
|
||||
border-radius: 0.4rem;
|
||||
}
|
||||
|
||||
.entry_input_area {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 1.4rem;
|
||||
resize: none;
|
||||
&.is_disable {
|
||||
color: var(--dark_500_color);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from "react";
|
||||
import { useI18n } from "@useI18n";
|
||||
import clsx from "clsx";
|
||||
import styles from "./VolumeCheckButton.module.scss";
|
||||
|
||||
export const VolumeCheckButton = React.memo((props) => {
|
||||
const { t } = useI18n();
|
||||
const getClassNames = (baseClass) => clsx(baseClass, {
|
||||
[styles.is_active]: (props.isChecking?.data === true),
|
||||
[styles.is_pending]: (props.isChecking.state === "pending"),
|
||||
});
|
||||
|
||||
const toggleFunction = () => {
|
||||
if (props.isChecking?.data === true) {
|
||||
props.stopFunction();
|
||||
} else if (props.isChecking?.data === false) {
|
||||
props.startFunction();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={getClassNames(styles.button)} onClick={toggleFunction}>
|
||||
<props.SvgComponent className={styles.button_svg} />
|
||||
<p className={styles.button_text}>{t("config_page.device.check_volume")}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
VolumeCheckButton.displayName = "VolumeCheckButton";
|
||||
@@ -0,0 +1,44 @@
|
||||
.button {
|
||||
width: 100%;
|
||||
background-color: var(--dark_800_color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0.8rem 1rem;
|
||||
border-radius: 0.4rem;
|
||||
gap: 0.4rem;
|
||||
cursor: pointer;
|
||||
&.is_active {
|
||||
background-color: var(--primary_500_color);
|
||||
&:hover {
|
||||
background-color: var(--primary_450_color);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--primary_550_color);
|
||||
}
|
||||
}
|
||||
&.is_pending {
|
||||
background-color: var(--dark_850_color);
|
||||
pointer-events: none;
|
||||
.button_svg, .button_text {
|
||||
color: var(--dark_500_color);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
&:hover {
|
||||
background-color: var(--dark_775_color);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--dark_850_color);
|
||||
}
|
||||
}
|
||||
|
||||
.button_svg {
|
||||
width: 1.4rem;
|
||||
color: var(--dark_350_color);
|
||||
}
|
||||
|
||||
.button_text {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { useI18n } from "@useI18n";
|
||||
import styles from "./WordFilter.module.scss";
|
||||
import { _Entry } from "../_atoms/_entry/_Entry";
|
||||
import { useState } from "react";
|
||||
import { useStore_IsOpenedMicWordFilterList } from "@store";
|
||||
import { useTranscription } from "@logics_configs";
|
||||
|
||||
export const WordFilter = () => {
|
||||
const { t } = useI18n();
|
||||
|
||||
const [input_value, setInputValue] = useState("");
|
||||
const { currentMicWordFilterList, updateMicWordFilterList, setMicWordFilterList } = useTranscription();
|
||||
const { currentIsOpenedMicWordFilterList, updateIsOpenedMicWordFilterList } = useStore_IsOpenedMicWordFilterList();
|
||||
|
||||
const onChangeEntry = (e) => {
|
||||
setInputValue(e.target.value);
|
||||
};
|
||||
|
||||
const addWords = () => {
|
||||
if (input_value === undefined) return;
|
||||
updateMicWordFilterList((prev_list) => {
|
||||
const input_value_array = input_value.split(",");
|
||||
let updated_list = [...prev_list.data];
|
||||
for (let each_input_value of input_value_array) {
|
||||
each_input_value = each_input_value.trim();
|
||||
if (each_input_value) {
|
||||
const exists = updated_list.find((item) => item === each_input_value);
|
||||
if (!exists) {
|
||||
updated_list = [...updated_list, each_input_value];
|
||||
}
|
||||
}
|
||||
}
|
||||
setMicWordFilterList(updated_list);
|
||||
return updated_list;
|
||||
});
|
||||
|
||||
updateIsOpenedMicWordFilterList(true);
|
||||
setInputValue("");
|
||||
};
|
||||
|
||||
|
||||
const deleteAction = (target_item_value) => {
|
||||
updateMicWordFilterList((prev_list) => {
|
||||
const updated_list = prev_list.data.filter((item) => item !== target_item_value);
|
||||
setMicWordFilterList(updated_list);
|
||||
return updated_list;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{ currentIsOpenedMicWordFilterList.data &&
|
||||
<div className={styles.list_section_wrapper}>
|
||||
{
|
||||
currentMicWordFilterList.data.map((item, index) => {
|
||||
return <WordFilterItem value={item} key={index} deleteAction={deleteAction}/>;
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div className={styles.entry_section_wrapper}>
|
||||
<_Entry width="30rem" onChange={onChangeEntry} ui_variable={input_value}/>
|
||||
<button className={styles.add_button} onClick={addWords}>{t("config_page.transcription.mic_word_filter.add_button_label")}</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
import DeleteSvg from "@images/cancel.svg?react";
|
||||
import clsx from "clsx";
|
||||
const WordFilterItem = (props) => {
|
||||
const item_wrapper_class_names = clsx(styles["item_wrapper"]);
|
||||
const item_text_class_names = clsx(styles["item_text"]);
|
||||
|
||||
return (
|
||||
<div className={item_wrapper_class_names}>
|
||||
<p className={item_text_class_names}>{props.value}</p>
|
||||
<button className={clsx(styles.action_button, styles.delete)} onClick={() => props.deleteAction(props.value)}>
|
||||
<DeleteSvg className={styles.delete_svg}/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
import ArrowLeftSvg from "@images/arrow_left.svg?react";
|
||||
export const WordFilterListToggleComponent = () => {
|
||||
const { t } = useI18n();
|
||||
const { currentIsOpenedMicWordFilterList, updateIsOpenedMicWordFilterList } = useStore_IsOpenedMicWordFilterList();
|
||||
const { currentMicWordFilterList } = useTranscription();
|
||||
|
||||
const svg_class_names = clsx(styles["arrow_left_svg"], {
|
||||
[styles.to_down]: !currentIsOpenedMicWordFilterList.data,
|
||||
[styles.to_up]: currentIsOpenedMicWordFilterList.data
|
||||
});
|
||||
|
||||
const OnclickFunction = () => {
|
||||
updateIsOpenedMicWordFilterList(!currentIsOpenedMicWordFilterList.data);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.toggle_button_container}>
|
||||
<p className={styles.words_count_text}>{t("config_page.transcription.mic_word_filter.count_desc", {count: currentMicWordFilterList.data.length} )}</p>
|
||||
<button className={styles.toggle_button_wrapper} onClick={OnclickFunction}>
|
||||
<ArrowLeftSvg className={svg_class_names}/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
|
||||
.list_section_wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
gap: 0.6rem;
|
||||
overflow-y: auto;
|
||||
max-height: 20rem;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
.item_wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
background-color: var(--dark_800_color);
|
||||
padding: 0.2rem 0.2rem 0.2rem 1rem;
|
||||
border-radius: 0.4rem;
|
||||
}
|
||||
.item_text {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.action_button {
|
||||
border-radius: 0.4rem;
|
||||
&:hover {
|
||||
background-color: var(--dark_750_color);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--dark_850_color);
|
||||
}
|
||||
&.delete {
|
||||
padding: 0.2rem;
|
||||
}
|
||||
&.redo {
|
||||
padding: 0.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.delete_svg {
|
||||
width: 2.4rem;
|
||||
color: var(--error_bc_color);
|
||||
}
|
||||
|
||||
.redo_svg {
|
||||
width: 1.6rem;
|
||||
color: var(--dark_500_color);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.entry_section_wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.add_button {
|
||||
padding: 0.8rem 1.2rem;
|
||||
background-color: var(--primary_600_color);
|
||||
font-size: 1.4rem;
|
||||
border-radius: 0.4rem;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
&:hover {
|
||||
background-color: var(--primary_500_color);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--primary_700_color);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.toggle_button_container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.words_count_text {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.toggle_button_wrapper {
|
||||
padding: 1.6rem;
|
||||
border-radius: 0.4rem;
|
||||
&:hover {
|
||||
background-color: var(--dark_825_color);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--dark_900_color);
|
||||
}
|
||||
}
|
||||
|
||||
.arrow_left_svg {
|
||||
width: 2.4rem;
|
||||
&.to_down {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
&.to_up {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import clsx from "clsx";
|
||||
|
||||
import styles from "./Templates.module.scss";
|
||||
import { useStore_IsOpenedDropdownMenu, useStore_IsBreakPoint } from "@store";
|
||||
import {
|
||||
LabelComponent,
|
||||
DropdownMenu,
|
||||
Slider,
|
||||
SwitchBox,
|
||||
Entry,
|
||||
EntryWithSaveButton,
|
||||
HotkeysEntry,
|
||||
RadioButton,
|
||||
OpenWebpage_DeeplAuthKey,
|
||||
DeeplAuthKey,
|
||||
ActionButton,
|
||||
ComputeDevice,
|
||||
WordFilter,
|
||||
WordFilterListToggleComponent,
|
||||
DownloadModels,
|
||||
MessageFormat,
|
||||
} from "../_components";
|
||||
import { Checkbox } from "@common_components";
|
||||
|
||||
const LabeledContainer = ({ children, label, desc, custom_class_name }) => (
|
||||
<div className={clsx(styles.container, custom_class_name)}>
|
||||
<LabelComponent label={label} desc={desc} />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const useOnMouseLeaveDropdownMenu = () => {
|
||||
const { updateIsOpenedDropdownMenu } = useStore_IsOpenedDropdownMenu();
|
||||
|
||||
const onMouseLeaveFunction = () => {
|
||||
updateIsOpenedDropdownMenu("");
|
||||
};
|
||||
|
||||
return { onMouseLeaveFunction };
|
||||
};
|
||||
|
||||
export const DropdownMenuContainer = (props) => {
|
||||
const { onMouseLeaveFunction } = useOnMouseLeaveDropdownMenu();
|
||||
return (
|
||||
<div className={styles.container} onMouseLeave={onMouseLeaveFunction}>
|
||||
<LabelComponent label={props.label} desc={props.desc} />
|
||||
<DropdownMenu {...props} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CommonContainer = ({ Component, ...props }) => {
|
||||
const { currentIsBreakPoint } = useStore_IsBreakPoint();
|
||||
|
||||
const container_class = clsx(styles.container, {
|
||||
[styles.is_break_point]: props.add_break_point ?? currentIsBreakPoint.data,
|
||||
});
|
||||
|
||||
return (
|
||||
<LabeledContainer label={props.label} desc={props.desc} custom_class_name={container_class}>
|
||||
<Component {...props} is_break_point={currentIsBreakPoint.data} />
|
||||
</LabeledContainer>
|
||||
);
|
||||
};
|
||||
export const SliderContainer = (props) => (
|
||||
<CommonContainer Component={Slider} {...props} />
|
||||
);
|
||||
|
||||
export const CheckboxContainer = (props) => (
|
||||
<CommonContainer Component={Checkbox} {...props} add_break_point={false} />
|
||||
);
|
||||
|
||||
export const SwitchBoxContainer = (props) => (
|
||||
<CommonContainer Component={SwitchBox} {...props} add_break_point={false}/>
|
||||
);
|
||||
|
||||
export const EntryContainer = (props) => (
|
||||
<CommonContainer Component={Entry} {...props} add_break_point={false} />
|
||||
);
|
||||
export const EntryWithSaveButtonContainer = (props) => (
|
||||
<CommonContainer Component={EntryWithSaveButton} {...props} add_break_point={false} />
|
||||
);
|
||||
|
||||
export const HotkeysEntryContainer = (props) => (
|
||||
<CommonContainer Component={HotkeysEntry} {...props} />
|
||||
);
|
||||
|
||||
export const RadioButtonContainer = (props) => (
|
||||
<CommonContainer Component={RadioButton} {...props} />
|
||||
);
|
||||
|
||||
export const DeeplAuthKeyContainer = (props) => {
|
||||
const { currentIsBreakPoint } = useStore_IsBreakPoint();
|
||||
const container_class = clsx(styles.container, {
|
||||
[styles.is_break_point]: currentIsBreakPoint.data,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={container_class}>
|
||||
<div className={styles.deepl_auth_key_label_section}>
|
||||
<LabelComponent label={props.label} desc={props.desc} />
|
||||
<OpenWebpage_DeeplAuthKey />
|
||||
</div>
|
||||
<DeeplAuthKey {...props} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ActionButtonContainer = (props) => (
|
||||
<CommonContainer Component={ActionButton} {...props} add_break_point={false}/>
|
||||
);
|
||||
|
||||
export const ComputeDeviceContainer = (props) => (
|
||||
<CommonContainer Component={ComputeDevice} {...props} />
|
||||
);
|
||||
|
||||
export const WordFilterContainer = (props) => (
|
||||
<div className={styles.word_filter_container}>
|
||||
<div className={styles.word_filter_switch_section}>
|
||||
<div className={styles.word_filter_label_wrapper}>
|
||||
<LabelComponent label={props.label} desc={props.desc} />
|
||||
</div>
|
||||
<WordFilterListToggleComponent />
|
||||
</div>
|
||||
<div className={styles.word_filter_section}>
|
||||
<WordFilter {...props} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const DownloadModelsContainer = (props) => (
|
||||
<CommonContainer Component={DownloadModels} {...props} />
|
||||
);
|
||||
|
||||
export const MessageFormatContainer = (props) => {
|
||||
return (
|
||||
<div className={clsx(styles.container, styles.flex_column)}>
|
||||
<div className={styles.label_only_section}>
|
||||
<LabelComponent label={props.label} desc={props.desc} />
|
||||
</div>
|
||||
<div className={styles.message_format_section}>
|
||||
<MessageFormat {...props}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
.container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
gap: 2rem;
|
||||
&.flex_column {
|
||||
flex-direction: column;
|
||||
}
|
||||
border-bottom: solid 0.1rem var(--dark_800_color);
|
||||
|
||||
&.is_break_point {
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
align-items: start;
|
||||
}
|
||||
}
|
||||
|
||||
.label_only_section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.deepl_auth_key_label_section {
|
||||
max-width: 34rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1.4rem;
|
||||
}
|
||||
|
||||
.message_format_section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.word_filter_container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.word_filter_switch_section {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.word_filter_label_wrapper {
|
||||
max-width: 34rem;
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import styles from "./AboutVrct.module.scss";
|
||||
import dev_section_title from "@images/about_vrct/dev_section_title.png";
|
||||
import dev_misya from "@images/about_vrct/dev_misya.png";
|
||||
import dev_shiina from "@images/about_vrct/dev_shiina.png";
|
||||
import vrct_logo_for_about_vrct from "@images/about_vrct/vrct_logo_for_about_vrct.png";
|
||||
|
||||
import contributors_section_title from "@images/about_vrct/contributors_section_title.png";
|
||||
import contributor_done from "@images/about_vrct/contributor_done.png";
|
||||
import contributor_iya from "@images/about_vrct/contributor_iya.png";
|
||||
import contributor_rera from "@images/about_vrct/contributor_rera.png";
|
||||
import contributor_poposuke from "@images/about_vrct/contributor_poposuke.png";
|
||||
import contributor_kumaguma from "@images/about_vrct/contributor_kumaguma.png";
|
||||
import contributor_riku from "@images/about_vrct/contributor_riku.png";
|
||||
|
||||
import localization_section_title from "@images/about_vrct/localization_section_title.png";
|
||||
import localization_1 from "@images/about_vrct/localization_1.png";
|
||||
import localization_2 from "@images/about_vrct/localization_2.png";
|
||||
import localization_3 from "@images/about_vrct/localization_3.png";
|
||||
import localization_4 from "@images/about_vrct/localization_4.png";
|
||||
import localization_5 from "@images/about_vrct/localization_5.png";
|
||||
|
||||
import special_thanks_section_title from "@images/about_vrct/special_thanks_section_title.png";
|
||||
import special_thanks_members from "@images/about_vrct/special_thanks_members.png";
|
||||
import special_thanks_message_en from "@images/about_vrct/special_thanks_message_en.png";
|
||||
import special_thanks_message_ja from "@images/about_vrct/special_thanks_message_ja.png";
|
||||
|
||||
import poster_showcase_section_title from "@images/about_vrct/poster_showcase_section_title.png";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { useI18n } from "@useI18n";
|
||||
import { useAppearance } from "@logics_configs";
|
||||
import { PosterShowcaseContents } from "./poster_showcase_contents/PosterShowcaseContents";
|
||||
|
||||
export const AboutVrct = () => {
|
||||
const { t } = useI18n();
|
||||
const { currentUiLanguage } = useAppearance();
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.dev_section}>
|
||||
<img src={dev_section_title} className={clsx(styles.section_title, styles.the_developers)} />
|
||||
<div className={styles.dev_section_wrapper}>
|
||||
<div className={styles.dev_card_wrapper}>
|
||||
<img src={dev_misya} className={styles.dev_card_img} />
|
||||
<OpenLinkContainer className={styles.dev_misya_x} href_id="dev_misya_x" />
|
||||
<OpenLinkContainer className={styles.dev_misya_github} href_id="dev_misya_github" />
|
||||
</div>
|
||||
<div className={styles.dev_card_wrapper}>
|
||||
<img src={dev_shiina} className={styles.dev_card_img} />
|
||||
<OpenLinkContainer className={styles.dev_shiina_x} href_id="dev_shiina_x" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.project_links_and_logo_section}>
|
||||
<div className={styles.about_vrct_logo_wrapper}>
|
||||
<img src={vrct_logo_for_about_vrct} className={styles.about_vrct_logo} />
|
||||
</div>
|
||||
<div className={styles.project_links_wrapper}>
|
||||
<OpenLinkContainer className={styles.project_link} href_id="project_link_booth" />
|
||||
<OpenLinkContainer className={styles.project_link} href_id="project_link_documents" />
|
||||
<OpenLinkContainer className={styles.project_link} href_id="project_link_vrct_github" />
|
||||
<OpenLinkContainer className={styles.project_link} href_id="project_link_contact_us" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.contributors_section}>
|
||||
<img src={contributors_section_title} className={clsx(styles.section_title, styles.contributors)} />
|
||||
<div className={styles.contributors_img_wrapper}>
|
||||
<div className={styles.contributor_card_wrapper}>
|
||||
<img src={contributor_done} className={clsx(styles.contributors_img, styles.contributors)} />
|
||||
<OpenLinkContainer className={styles.contributors_done_san_x} href_id="contributors_done_san_x" />
|
||||
</div>
|
||||
<div className={styles.contributor_card_wrapper}>
|
||||
<img src={contributor_iya} className={clsx(styles.contributors_img, styles.contributors)} />
|
||||
<OpenLinkContainer className={styles.contributors_iya_x} href_id="contributors_iya_x" />
|
||||
</div>
|
||||
<div className={styles.contributor_card_wrapper}>
|
||||
<img src={contributor_rera} className={clsx(styles.contributors_img, styles.contributors)} />
|
||||
<OpenLinkContainer className={styles.contributors_rera_x} href_id="contributors_rera_x" />
|
||||
<OpenLinkContainer className={styles.contributors_rera_github} href_id="contributors_rera_github" />
|
||||
</div>
|
||||
<div className={styles.contributor_card_wrapper}>
|
||||
<img src={contributor_poposuke} className={clsx(styles.contributors_img, styles.contributors)} />
|
||||
<OpenLinkContainer className={styles.contributors_poposuke_x} href_id="contributors_poposuke_x" />
|
||||
</div>
|
||||
<div className={styles.contributor_card_wrapper}>
|
||||
<img src={contributor_kumaguma} className={clsx(styles.contributors_img, styles.contributors)} />
|
||||
<OpenLinkContainer className={styles.contributors_kumaguma_x} href_id="contributors_kumaguma_x" />
|
||||
</div>
|
||||
<div className={styles.contributor_card_wrapper}>
|
||||
<img src={contributor_riku} className={clsx(styles.contributors_img, styles.contributors)} />
|
||||
<OpenLinkContainer className={styles.contributors_riku_x} href_id="contributors_riku_x" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.localization_section}>
|
||||
<img src={localization_section_title} className={clsx(styles.section_title, styles.localization)} />
|
||||
<div className={styles.localization_members_wrapper}>
|
||||
<div className={styles.localization_members_row_wrapper}>
|
||||
<img src={localization_1} className={styles.localization_members_img} />
|
||||
<img src={localization_2} className={styles.localization_members_img} />
|
||||
</div>
|
||||
<div className={styles.localization_members_row_wrapper}>
|
||||
<img src={localization_3} className={styles.localization_members_img} />
|
||||
<img src={localization_4} className={styles.localization_members_img} />
|
||||
</div>
|
||||
<div className={styles.localization_members_row_wrapper}>
|
||||
<img src={localization_5} className={styles.localization_members_img} />
|
||||
{/* <img src={localization_6} className={styles.localization_members_img} /> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.special_thanks_section}>
|
||||
<img src={special_thanks_section_title} className={clsx(styles.section_title, styles.special_thanks)} />
|
||||
<img src={special_thanks_members} className={styles.special_thanks_members_img} />
|
||||
{
|
||||
currentUiLanguage.data === "ja"
|
||||
? <img src={special_thanks_message_ja} className={styles.special_thanks_message_img} />
|
||||
: <img src={special_thanks_message_en} className={styles.special_thanks_message_img} />
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
<div className={styles.poster_showcase_section}>
|
||||
<img src={poster_showcase_section_title} className={clsx(styles.section_title, styles.poster_showcase)} />
|
||||
<PosterShowcaseContents />
|
||||
</div>
|
||||
|
||||
<div className={styles.vrchat_disclaimer_section}>
|
||||
<p className={styles.vrchat_disclaimer}>VRCT is not endorsed by VRChat and does not reflect the views or opinions of VRChat or anyone officially involved in producing or managing VRChat properties. VRChat and all associated properties are trademarks or registered trademarks of VRChat Inc. VRChat © VRChat Inc.</p>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
import dev_x_icon from "@images/about_vrct/dev_x_icon.png";
|
||||
import dev_github_icon from "@images/about_vrct/dev_github_icon.png";
|
||||
import contributors_x_icon from "@images/about_vrct/contributors_x_icon.png";
|
||||
import contributors_github_icon from "@images/about_vrct/contributors_github_icon.png";
|
||||
|
||||
import project_link_booth from "@images/about_vrct/project_link_booth.png";
|
||||
import project_link_documents from "@images/about_vrct/project_link_documents.png";
|
||||
import project_link_vrct_github from "@images/about_vrct/project_link_vrct_github.png";
|
||||
import project_link_contact_us from "@images/about_vrct/project_link_contact_us.png";
|
||||
|
||||
const about_vrct_links = {
|
||||
dev_misya_x: { img: dev_x_icon, href: "https://twitter.com/misya_ai" },
|
||||
dev_misya_github: { img: dev_github_icon, href: "https://github.com/misyaguziya" },
|
||||
dev_shiina_x: { img: dev_x_icon, href: "https://twitter.com/Shiina_12siy" },
|
||||
|
||||
project_link_booth: { img: project_link_booth, href: "https://misyaguziya.booth.pm/items/5155325" },
|
||||
project_link_documents: { img: project_link_documents, href: "https://mzsoftware.notion.site/VRCT-Documents-be79b7a165f64442ad8f326d86c22246" },
|
||||
project_link_vrct_github: { img: project_link_vrct_github, href: "https://github.com/misyaguziya/VRCT" },
|
||||
project_link_contact_us: { img: project_link_contact_us, href: "https://docs.google.com/forms/d/e/1FAIpQLSei-xoydOY60ivXqhOjaTzNN8PiBQIDcNhzfy6cw2sjYkcg_g/viewform" },
|
||||
|
||||
contributors_done_san_x: { img: contributors_x_icon, href: "https://twitter.com/done_vrc" },
|
||||
contributors_iya_x: { img: contributors_x_icon, href: "https://twitter.com/IYAA_HHHH" },
|
||||
contributors_rera_x: { img: contributors_x_icon, href: "https://twitter.com/rerassi" },
|
||||
contributors_rera_github: { img: contributors_github_icon, href: "https://github.com/soumt-r" },
|
||||
contributors_poposuke_x: { img: contributors_x_icon, href: "https://twitter.com/sig_popo" },
|
||||
contributors_kumaguma_x: { img: contributors_x_icon, href: "https://twitter.com/K_kumaguma_A" },
|
||||
contributors_riku_x: { img: contributors_x_icon, href: "https://twitter.com/Riku7302" },
|
||||
};
|
||||
|
||||
const OpenLinkContainer = ({className, href_id}) => {
|
||||
const href = about_vrct_links[href_id].href;
|
||||
const img = about_vrct_links[href_id].img;
|
||||
return (
|
||||
<a className={className} href={href} target="_blank" rel="noreferrer" >
|
||||
{/* for adjust size to their parent component's width. */}
|
||||
<img style={ {height: "100%", width: "100%", "objectFit": "contain" }} src={img} />
|
||||
</a>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,215 @@
|
||||
.container {
|
||||
display: flex;
|
||||
gap: 2.2rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
max-width: 72rem;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.section_title {
|
||||
height: 1.2rem;
|
||||
object-fit: contain;
|
||||
object-position: left;
|
||||
&.the_developers {
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
&.contributors {
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
&.special_thanks {
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
&.poster_showcase {
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dev_section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.dev_section_wrapper {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
.dev_card_wrapper {
|
||||
position: relative;
|
||||
// width: 100%;
|
||||
}
|
||||
.dev_card_img {
|
||||
width: 34.6rem;
|
||||
}
|
||||
|
||||
@mixin dev_sns_styles($right) {
|
||||
position: absolute;
|
||||
right: $right;
|
||||
bottom: 0.6rem;
|
||||
width: 2.8rem;
|
||||
padding: 0.4rem;
|
||||
border-radius: 0.4rem;
|
||||
&:hover {
|
||||
background-color: var(--dark_800_color);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--dark_900_color)
|
||||
}
|
||||
}
|
||||
.dev_misya_x {
|
||||
@include dev_sns_styles(6rem);
|
||||
}
|
||||
.dev_misya_github {
|
||||
@include dev_sns_styles(3rem);
|
||||
}
|
||||
.dev_shiina_x {
|
||||
@include dev_sns_styles(2.4rem);
|
||||
}
|
||||
|
||||
|
||||
.project_links_and_logo_section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 0 5.5rem;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
.about_vrct_logo_wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
.about_vrct_logo {
|
||||
width: 20rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
.project_links_wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.2rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.project_link {
|
||||
height: 2.6rem;
|
||||
padding: 0.4rem 1rem;
|
||||
border-radius: 0.4rem;
|
||||
flex-shrink: 0;
|
||||
&:hover {
|
||||
background-color: var(--dark_850_color);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--dark_900_color)
|
||||
}
|
||||
}
|
||||
|
||||
.contributors_img_wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.contributor_card_wrapper {
|
||||
position: relative;
|
||||
}
|
||||
.contributors_img {
|
||||
width: 22rem;
|
||||
}
|
||||
|
||||
@mixin contributors_sns_styles($bottom, $left) {
|
||||
position: absolute;
|
||||
left: $left;
|
||||
bottom: $bottom;
|
||||
width: 2.4rem;
|
||||
padding: 0.4rem;
|
||||
border-radius: 0.4rem;
|
||||
transform: translate(50%, 50%);
|
||||
&:hover {
|
||||
background-color: var(--dark_825_color);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--dark_888_color)
|
||||
}
|
||||
}
|
||||
|
||||
$bottom_pos: 16%;
|
||||
$sns_left_pos: 0.8rem;
|
||||
.contributors_done_san_x {
|
||||
@include contributors_sns_styles($bottom_pos, $sns_left_pos);
|
||||
}
|
||||
.contributors_iya_x {
|
||||
@include contributors_sns_styles($bottom_pos, $sns_left_pos);
|
||||
}
|
||||
.contributors_rera_x {
|
||||
@include contributors_sns_styles($bottom_pos, calc($sns_left_pos - 1.4rem));
|
||||
}
|
||||
.contributors_rera_github {
|
||||
@include contributors_sns_styles($bottom_pos, calc($sns_left_pos + 1.4rem));
|
||||
}
|
||||
|
||||
.contributors_poposuke_x {
|
||||
@include contributors_sns_styles($bottom_pos, $sns_left_pos);
|
||||
}
|
||||
.contributors_kumaguma_x {
|
||||
@include contributors_sns_styles($bottom_pos, $sns_left_pos);
|
||||
}
|
||||
.contributors_riku_x {
|
||||
@include contributors_sns_styles($bottom_pos, $sns_left_pos);
|
||||
}
|
||||
|
||||
.localization_section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.localization_members_wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: start;
|
||||
column-gap: 6rem;
|
||||
row-gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.localization_members_row_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: start;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
.localization_members_img {
|
||||
height: 2.2rem;
|
||||
}
|
||||
|
||||
.special_thanks_section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.special_thanks_members_img {
|
||||
width: 100%;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
.special_thanks_message_img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.poster_showcase_section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vrchat_disclaimer {
|
||||
font-size: 1.2rem;
|
||||
margin-top: 8rem;
|
||||
color: var(--dark_600_color);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: start;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import clsx from "clsx";
|
||||
import styles from "./PosterShowcaseWorldsContents.module.scss";
|
||||
import { useStore_PosterShowcaseWorldPageIndex } from "@store";
|
||||
const images = import.meta.glob("@images/about_vrct/showcased_worlds/*.{png,jpg,jpeg,svg}", { eager: true });
|
||||
|
||||
const getImageByFileName = (file_name) => {
|
||||
const imagePath = Object.keys(images).find((path) => path.endsWith(file_name + ".png"));
|
||||
return imagePath ? images[imagePath]?.default : null;
|
||||
};
|
||||
|
||||
import poster_showcase_worlds_settings from "./poster_showcase_worlds_settings";
|
||||
import { chunkArray } from "@utils";
|
||||
|
||||
export const PosterShowcaseWorldsContents = () => {
|
||||
const { currentPosterShowcaseWorldPageIndex } = useStore_PosterShowcaseWorldPageIndex();
|
||||
const poster_showcase_world_images = poster_showcase_worlds_settings.map((setting) => ({
|
||||
img: getImageByFileName(setting.image_file_name),
|
||||
x_post_num: setting.x_post_num
|
||||
}));
|
||||
|
||||
const chunked_poster_showcase_world_images = chunkArray(poster_showcase_world_images, 8);
|
||||
const target_poster_showcase_world_images = chunked_poster_showcase_world_images[currentPosterShowcaseWorldPageIndex.data];
|
||||
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.poster_showcase_world_container}>
|
||||
{target_poster_showcase_world_images.map((poster, index) => {
|
||||
const class_names = clsx(styles.poster_showcase_world_wrapper, {
|
||||
[styles.clickable]: (poster.x_post_num !== null)
|
||||
});
|
||||
|
||||
const content = (
|
||||
<div className={styles.poster_showcase_world_img} >
|
||||
<img style={ {height: "100%", width: "100%", "objectFit": "contain" }} src={poster.img} />
|
||||
</div>
|
||||
);
|
||||
if (poster.x_post_num !== null) {
|
||||
return (
|
||||
<a href={`https://x.com/Shiina_12siy/status/${poster.x_post_num}`} target="_blank" rel="noreferrer" className={class_names} key={index}>
|
||||
{content}
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className={class_names} key={index}>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
<PosterShowcaseWorldsPagination page_length={chunked_poster_showcase_world_images.length}/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
import chat_white_square from "@images/chato_white_square.png";
|
||||
import { useEffect } from "react";
|
||||
import { randomIntMinMax } from "@utils";
|
||||
const PosterShowcaseWorldsPagination = ({ page_length }) => {
|
||||
const { currentPosterShowcaseWorldPageIndex, updatePosterShowcaseWorldPageIndex } = useStore_PosterShowcaseWorldPageIndex();
|
||||
|
||||
useEffect(() => {
|
||||
updatePosterShowcaseWorldPageIndex(randomIntMinMax(page_length -1));
|
||||
},[page_length]);
|
||||
|
||||
const setPage = (index) => {
|
||||
updatePosterShowcaseWorldPageIndex(index);
|
||||
};
|
||||
|
||||
const getClassNames = (index, baseClass) => clsx(baseClass, {
|
||||
[styles.is_active]: (currentPosterShowcaseWorldPageIndex.data === index),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.pagination_container}>
|
||||
{[...Array(page_length).keys()].map((index) => {
|
||||
return (
|
||||
<div key={index} className={getClassNames(index, styles.pagination_box)} onClick={() => setPage(index)}>
|
||||
<div className={styles.chato_box}>
|
||||
<img src={chat_white_square} className={getClassNames(index, styles.pagination_chato_img)}/>
|
||||
</div>
|
||||
<div className={styles.indicator_box}>
|
||||
<div className={getClassNames(index, styles.indicator)}></div>
|
||||
<p className={getClassNames(index, styles.pagination_num)}>{index + 1}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// flex: 1;
|
||||
gap: 1rem;
|
||||
justify-content: space-between;
|
||||
width: 42rem;
|
||||
}
|
||||
$image_height: 2.8rem;
|
||||
$y_padding: 0.4rem;
|
||||
$image_height_gap: 0.4rem;
|
||||
.poster_showcase_world_container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: $image_height_gap;
|
||||
height: calc( (($image_height + ($y_padding*2)) * 8) + ($image_height_gap * (8 - 1)) );
|
||||
}
|
||||
.poster_showcase_world_wrapper {
|
||||
display: flex;
|
||||
padding: $y_padding 0.6rem $y_padding 0.8rem;
|
||||
border-radius: 0.4rem 0 0 0.4rem;
|
||||
&.clickable {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: var(--dark_850_color);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--dark_925_color);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.poster_showcase_world_img {
|
||||
height: $image_height;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.pagination_container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 6%;
|
||||
margin: 0 2.6rem;
|
||||
}
|
||||
|
||||
$animation_duration: .1s;
|
||||
.pagination_box {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
&:active .pagination_chato_img {
|
||||
animation: tremble_animation $animation_duration ease-out;
|
||||
}
|
||||
&:active.is_active .pagination_chato_img {
|
||||
transform: translate(-50%, -50%) rotate(-22deg);
|
||||
}
|
||||
&.is_active .pagination_chato_img {
|
||||
top: 48%;
|
||||
animation: rotate_animation $animation_duration ease-out;
|
||||
}
|
||||
&:hover {
|
||||
& .pagination_chato_img {
|
||||
top: 108%;
|
||||
}
|
||||
&.is_active .pagination_chato_img {
|
||||
animation: tremble_animation $animation_duration ease-out;
|
||||
}
|
||||
& .pagination_num {
|
||||
color: var(--dark_400_color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.indicator_box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
width: 100%;
|
||||
height: 0.2rem;
|
||||
background-color: var(--dark_825_color);
|
||||
&.is_active {
|
||||
background-color: var(--primary_400_color);
|
||||
}
|
||||
}
|
||||
|
||||
.pagination_num {
|
||||
font-size: 1.8rem;
|
||||
padding: 1rem;
|
||||
color: var(--dark_600_color);
|
||||
&.is_active {
|
||||
color: var(--primary_300_color);
|
||||
}
|
||||
}
|
||||
|
||||
.chato_box {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 6rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pagination_chato_img {
|
||||
position: absolute;
|
||||
top: 200%;
|
||||
left: 51%;
|
||||
transform: translate(-50%, -50%) rotate(22deg);
|
||||
width: 2.8rem;
|
||||
transition: top $animation_duration ease-out;
|
||||
}
|
||||
@keyframes rotate_animation {
|
||||
0% {
|
||||
transform: translate(-50%, -50%) rotate(22deg);
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%, -50%) rotate(360deg + 22deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes tremble_animation {
|
||||
0% { left: 51%; }
|
||||
25% { left: 55%; }
|
||||
50% { left: 45%; }
|
||||
75% { left: 48%; }
|
||||
100% { left: 51%; }
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
const poster_showcase_worlds_settings = [
|
||||
// トサカひよ
|
||||
{ image_file_name: "kokekkopiyopiyo", x_post_num: "1779076974369276014" },
|
||||
|
||||
// MiuJepang
|
||||
{ image_file_name: "ippaidou", x_post_num: "1787801976354513319" },
|
||||
{ image_file_name: "nihongokurabu", x_post_num: "1779004631936614893" },
|
||||
{ image_file_name: "language_exchange_tervern", x_post_num: "1779749425923150317" },
|
||||
{ image_file_name: "japanese_culture_osenbeito", x_post_num: "1788522972409721137" },
|
||||
{ image_file_name: "silakan_datang_ke_rumahku", x_post_num: "1788522607631056941" },
|
||||
{ image_file_name: "uj_club", x_post_num: "1780791654196388201" },
|
||||
{ image_file_name: "sushi_stand_guruguru", x_post_num: "1788523302404952218" },
|
||||
{ image_file_name: "sushi_guru_annex", x_post_num: "1825426749770932457" },
|
||||
{ image_file_name: "una_yosh", x_post_num: "1820329216598311065" },
|
||||
{ image_file_name: "cam", x_post_num: "1825427064985686138" },
|
||||
{ image_file_name: "language_exchange_park", x_post_num: "1825806455322324993" },
|
||||
|
||||
// poposuke_sig
|
||||
{ image_file_name: "usanezumi_shrine2", x_post_num: "1781224020383506649" },
|
||||
|
||||
// KUROINU_YOUHEI
|
||||
{ image_file_name: "kuroinu_work_room", x_post_num: "1779750007564112146" },
|
||||
|
||||
// いちや_ICHIYA
|
||||
{ image_file_name: "ehon_no_heikousekai", x_post_num: "1843891361478783425" },
|
||||
{ image_file_name: "ehon_no_heikousekai_1st_anniv", x_post_num: "1842088383746875535" },
|
||||
{ image_file_name: "ehon_no_heikousekai_jimusho", x_post_num: "1780792306976850285" },
|
||||
{ image_file_name: "ikoiba", x_post_num: "1782723006923780580" },
|
||||
{ image_file_name: "kimodameshi", x_post_num: "1781224391692714133" },
|
||||
{ image_file_name: "parallel_collar", x_post_num: "1820693442105934068" },
|
||||
{ image_file_name: "yoru_color", x_post_num: "1842088701599396075" },
|
||||
|
||||
// HayaTikaze
|
||||
{ image_file_name: "study_japanese_world_japanichijou", x_post_num: "1781539871829766550" },
|
||||
|
||||
// aji_3
|
||||
{ image_file_name: "yuttari_eikaiwa", x_post_num: "1779002892999078046" },
|
||||
|
||||
// 八葉そるち
|
||||
{ image_file_name: "re_yatuha_room", x_post_num: "1779830390435590196" },
|
||||
|
||||
// chakamoto
|
||||
{ image_file_name: "chakachaka_multipurpose_room", x_post_num: "1818107831289295065" },
|
||||
|
||||
// MloYolM (よるむ)
|
||||
{ image_file_name: "cafe_cian", x_post_num: "1787802552907739504" },
|
||||
|
||||
// ミラクル・オルカ
|
||||
{ image_file_name: "mamehinata_dogrun", x_post_num: "1782723423179100471" },
|
||||
|
||||
// いんく(Eenkoo)
|
||||
{ image_file_name: "tyuuniti_kouryuukai", x_post_num: "1818109101731422617" },
|
||||
|
||||
// 1ban_meno
|
||||
{ image_file_name: "bar_asagao", x_post_num: "1788523857642758370" },
|
||||
|
||||
// 沈黙静寂
|
||||
{ image_file_name: "monogatari_meetup", x_post_num: "1781538415789674976" },
|
||||
|
||||
// tommie_500
|
||||
{ image_file_name: "stretch_club_starting_from_minus", x_post_num: "1825048889550102597" },
|
||||
|
||||
// MiMi_Sorahana # VRC日韓交流会 (KRJPEX.1355)
|
||||
{ image_file_name: "kr_jp_exchange", x_post_num: "1820328755950473668" },
|
||||
|
||||
// Ein(アイン)
|
||||
{ image_file_name: "smokerz_guild_v2", x_post_num: "1825049450190127187" },
|
||||
|
||||
// KokiM1018
|
||||
{ image_file_name: "poker_room_elysion", x_post_num: "1818880695344980208" },
|
||||
|
||||
// NEET ENGINEER
|
||||
{ image_file_name: "japan_street", x_post_num: "1818881593114861924" },
|
||||
|
||||
// RIKU_VR
|
||||
{ image_file_name: "celestial_blooms", x_post_num: "1820694531568001061" },
|
||||
|
||||
// ღKAEDEಇ
|
||||
{ image_file_name: "omoshiro_kotoba_asobi_game", x_post_num: "1825806909343199700" },
|
||||
{ image_file_name: "chill_sleep_room_03", x_post_num: "1842741645231677506" },
|
||||
{ image_file_name: "chill_sleep_room_04", x_post_num: "1842742135042555906" },
|
||||
|
||||
// アスタルテア
|
||||
{ image_file_name: "oto_no_shitatei", x_post_num: "1831575615520305619" },
|
||||
|
||||
// Sayascape
|
||||
{ image_file_name: "sayasuke_hotel", x_post_num: "1843537673224630740" },
|
||||
|
||||
// ふながし
|
||||
{ image_file_name: "su", x_post_num: "1843537207401058558" },
|
||||
|
||||
// さや-sayasoft
|
||||
{ image_file_name: "saya_town", x_post_num: null },
|
||||
];
|
||||
export default poster_showcase_worlds_settings;
|
||||
@@ -0,0 +1,69 @@
|
||||
import clsx from "clsx";
|
||||
import styles from "./PostersContents.module.scss";
|
||||
import { useAppearance } from "@logics_configs";
|
||||
|
||||
import { useStore_VrctPosterIndex } from "@store";
|
||||
import ArrowLeftSvg from "@images/arrow_left.svg?react";
|
||||
|
||||
import iya_vrct_poster_ja from "@images/about_vrct/vrct_posters/iya_vrct_poster_ja.png";
|
||||
import iya_vrct_poster_en from "@images/about_vrct/vrct_posters/iya_vrct_poster_en.png";
|
||||
import iya_vrct_poster_cn from "@images/about_vrct/vrct_posters/iya_vrct_poster_cn.png";
|
||||
import iya_vrct_poster_ko from "@images/about_vrct/vrct_posters/iya_vrct_poster_ko.png";
|
||||
import iya_vrct_manga_ja from "@images/about_vrct/vrct_posters/iya_vrct_manga_ja.png";
|
||||
import iya_vrct_manga_en from "@images/about_vrct/vrct_posters/iya_vrct_manga_en.png";
|
||||
import iya_vrct_manga_ko from "@images/about_vrct/vrct_posters/iya_vrct_manga_ko.png";
|
||||
|
||||
const poster_images = [
|
||||
{ img: iya_vrct_poster_ja, poster_type: "poster" },
|
||||
{ img: iya_vrct_poster_en, poster_type: "poster" },
|
||||
{ img: iya_vrct_poster_cn, poster_type: "poster" },
|
||||
{ img: iya_vrct_poster_ko, poster_type: "poster" },
|
||||
{ img: iya_vrct_manga_ja, poster_type: "manga" },
|
||||
{ img: iya_vrct_manga_en, poster_type: "manga" },
|
||||
{ img: iya_vrct_manga_ko, poster_type: "manga" },
|
||||
];
|
||||
|
||||
import poster_images_authors_ja from "@images/about_vrct/vrct_posters/authors/poster_images_authors_ja.png";
|
||||
import poster_images_authors_en from "@images/about_vrct/vrct_posters/authors/poster_images_authors_en.png";
|
||||
import poster_images_authors_m_ja from "@images/about_vrct/vrct_posters/authors/poster_images_authors_m_ja.png";
|
||||
import poster_images_authors_m_en from "@images/about_vrct/vrct_posters/authors/poster_images_authors_m_en.png";
|
||||
|
||||
export const PostersContents = () => {
|
||||
const { currentVrctPosterIndex, updateVrctPosterIndex } = useStore_VrctPosterIndex();
|
||||
const { currentUiLanguage } = useAppearance();
|
||||
|
||||
|
||||
const updateIndex = (delta) => {
|
||||
const newIndex = (currentVrctPosterIndex.data + delta + poster_images.length) % poster_images.length;
|
||||
updateVrctPosterIndex(newIndex);
|
||||
};
|
||||
|
||||
const current_poster = poster_images[currentVrctPosterIndex.data];
|
||||
const current_poster_authors_img_ja = (current_poster.poster_type === "poster") ? poster_images_authors_ja : poster_images_authors_m_ja;
|
||||
const current_poster_authors_img_en = (current_poster.poster_type === "poster") ? poster_images_authors_en : poster_images_authors_m_en;
|
||||
|
||||
return (
|
||||
<div className={styles.poster_pagination_container}>
|
||||
<div className={styles.poster_pagination_wrapper}>
|
||||
<button
|
||||
className={clsx(styles.poster_pagination_button, styles.poster_prev)}
|
||||
onClick={() => updateIndex(-1)}
|
||||
>
|
||||
<ArrowLeftSvg className={clsx(styles.poster_pagination_svg, styles.poster_prev_svg)} />
|
||||
</button>
|
||||
<img src={current_poster.img} className={styles.poster_img} />
|
||||
<button
|
||||
className={clsx(styles.poster_pagination_button, styles.poster_next)}
|
||||
onClick={() => updateIndex(1)}
|
||||
>
|
||||
<ArrowLeftSvg className={clsx(styles.poster_pagination_svg, styles.poster_next_svg)} />
|
||||
</button>
|
||||
</div>
|
||||
{
|
||||
currentUiLanguage.data === "ja"
|
||||
? <img src={current_poster_authors_img_ja} className={styles.poster_authors_img} />
|
||||
: <img src={current_poster_authors_img_en} className={styles.poster_authors_img} />
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
.poster_pagination_container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.poster_pagination_wrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
$poster_img_width: 18rem;
|
||||
.poster_img {
|
||||
width: $poster_img_width;
|
||||
}
|
||||
|
||||
$poster_pagination_button_width: 4.6rem;
|
||||
.poster_pagination_button {
|
||||
width: $poster_pagination_button_width;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--dark_700_color);
|
||||
&:hover {
|
||||
background-color: var(--dark_900_color);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--dark_925_color);
|
||||
}
|
||||
&.poster_prev {
|
||||
border-radius: 0.8rem 0 0 0.8rem;
|
||||
}
|
||||
&.poster_next {
|
||||
border-radius: 0 0.8rem 0.8rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.poster_pagination_svg {
|
||||
width: 3.2rem;
|
||||
&.poster_next_svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.poster_authors_img {
|
||||
width: $poster_img_width + $poster_pagination_button_width;
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useI18n } from "@useI18n";
|
||||
import styles from "./AdvancedSettings.module.scss";
|
||||
|
||||
import { useOpenFolder } from "@logics_common";
|
||||
import {
|
||||
useAdvancedSettings,
|
||||
} from "@logics_configs";
|
||||
|
||||
import {
|
||||
CheckboxContainer,
|
||||
ActionButtonContainer,
|
||||
EntryWithSaveButtonContainer,
|
||||
} from "../_templates/Templates";
|
||||
|
||||
import {
|
||||
SectionLabelComponent,
|
||||
} from "../_components";
|
||||
|
||||
import OpenFolderSvg from "@images/open_folder.svg?react";
|
||||
import HelpSvg from "@images/help.svg?react";
|
||||
|
||||
export const AdvancedSettings = () => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div>
|
||||
<OscIpAddressContainer />
|
||||
<OscPortContainer />
|
||||
<OpenConfigFolderContainer />
|
||||
<OpenSwitchComputeDeviceModalContainer />
|
||||
</div>
|
||||
<WebsocketContainer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const OscIpAddressContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const { currentOscIpAddress, setOscIpAddress } = useAdvancedSettings();
|
||||
const [input_value, setInputValue] = useState(currentOscIpAddress.data);
|
||||
|
||||
const onChangeFunction = (value) => {
|
||||
setInputValue(value);
|
||||
};
|
||||
|
||||
const saveFunction = () => {
|
||||
setOscIpAddress(input_value);
|
||||
};
|
||||
|
||||
useEffect(()=> {
|
||||
if (currentOscIpAddress.state === "pending") return;
|
||||
setInputValue(currentOscIpAddress.data);
|
||||
}, [currentOscIpAddress]);
|
||||
|
||||
return (
|
||||
<EntryWithSaveButtonContainer
|
||||
label={t("config_page.advanced_settings.osc_ip_address.label")}
|
||||
variable={input_value}
|
||||
saveFunction={saveFunction}
|
||||
onChangeFunction={onChangeFunction}
|
||||
state={currentOscIpAddress.state}
|
||||
width="14rem"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const OscPortContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const { currentOscPort, setOscPort } = useAdvancedSettings();
|
||||
const [input_value, setInputValue] = useState(currentOscPort.data);
|
||||
|
||||
const onChangeFunction = (value) => {
|
||||
value = value.replace(/[^0-9]/g, "");
|
||||
setInputValue(value);
|
||||
};
|
||||
|
||||
const saveFunction = () => {
|
||||
setOscPort(input_value);
|
||||
};
|
||||
|
||||
useEffect(()=> {
|
||||
if (currentOscPort.state === "pending") return;
|
||||
setInputValue(currentOscPort.data);
|
||||
}, [currentOscPort]);
|
||||
|
||||
return (
|
||||
<EntryWithSaveButtonContainer
|
||||
label={t("config_page.advanced_settings.osc_port.label")}
|
||||
variable={input_value}
|
||||
saveFunction={saveFunction}
|
||||
onChangeFunction={onChangeFunction}
|
||||
state={currentOscPort.state}
|
||||
width="10rem"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const OpenConfigFolderContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const { openFolder_ConfigFile } = useOpenFolder();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionButtonContainer
|
||||
label={t("config_page.advanced_settings.open_config_filepath.label")}
|
||||
IconComponent={OpenFolderSvg}
|
||||
onclickFunction={openFolder_ConfigFile}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// Duplicate
|
||||
import { useStore_OpenedQuickSetting } from "@store";
|
||||
const OpenSwitchComputeDeviceModalContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const { updateOpenedQuickSetting } = useStore_OpenedQuickSetting();
|
||||
const onClickFunction = () => {
|
||||
updateOpenedQuickSetting("update_software");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionButtonContainer
|
||||
label={t("config_page.advanced_settings.switch_compute_device.label")}
|
||||
IconComponent={HelpSvg}
|
||||
onclickFunction={onClickFunction}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const WebsocketContainer = () => {
|
||||
return (
|
||||
<div>
|
||||
<SectionLabelComponent label="WebSocket" />
|
||||
<EnableWebsocketContainer />
|
||||
<WebsocketHostContainer />
|
||||
<WebsocketPortContainer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EnableWebsocketContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const { currentEnableWebsocket, toggleEnableWebsocket } = useAdvancedSettings();
|
||||
|
||||
return (
|
||||
<CheckboxContainer
|
||||
label={t("config_page.advanced_settings.enable_websocket.label")}
|
||||
variable={currentEnableWebsocket}
|
||||
toggleFunction={toggleEnableWebsocket}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const WebsocketHostContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const { currentWebsocketHost, setWebsocketHost } = useAdvancedSettings();
|
||||
const [input_value, setInputValue] = useState(currentWebsocketHost.data);
|
||||
|
||||
const onChangeFunction = (value) => {
|
||||
setInputValue(value);
|
||||
};
|
||||
|
||||
const saveFunction = () => {
|
||||
setWebsocketHost(input_value);
|
||||
};
|
||||
|
||||
useEffect(()=> {
|
||||
if (currentWebsocketHost.state === "pending") return;
|
||||
setInputValue(currentWebsocketHost.data);
|
||||
}, [currentWebsocketHost]);
|
||||
|
||||
return (
|
||||
<EntryWithSaveButtonContainer
|
||||
label={t("config_page.advanced_settings.websocket_host.label")}
|
||||
variable={input_value}
|
||||
saveFunction={saveFunction}
|
||||
onChangeFunction={onChangeFunction}
|
||||
state={currentWebsocketHost.state}
|
||||
width="14rem"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const WebsocketPortContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const { currentWebsocketPort, setWebsocketPort } = useAdvancedSettings();
|
||||
const [input_value, setInputValue] = useState(currentWebsocketPort.data);
|
||||
|
||||
const onChangeFunction = (value) => {
|
||||
value = value.replace(/[^0-9]/g, "");
|
||||
setInputValue(value);
|
||||
};
|
||||
|
||||
const saveFunction = () => {
|
||||
setWebsocketPort(input_value);
|
||||
};
|
||||
|
||||
useEffect(()=> {
|
||||
if (currentWebsocketPort.state === "pending") return;
|
||||
setInputValue(currentWebsocketPort.data);
|
||||
}, [currentWebsocketPort]);
|
||||
|
||||
return (
|
||||
<EntryWithSaveButtonContainer
|
||||
label={t("config_page.advanced_settings.websocket_port.label")}
|
||||
variable={input_value}
|
||||
saveFunction={saveFunction}
|
||||
onChangeFunction={onChangeFunction}
|
||||
state={currentWebsocketPort.state}
|
||||
width="10rem"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
.container {
|
||||
display: flex;
|
||||
gap: 6.4rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useI18n } from "@useI18n";
|
||||
import styles from "./Appearance.module.scss";
|
||||
import { ui_configs } from "@ui_configs";
|
||||
import { useStore_SelectableFontFamilyList } from "@store";
|
||||
|
||||
import {
|
||||
useWindow,
|
||||
} from "@logics_common";
|
||||
|
||||
import {
|
||||
useAppearance,
|
||||
} from "@logics_configs";
|
||||
|
||||
import {
|
||||
SliderContainer,
|
||||
DropdownMenuContainer,
|
||||
RadioButtonContainer,
|
||||
CheckboxContainer,
|
||||
} from "../_templates/Templates";
|
||||
|
||||
export const Appearance = () => {
|
||||
return (
|
||||
<>
|
||||
<UiLanguageContainer />
|
||||
<UiScalingContainer />
|
||||
<MessageLogUiScalingContainer />
|
||||
<SendMessageButtonTypeContainer />
|
||||
<ShowResendButtonContainer />
|
||||
<FontFamilyContainer />
|
||||
<TransparencyContainer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const UiLanguageContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const { currentUiLanguage, setUiLanguage } = useAppearance();
|
||||
|
||||
const is_not_en_lang = currentUiLanguage.data !== "en" && currentUiLanguage.data !== undefined;
|
||||
return (
|
||||
<RadioButtonContainer
|
||||
label={is_not_en_lang ? "UI Language" : t("config_page.appearance.ui_language.label")}
|
||||
desc={is_not_en_lang ? t("config_page.appearance.ui_language.label") : false}
|
||||
selectFunction={setUiLanguage}
|
||||
name="ui_language"
|
||||
options={ui_configs.selectable_ui_languages}
|
||||
checked_variable={currentUiLanguage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const UiScalingContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const { currentUiScaling, setUiScaling } = useAppearance();
|
||||
const { asyncUpdateBreakPoint } = useWindow();
|
||||
|
||||
const [ui_ui_scaling, setUiUiScaling] = useState(currentUiScaling.data);
|
||||
|
||||
const onchangeFunction = (value) => {
|
||||
setUiUiScaling(value);
|
||||
};
|
||||
const onchangeCommittedFunction = (value) => {
|
||||
setUiScaling(value);
|
||||
};
|
||||
useEffect(() => {
|
||||
setUiUiScaling(currentUiScaling.data);
|
||||
asyncUpdateBreakPoint();
|
||||
}, [currentUiScaling.data]);
|
||||
|
||||
// [Duplicated]
|
||||
const createMarks = (min, max) => {
|
||||
const marks = [];
|
||||
for (let value = min; value <= max; value += 10) {
|
||||
const label = ([50,70,90,110,130,150,170,190].includes(value)) ? "" : value;
|
||||
marks.push({ value, label: `${label}` });
|
||||
}
|
||||
return marks;
|
||||
};
|
||||
|
||||
const marks = createMarks(40, 200);
|
||||
|
||||
return (
|
||||
<SliderContainer
|
||||
label={t("config_page.appearance.ui_size.label") + " (%)"}
|
||||
min="40"
|
||||
max="200"
|
||||
onchangeCommittedFunction={onchangeCommittedFunction}
|
||||
onchangeFunction={onchangeFunction}
|
||||
variable={ui_ui_scaling}
|
||||
marks={marks}
|
||||
step={null}
|
||||
track={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export const MessageLogUiScalingContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const { currentMessageLogUiScaling, setMessageLogUiScaling } = useAppearance();
|
||||
const [ui_message_log_ui_scaling, setUiMessageLogUiScaling] = useState(currentMessageLogUiScaling.data);
|
||||
|
||||
const onchangeFunction = (value) => {
|
||||
setUiMessageLogUiScaling(value);
|
||||
};
|
||||
const onchangeCommittedFunction = (value) => {
|
||||
setMessageLogUiScaling(value);
|
||||
};
|
||||
useEffect(() => {
|
||||
setUiMessageLogUiScaling(currentMessageLogUiScaling.data);
|
||||
}, [currentMessageLogUiScaling.data]);
|
||||
|
||||
// [Duplicated]
|
||||
const createMarks = (min, max) => {
|
||||
const marks = [];
|
||||
for (let value = min; value <= max; value += 10) {
|
||||
const label = ([50,70,90,110,130,150,170,190].includes(value)) ? "" : value;
|
||||
marks.push({ value, label: `${label}` });
|
||||
}
|
||||
return marks;
|
||||
};
|
||||
|
||||
const marks = createMarks(40, 200);
|
||||
|
||||
return (
|
||||
<SliderContainer
|
||||
label={t("config_page.appearance.textbox_ui_size.label") + " (%)"}
|
||||
min="40"
|
||||
max="200"
|
||||
onchangeCommittedFunction={onchangeCommittedFunction}
|
||||
onchangeFunction={onchangeFunction}
|
||||
variable={ui_message_log_ui_scaling}
|
||||
marks={marks}
|
||||
step={null}
|
||||
track={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const SendMessageButtonTypeContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const { currentSendMessageButtonType, setSendMessageButtonType } = useAppearance();
|
||||
|
||||
return (
|
||||
<RadioButtonContainer
|
||||
label={t("config_page.appearance.send_message_button_type.label")}
|
||||
selectFunction={setSendMessageButtonType}
|
||||
name="send_message_button_type"
|
||||
options={[
|
||||
{ id: "hide", label: t("config_page.appearance.send_message_button_type.hide") },
|
||||
{ id: "show", label: t("config_page.appearance.send_message_button_type.show") },
|
||||
{ id: "show_and_disable_enter_key", label: t("config_page.appearance.send_message_button_type.show_and_disable_enter_key") },
|
||||
]}
|
||||
checked_variable={currentSendMessageButtonType}
|
||||
column={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ShowResendButtonContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const { currentShowResendButton, toggleShowResendButton } = useAppearance();
|
||||
|
||||
return (
|
||||
<CheckboxContainer
|
||||
label={t("config_page.appearance.show_resend_button.label")}
|
||||
desc={t("config_page.appearance.show_resend_button.desc")}
|
||||
variable={currentShowResendButton}
|
||||
toggleFunction={toggleShowResendButton}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const FontFamilyContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const { currentSelectedFontFamily, setSelectedFontFamily } = useAppearance();
|
||||
|
||||
const selectFunction = (selected_data) => {
|
||||
setSelectedFontFamily(selected_data.selected_id);
|
||||
};
|
||||
const { currentSelectableFontFamilyList } = useStore_SelectableFontFamilyList();
|
||||
|
||||
return (
|
||||
<DropdownMenuContainer
|
||||
dropdown_id="font_family"
|
||||
label={t("config_page.appearance.font_family.label")}
|
||||
selected_id={currentSelectedFontFamily.data}
|
||||
list={currentSelectableFontFamilyList.data}
|
||||
selectFunction={selectFunction}
|
||||
state={currentSelectedFontFamily.state}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const TransparencyContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const { currentTransparency, setTransparency } = useAppearance();
|
||||
const [ui_message_log_ui_scaling, setUiTransparency] = useState(currentTransparency.data);
|
||||
|
||||
const onchangeFunction = (value) => {
|
||||
setUiTransparency(value);
|
||||
};
|
||||
const onchangeCommittedFunction = (value) => {
|
||||
setTransparency(value);
|
||||
};
|
||||
useEffect(() => {
|
||||
setUiTransparency(currentTransparency.data);
|
||||
}, [currentTransparency.data]);
|
||||
|
||||
// [Duplicated]
|
||||
const createMarks = (min, max) => {
|
||||
const marks = [];
|
||||
for (let value = min; value <= max; value += 10) {
|
||||
marks.push({ value, label: `${value}` });
|
||||
}
|
||||
return marks;
|
||||
};
|
||||
|
||||
const marks = createMarks(40, 100);
|
||||
|
||||
return (
|
||||
<SliderContainer
|
||||
label={t("config_page.appearance.transparency.label") + " (%)"}
|
||||
min="40"
|
||||
max="100"
|
||||
onchangeCommittedFunction={onchangeCommittedFunction}
|
||||
onchangeFunction={onchangeFunction}
|
||||
variable={ui_message_log_ui_scaling}
|
||||
marks={marks}
|
||||
step={null}
|
||||
track={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
@import "@scss_mixins";
|
||||
@@ -0,0 +1,225 @@
|
||||
import { useI18n } from "@useI18n";
|
||||
import styles from "./Device.module.scss";
|
||||
import clsx from "clsx";
|
||||
import { useStore_IsBreakPoint } from "@store";
|
||||
import { ui_configs } from "@ui_configs";
|
||||
import {
|
||||
useDevice,
|
||||
} from "@logics_configs";
|
||||
|
||||
import {
|
||||
useOnMouseLeaveDropdownMenu,
|
||||
} from "../_templates/Templates";
|
||||
|
||||
import {
|
||||
LabelComponent,
|
||||
DropdownMenu,
|
||||
ThresholdComponent,
|
||||
SwitchBox,
|
||||
} from "../_components";
|
||||
|
||||
export const Device = () => {
|
||||
return (
|
||||
<>
|
||||
<Mic_Container />
|
||||
<Speaker_Container />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Mic_Container = () => {
|
||||
const { t } = useI18n();
|
||||
const {
|
||||
currentEnableAutoMicSelect,
|
||||
toggleEnableAutoMicSelect,
|
||||
currentMicDeviceList,
|
||||
currentMicHostList,
|
||||
|
||||
currentSelectedMicHost,
|
||||
setSelectedMicHost,
|
||||
currentSelectedMicDevice,
|
||||
setSelectedMicDevice,
|
||||
|
||||
currentEnableAutomaticMicThreshold,
|
||||
toggleEnableAutomaticMicThreshold,
|
||||
} = useDevice();
|
||||
const { onMouseLeaveFunction } = useOnMouseLeaveDropdownMenu();
|
||||
|
||||
const selectFunction_host = (selected_data) => {
|
||||
setSelectedMicHost(selected_data.selected_id);
|
||||
};
|
||||
|
||||
const selectFunction_device = (selected_data) => {
|
||||
setSelectedMicDevice(selected_data.selected_id);
|
||||
};
|
||||
|
||||
// [Fix me] currentEnableAutoMicSelect.data === "pending"; ? not currentEnableAutoMicSelect.state === "pending"; ??(.state)
|
||||
const is_disabled_selector = currentEnableAutoMicSelect.data === true || currentEnableAutoMicSelect.data === "pending";
|
||||
|
||||
const getLabels = () => {
|
||||
if (currentEnableAutomaticMicThreshold.data === true) {
|
||||
return {
|
||||
label: t("config_page.device.mic_dynamic_energy_threshold.label_for_automatic"),
|
||||
desc: t("config_page.device.mic_dynamic_energy_threshold.desc_for_automatic"),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
label: t("config_page.device.mic_dynamic_energy_threshold.label_for_manual"),
|
||||
desc: t("config_page.device.mic_dynamic_energy_threshold.desc_for_manual"),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const { currentIsBreakPoint } = useStore_IsBreakPoint();
|
||||
const device_container_class = clsx(styles.device_container, {
|
||||
[styles.is_break_point]: currentIsBreakPoint.data,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.mic_container}>
|
||||
<div className={device_container_class} onMouseLeave={onMouseLeaveFunction}>
|
||||
<LabelComponent label={t("config_page.device.mic_host_device.label")} />
|
||||
<div className={styles.device_contents}>
|
||||
|
||||
<div className={styles.device_auto_select_wrapper}>
|
||||
<p className={styles.device_secondary_label}>{t("config_page.device.label_auto_select")}</p>
|
||||
<SwitchBox
|
||||
variable={currentEnableAutoMicSelect}
|
||||
toggleFunction={toggleEnableAutoMicSelect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.device_dropdown_wrapper}>
|
||||
<div className={styles.device_dropdown}>
|
||||
<p className={styles.device_secondary_label}>{t("config_page.device.label_host")}</p>
|
||||
<DropdownMenu
|
||||
dropdown_id="mic_host"
|
||||
selected_id={currentSelectedMicHost.data}
|
||||
list={currentMicHostList.data}
|
||||
selectFunction={selectFunction_host}
|
||||
state={currentSelectedMicHost.state}
|
||||
style={{ maxWidth: "20rem", minWidth: "10rem" }}
|
||||
is_disabled={is_disabled_selector}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.device_dropdown}>
|
||||
<p className={styles.device_secondary_label}>{t("config_page.device.label_device")}</p>
|
||||
<DropdownMenu
|
||||
dropdown_id="mic_device"
|
||||
selected_id={currentSelectedMicDevice.data}
|
||||
list={currentMicDeviceList.data}
|
||||
selectFunction={selectFunction_device}
|
||||
state={currentSelectedMicDevice.state}
|
||||
is_disabled={is_disabled_selector}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.threshold_container}>
|
||||
<div className={styles.threshold_switch_section}>
|
||||
<LabelComponent {...getLabels()} />
|
||||
<SwitchBox
|
||||
variable={currentEnableAutomaticMicThreshold}
|
||||
toggleFunction={toggleEnableAutomaticMicThreshold}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.threshold_section}>
|
||||
<ThresholdComponent
|
||||
id="mic_threshold"
|
||||
min={ui_configs.mic_threshold_min}
|
||||
max={ui_configs.mic_threshold_max}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Speaker_Container = () => {
|
||||
const { t } = useI18n();
|
||||
const {
|
||||
currentEnableAutoSpeakerSelect,
|
||||
toggleEnableAutoSpeakerSelect,
|
||||
currentSpeakerDeviceList,
|
||||
currentSelectedSpeakerDevice,
|
||||
setSelectedSpeakerDevice,
|
||||
currentEnableAutomaticSpeakerThreshold,
|
||||
toggleEnableAutomaticSpeakerThreshold,
|
||||
} = useDevice();
|
||||
const { onMouseLeaveFunction } = useOnMouseLeaveDropdownMenu();
|
||||
|
||||
const selectFunction = (selected_data) => {
|
||||
setSelectedSpeakerDevice(selected_data.selected_id);
|
||||
};
|
||||
|
||||
const is_disabled_selector = currentEnableAutoSpeakerSelect.data === true || currentEnableAutoSpeakerSelect.data === "pending";
|
||||
|
||||
const getLabels = () => {
|
||||
if (currentEnableAutomaticSpeakerThreshold.data === true) {
|
||||
return {
|
||||
label: t("config_page.device.speaker_dynamic_energy_threshold.label_for_automatic"),
|
||||
desc: t("config_page.device.speaker_dynamic_energy_threshold.desc_for_automatic"),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
label: t("config_page.device.speaker_dynamic_energy_threshold.label_for_manual"),
|
||||
desc: t("config_page.device.speaker_dynamic_energy_threshold.desc_for_manual"),
|
||||
};
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const { currentIsBreakPoint } = useStore_IsBreakPoint();
|
||||
const device_container_class = clsx(styles.device_container, {
|
||||
[styles.is_break_point]: currentIsBreakPoint.data,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.speaker_container}>
|
||||
<div className={device_container_class} onMouseLeave={onMouseLeaveFunction}>
|
||||
<LabelComponent label={t("config_page.device.speaker_device.label")} />
|
||||
<div className={styles.device_contents}>
|
||||
|
||||
<div className={styles.device_auto_select_wrapper}>
|
||||
<p className={styles.device_secondary_label}>{t("config_page.device.label_auto_select")}</p>
|
||||
<SwitchBox
|
||||
variable={currentEnableAutoSpeakerSelect}
|
||||
toggleFunction={toggleEnableAutoSpeakerSelect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.device_dropdown}>
|
||||
<p className={styles.device_secondary_label}>{t("config_page.device.label_device")}</p>
|
||||
<DropdownMenu
|
||||
dropdown_id="speaker_device"
|
||||
label={t("config_page.device.speaker_device.label")}
|
||||
selected_id={currentSelectedSpeakerDevice.data}
|
||||
list={currentSpeakerDeviceList.data}
|
||||
selectFunction={selectFunction}
|
||||
state={currentSelectedSpeakerDevice.state}
|
||||
is_disabled={is_disabled_selector}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.threshold_container}>
|
||||
<div className={styles.threshold_switch_section}>
|
||||
<LabelComponent {...getLabels()}/>
|
||||
<SwitchBox
|
||||
variable={currentEnableAutomaticSpeakerThreshold}
|
||||
toggleFunction={toggleEnableAutomaticSpeakerThreshold}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.threshold_section}>
|
||||
<ThresholdComponent
|
||||
id="speaker_threshold"
|
||||
min={ui_configs.speaker_threshold_min}
|
||||
max={ui_configs.speaker_threshold_max}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,105 @@
|
||||
.mic_container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-bottom: solid 0.1rem var(--dark_800_color);
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.speaker_container {
|
||||
padding-top: 0rem;
|
||||
}
|
||||
|
||||
.device_container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
margin-bottom: 0rem;
|
||||
&.is_break_point {
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
align-items: start;
|
||||
& .device_contents {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
padding-left: 0rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.threshold_container {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.threshold_container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.threshold_switch_section {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.threshold_section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.device_label {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.device_contents {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: end;
|
||||
padding-left: 2rem;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.device_auto_select_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.device_dropdown_wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 2.8rem;
|
||||
}
|
||||
|
||||
.device_dropdown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
white-space: nowrap;
|
||||
max-width: 24rem;
|
||||
&.is_disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.device_secondary_label {
|
||||
padding-left: 0.2rem;
|
||||
padding-right: 0.4rem;
|
||||
font-size: 1.4rem;
|
||||
color: var(--dark_500_color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useHotkeys } from "@logics_configs";
|
||||
import styles from "./Hotkeys.module.scss";
|
||||
import { HotkeysEntryContainer } from "../_templates/Templates";
|
||||
import { useI18n } from "@useI18n";
|
||||
export const Hotkeys = () => {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<HotkeysBoxContainer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const HotkeysBoxContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const { currentHotkeys, setHotkeys } = useHotkeys();
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<HotkeysEntryContainer
|
||||
label={t("config_page.hotkeys.toggle_vrct_visibility.label")}
|
||||
hotkey_id="toggle_vrct_visibility"
|
||||
value={currentHotkeys.data}
|
||||
state={currentHotkeys.state}
|
||||
setHotkeys={setHotkeys}
|
||||
/>
|
||||
<HotkeysEntryContainer
|
||||
label={t("config_page.hotkeys.toggle_translation.label", {translation: t("main_page.translation")})}
|
||||
hotkey_id="toggle_translation"
|
||||
value={currentHotkeys.data}
|
||||
state={currentHotkeys.state}
|
||||
setHotkeys={setHotkeys}
|
||||
/>
|
||||
<HotkeysEntryContainer
|
||||
label={t("config_page.hotkeys.toggle_transcription_send.label", {transcription_send: t("main_page.transcription_send")})}
|
||||
hotkey_id="toggle_transcription_send"
|
||||
value={currentHotkeys.data}
|
||||
state={currentHotkeys.state}
|
||||
setHotkeys={setHotkeys}
|
||||
/>
|
||||
<HotkeysEntryContainer
|
||||
label={t("config_page.hotkeys.toggle_transcription_receive.label", {transcription_receive: t("main_page.transcription_receive")})}
|
||||
hotkey_id="toggle_transcription_receive"
|
||||
value={currentHotkeys.data}
|
||||
state={currentHotkeys.state}
|
||||
setHotkeys={setHotkeys}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
.container {
|
||||
display: flex;
|
||||
// gap: 6.4rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export { Device } from "./device/Device";
|
||||
export { Appearance, MessageLogUiScalingContainer } from "./appearance/Appearance";
|
||||
export { Translation } from "./translation/Translation";
|
||||
export { Transcription } from "./transcription/Transcription";
|
||||
export { Others, VrcMicMuteSyncContainer } from "./others/Others";
|
||||
export { AdvancedSettings } from "./advanced_settings/AdvancedSettings";
|
||||
export { Vr } from "./vr/Vr";
|
||||
export { Hotkeys } from "./hotkeys/Hotkeys";
|
||||
export { Plugins } from "./plugins/Plugins";
|
||||
export { AboutVrct } from "./about_vrct/AboutVrct";
|
||||
export { Supporters } from "./supporters/Supporters";
|
||||
@@ -0,0 +1,240 @@
|
||||
import { useI18n } from "@useI18n";
|
||||
import styles from "./Others.module.scss";
|
||||
|
||||
import { useOpenFolder } from "@logics_common";
|
||||
import {
|
||||
useOthers,
|
||||
} from "@logics_configs";
|
||||
|
||||
import {
|
||||
CheckboxContainer,
|
||||
MessageFormatContainer,
|
||||
} from "../_templates/Templates";
|
||||
|
||||
import {
|
||||
LabelComponent,
|
||||
ActionButton,
|
||||
SectionLabelComponent,
|
||||
} from "../_components";
|
||||
import { Checkbox } from "@common_components";
|
||||
|
||||
import OpenFolderSvg from "@images/open_folder.svg?react";
|
||||
|
||||
export const Others = () => {
|
||||
const { t } = useI18n();
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div>
|
||||
<AutoClearMessageInputBoxContainer />
|
||||
<SendOnlyTranslatedMessagesContainer />
|
||||
<AutoExportMessageLogsContainer />
|
||||
<VrcMicMuteSyncContainer />
|
||||
<SendMessageToVrcContainer />
|
||||
</div>
|
||||
<div>
|
||||
<SectionLabelComponent label={t("config_page.others.section_label_sounds")} />
|
||||
<EnableNotificationVrcSfxContainer />
|
||||
</div>
|
||||
<div>
|
||||
<SectionLabelComponent label="Speaker2Chatbox" />
|
||||
<SendReceivedMessageToVrcContainer />
|
||||
</div>
|
||||
<div>
|
||||
<SectionLabelComponent label={t("config_page.others.section_label_message_formats")} />
|
||||
<SendMessageFormatPartsContainer />
|
||||
<ReceivedMessageFormatPartsContainer />
|
||||
</div>
|
||||
<div>
|
||||
<ConvertMessageToRomajiContainer />
|
||||
<ConvertMessageToHiraganaContainer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AutoClearMessageInputBoxContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const { currentEnableAutoClearMessageInputBox, toggleEnableAutoClearMessageInputBox } = useOthers();
|
||||
|
||||
return (
|
||||
<CheckboxContainer
|
||||
label={t("config_page.others.auto_clear_the_message_box.label")}
|
||||
variable={currentEnableAutoClearMessageInputBox}
|
||||
toggleFunction={toggleEnableAutoClearMessageInputBox}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const SendOnlyTranslatedMessagesContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const { currentEnableSendOnlyTranslatedMessages, toggleEnableSendOnlyTranslatedMessages } = useOthers();
|
||||
|
||||
return (
|
||||
<CheckboxContainer
|
||||
label={t("config_page.others.send_only_translated_messages.label")}
|
||||
variable={currentEnableSendOnlyTranslatedMessages}
|
||||
toggleFunction={toggleEnableSendOnlyTranslatedMessages}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const AutoExportMessageLogsContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const { currentEnableAutoExportMessageLogs, toggleEnableAutoExportMessageLogs } = useOthers();
|
||||
const { openFolder_MessageLogs } = useOpenFolder();
|
||||
|
||||
return (
|
||||
<div className={styles.auto_export_message_logs_container}>
|
||||
<LabelComponent
|
||||
label={t("config_page.others.auto_export_message_logs.label")}
|
||||
desc={t("config_page.others.auto_export_message_logs.desc")}
|
||||
/>
|
||||
<div className={styles.auto_export_message_logs_switch_section_container}>
|
||||
<ActionButton
|
||||
IconComponent={OpenFolderSvg}
|
||||
onclickFunction={openFolder_MessageLogs}
|
||||
/>
|
||||
<Checkbox
|
||||
variable={currentEnableAutoExportMessageLogs}
|
||||
toggleFunction={toggleEnableAutoExportMessageLogs}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const VrcMicMuteSyncContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const { currentEnableVrcMicMuteSync, toggleEnableVrcMicMuteSync } = useOthers();
|
||||
|
||||
const variable = {
|
||||
state: currentEnableVrcMicMuteSync.state,
|
||||
data: currentEnableVrcMicMuteSync.data.is_enabled,
|
||||
};
|
||||
|
||||
return (
|
||||
<CheckboxContainer
|
||||
label={t("config_page.others.vrc_mic_mute_sync.label")}
|
||||
desc={t("config_page.others.vrc_mic_mute_sync.desc")}
|
||||
variable={variable}
|
||||
is_available={currentEnableVrcMicMuteSync.data.is_available}
|
||||
toggleFunction={toggleEnableVrcMicMuteSync}
|
||||
/>
|
||||
);
|
||||
};
|
||||
const SendMessageToVrcContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const { currentEnableSendMessageToVrc, toggleEnableSendMessageToVrc } = useOthers();
|
||||
|
||||
return (
|
||||
<CheckboxContainer
|
||||
label={t("config_page.others.send_message_to_vrc.label")}
|
||||
desc={t("config_page.others.send_message_to_vrc.desc")}
|
||||
variable={currentEnableSendMessageToVrc}
|
||||
toggleFunction={toggleEnableSendMessageToVrc}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const EnableNotificationVrcSfxContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const { currentEnableNotificationVrcSfx, toggleEnableNotificationVrcSfx } = useOthers();
|
||||
|
||||
return (
|
||||
<CheckboxContainer
|
||||
label={t("config_page.others.notification_vrc_sfx.label")}
|
||||
desc={t("config_page.others.notification_vrc_sfx.desc")}
|
||||
variable={currentEnableNotificationVrcSfx}
|
||||
toggleFunction={toggleEnableNotificationVrcSfx}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const SendReceivedMessageToVrcContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const { currentEnableSendReceivedMessageToVrc, toggleEnableSendReceivedMessageToVrc } = useOthers();
|
||||
|
||||
return (
|
||||
<CheckboxContainer
|
||||
label={t("config_page.others.send_received_message_to_vrc.label")}
|
||||
desc={t("config_page.others.send_received_message_to_vrc.desc")}
|
||||
variable={currentEnableSendReceivedMessageToVrc}
|
||||
toggleFunction={toggleEnableSendReceivedMessageToVrc}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const SendMessageFormatPartsContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const {
|
||||
currentSendMessageFormatParts,
|
||||
setSendMessageFormatParts,
|
||||
} = useOthers();
|
||||
|
||||
return (
|
||||
<MessageFormatContainer
|
||||
label={t("config_page.others.send_message_format.label")}
|
||||
desc={t("config_page.others.send_message_format.desc")}
|
||||
variable={currentSendMessageFormatParts}
|
||||
setFunction={setSendMessageFormatParts}
|
||||
format_id="send"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ReceivedMessageFormatPartsContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const {
|
||||
currentReceivedMessageFormatParts,
|
||||
setReceivedMessageFormatParts,
|
||||
} = useOthers();
|
||||
|
||||
return (
|
||||
<MessageFormatContainer
|
||||
label={t("config_page.others.received_message_format.label")}
|
||||
desc={t("config_page.others.received_message_format.desc")}
|
||||
variable={currentReceivedMessageFormatParts}
|
||||
setFunction={setReceivedMessageFormatParts}
|
||||
format_id="received"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ConvertMessageToRomajiContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const { currentConvertMessageToRomaji, toggleConvertMessageToRomaji } = useOthers();
|
||||
|
||||
const desc_1 = t("config_page.others.common_convert_message_hiragana_romaji.desc_1");
|
||||
const desc_2 = t("config_page.others.common_convert_message_hiragana_romaji.desc_2");
|
||||
const desc_romaji = t(
|
||||
"config_page.others.convert_message_to_romaji.desc",
|
||||
{ convert_message_to_hiragana: t("config_page.others.convert_message_to_hiragana.label") }
|
||||
);
|
||||
const desc = [desc_1, desc_2, desc_romaji].join("\n");
|
||||
|
||||
return (
|
||||
<CheckboxContainer
|
||||
label={t("config_page.others.convert_message_to_romaji.label")}
|
||||
desc={desc}
|
||||
variable={currentConvertMessageToRomaji}
|
||||
toggleFunction={toggleConvertMessageToRomaji}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ConvertMessageToHiraganaContainer = () => {
|
||||
const { t } = useI18n();
|
||||
const { currentConvertMessageToHiragana, toggleConvertMessageToHiragana } = useOthers();
|
||||
|
||||
const desc_1 = t("config_page.others.common_convert_message_hiragana_romaji.desc_1");
|
||||
const desc_2 = t("config_page.others.common_convert_message_hiragana_romaji.desc_2");
|
||||
const desc = [desc_1, desc_2].join("\n");
|
||||
|
||||
return (
|
||||
<CheckboxContainer
|
||||
label={t("config_page.others.convert_message_to_hiragana.label")}
|
||||
desc={desc}
|
||||
variable={currentConvertMessageToHiragana}
|
||||
toggleFunction={toggleConvertMessageToHiragana}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
.container {
|
||||
display: flex;
|
||||
gap: 6.4rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.auto_export_message_logs_container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
&.flex_column {
|
||||
flex-direction: column;
|
||||
}
|
||||
border-bottom: solid 0.1rem var(--dark_800_color);
|
||||
}
|
||||
|
||||
.auto_export_message_logs_switch_section_container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useI18n } from "@useI18n";
|
||||
import { usePlugins } from "@logics_configs";
|
||||
import styles from "./Plugins.module.scss";
|
||||
import { PluginsControlComponent } from "./plugins_control_component/PluginsControlComponent";
|
||||
import { useNotificationStatus } from "@logics_common";
|
||||
import { HomepageLinkButton } from "@common_components";
|
||||
|
||||
export const Plugins = () => {
|
||||
const {
|
||||
asyncFetchPluginsInfo,
|
||||
} = usePlugins();
|
||||
const hasRunRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasRunRef.current) {
|
||||
asyncFetchPluginsInfo();
|
||||
}
|
||||
return () => hasRunRef.current = true;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<PluginDownloadContainer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PluginDownloadContainer = () => {
|
||||
const { t, i18n } = useI18n();
|
||||
const {
|
||||
downloadAndExtractPlugin,
|
||||
currentPluginsData,
|
||||
currentSavedPluginsStatus,
|
||||
toggleSavedPluginsStatus,
|
||||
handlePendingPlugin,
|
||||
currentFetchedPluginsInfo,
|
||||
} = usePlugins();
|
||||
const { showNotification_Success, showNotification_Error } = useNotificationStatus();
|
||||
|
||||
// ダウンロード開始時の状態更新処理
|
||||
const downloadStartFunction = async (target_plugin_id) => {
|
||||
handlePendingPlugin(target_plugin_id, true);
|
||||
showNotification_Success(t("plugin_notifications.downloading"));
|
||||
|
||||
const target_plugin_info = currentPluginsData.data.find(
|
||||
(d) => d.plugin_id === target_plugin_id
|
||||
);
|
||||
downloadAndExtractPlugin(target_plugin_info).then(() => {
|
||||
handlePendingPlugin(target_plugin_id, false);
|
||||
showNotification_Success(t("plugin_notifications.downloaded_success"));
|
||||
}).catch(error => {
|
||||
console.error(error);
|
||||
showNotification_Error(t("plugin_notifications.downloaded_error"));
|
||||
});
|
||||
};
|
||||
|
||||
// プラグインのオンオフ切り替え処理
|
||||
const toggleFunction = (target_plugin_id) => {
|
||||
toggleSavedPluginsStatus(target_plugin_id);
|
||||
};
|
||||
|
||||
const variable_state = currentSavedPluginsStatus.state;
|
||||
|
||||
const filtered_plugins_data = currentPluginsData.data.filter(plugin => !plugin.is_outdated)
|
||||
|
||||
// plugin_id で ABC 順にソート
|
||||
const sorted_plugins_data = filtered_plugins_data.sort((a, b) =>
|
||||
a.plugin_id.localeCompare(b.plugin_id)
|
||||
);
|
||||
|
||||
// Duplicate
|
||||
const is_failed_to_fetch = currentFetchedPluginsInfo.state === "error";
|
||||
const is_fetching = currentFetchedPluginsInfo.state === "pending";
|
||||
|
||||
return (
|
||||
<div className={styles.plugins_list_container}>
|
||||
{is_failed_to_fetch && <p>Failed to fetch plugins data</p>}
|
||||
{is_fetching && <p>Fetching plugins data...</p>}
|
||||
{sorted_plugins_data.map((plugin) => {
|
||||
const target_info = plugin.is_downloaded
|
||||
? plugin.downloaded_plugin_info
|
||||
: plugin.latest_plugin_info;
|
||||
|
||||
const target_locale = target_info.locales && target_info.locales[i18n.language]
|
||||
? target_info.locales[i18n.language]
|
||||
: {
|
||||
title: target_info.title,
|
||||
desc: target_info.desc || null,
|
||||
};
|
||||
const homepage_link = plugin.latest_plugin_info?.homepage_link;
|
||||
|
||||
return (
|
||||
<div key={plugin.plugin_id} className={styles.plugin_wrapper}>
|
||||
<div className={styles.labels_wrapper}>
|
||||
<p className={styles.title}>
|
||||
{target_locale.title}
|
||||
</p>
|
||||
<p className={styles.desc}>
|
||||
{target_locale.desc}
|
||||
</p>
|
||||
{/* <p className={styles.plugin_id}>{plugin.plugin_id}</p> */}
|
||||
{homepage_link && <HomepageLinkButton homepage_link={homepage_link}/>
|
||||
}
|
||||
</div>
|
||||
<div className={styles.plugin_info_wrapper}>
|
||||
{plugin.is_error ? (
|
||||
<div>
|
||||
<p style={{ color: "red" }}>{t(`plugin_notifications.${plugin.error_message_type}`)}</p>
|
||||
<p style={{ color: "red" }}>plugin_version: {plugin.downloaded_plugin_info.plugin_version}</p>
|
||||
<p style={{ color: "red" }}>min_supported_vrct_version: {plugin.downloaded_plugin_info.min_supported_vrct_version}</p>
|
||||
<p style={{ color: "red" }}>max_supported_vrct_version: {plugin.downloaded_plugin_info.max_supported_vrct_version}</p>
|
||||
</div>
|
||||
) : (
|
||||
<PluginsControlComponent
|
||||
variable_state={variable_state}
|
||||
toggleFunction={toggleFunction}
|
||||
downloadStartFunction={downloadStartFunction}
|
||||
plugin_status={plugin}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
.container {
|
||||
display: flex;
|
||||
gap: 6.4rem;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.plugins_list_container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.plugin_wrapper {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
gap: 2rem;
|
||||
&:not(:last-child) {
|
||||
border-bottom: 0.1rem solid var(--dark_750_color);
|
||||
}
|
||||
}
|
||||
|
||||
.labels_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
max-width: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.plugin_info_wrapper {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.6rem;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.desc {
|
||||
font-size: 1.4rem;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
color: var(--dark_500_color);
|
||||
}
|
||||
// .plugin_id {
|
||||
// font-size: 1rem;
|
||||
// color: var(--dark_600_color);
|
||||
// width: 100%;
|
||||
// overflow: hidden;
|
||||
// white-space: nowrap;
|
||||
// text-overflow: ellipsis;
|
||||
// }
|
||||
@@ -0,0 +1,155 @@
|
||||
import { SwitchBox } from "../../_components";
|
||||
import { _DownloadButton } from "../../_components/_atoms/_download_button/_DownloadButton";
|
||||
import styles from "./PluginsControlComponent.module.scss";
|
||||
import { useI18n } from "@useI18n";
|
||||
|
||||
export const PluginsControlComponent = ({
|
||||
variable_state,
|
||||
plugin_status,
|
||||
toggleFunction,
|
||||
downloadStartFunction,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
|
||||
const option = {
|
||||
id: plugin_status.plugin_id,
|
||||
is_pending: plugin_status.is_pending,
|
||||
is_downloaded: plugin_status.is_downloaded,
|
||||
data: plugin_status.is_enabled,
|
||||
update_button: plugin_status.is_downloaded && plugin_status.is_latest_version_available,
|
||||
state: variable_state,
|
||||
progress: null,
|
||||
};
|
||||
|
||||
const downloaded_version = plugin_status.downloaded_plugin_info?.plugin_version;
|
||||
const latest_version = plugin_status.latest_plugin_info?.plugin_version;
|
||||
|
||||
const downloaded_version_label = t("config_page.plugins.downloaded_version",
|
||||
{ downloaded_version: downloaded_version }
|
||||
);
|
||||
const latest_version_label = t("config_page.plugins.latest_version",
|
||||
{ latest_version: latest_version }
|
||||
);
|
||||
|
||||
if (plugin_status.is_downloaded) {
|
||||
return (
|
||||
<DownloadedPluginControl
|
||||
option={option}
|
||||
plugin_status={plugin_status}
|
||||
toggleFunction={toggleFunction}
|
||||
downloadStartFunction={downloadStartFunction}
|
||||
downloaded_version_label={downloaded_version_label}
|
||||
latest_version_label={latest_version_label}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<NotDownloadedPluginControl
|
||||
option={option}
|
||||
plugin_status={plugin_status}
|
||||
downloadStartFunction={downloadStartFunction}
|
||||
downloaded_version_label={downloaded_version_label}
|
||||
latest_version_label={latest_version_label}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const DownloadedPluginControl = ({
|
||||
option,
|
||||
plugin_status,
|
||||
toggleFunction,
|
||||
downloadStartFunction,
|
||||
downloaded_version_label,
|
||||
latest_version_label,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
|
||||
const togglePlugin = () => {
|
||||
toggleFunction(plugin_status.plugin_id);
|
||||
};
|
||||
|
||||
if (!plugin_status.downloaded_plugin_info.is_plugin_supported) {
|
||||
if (plugin_status.is_latest_version_available) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<p>{downloaded_version_label}</p>
|
||||
<p>{latest_version_label}</p>
|
||||
<p>{t("config_page.plugins.available_after_updating")}</p>
|
||||
<_DownloadButton option={option} downloadStartFunction={downloadStartFunction} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<p>{t("config_page.plugins.unavailable_downloaded")}</p>
|
||||
</div>
|
||||
);
|
||||
} else if (plugin_status.is_outdated) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<p>{t("config_page.plugins.no_latest_info")}</p>
|
||||
<SwitchBox variable={option} toggleFunction={togglePlugin} />
|
||||
</div>
|
||||
);
|
||||
} else if (plugin_status.is_latest_version_already) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<p>{latest_version_label}</p>
|
||||
<p>{t("config_page.plugins.using_latest_version")}</p>
|
||||
<SwitchBox variable={option} toggleFunction={togglePlugin} />
|
||||
</div>
|
||||
);
|
||||
} else if (plugin_status.is_latest_version_available) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<p>{latest_version_label}</p>
|
||||
<p>{t("config_page.plugins.available_latest_version")}</p>
|
||||
<_DownloadButton option={option} downloadStartFunction={downloadStartFunction} />
|
||||
<SwitchBox variable={option} toggleFunction={togglePlugin} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<p>{t("config_page.plugins.available_latest_version")}</p>
|
||||
<SwitchBox variable={option} toggleFunction={togglePlugin} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const NotDownloadedPluginControl = ({
|
||||
option,
|
||||
plugin_status,
|
||||
downloadStartFunction,
|
||||
downloaded_version_label,
|
||||
latest_version_label,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
|
||||
if (plugin_status.is_latest_version_available) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<p>{latest_version_label}</p>
|
||||
<_DownloadButton option={option} downloadStartFunction={downloadStartFunction} />
|
||||
</div>
|
||||
);
|
||||
} else if (plugin_status.latest_plugin_info?.is_plugin_supported_latest_vrct) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<p>{latest_version_label}</p>
|
||||
<p>{t("config_page.plugins.available_in_latest_vrct_version")}</p>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<p>{latest_version_label}</p>
|
||||
<p>{t("config_page.plugins.unavailable_not_downloaded")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.unavailable_text {
|
||||
padding: 1rem;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import styles from "./Supporters.module.scss";
|
||||
import { SupportUsContainer } from "./support_us_container/SupportUsContainer";
|
||||
import { SupportersContainer } from "./supporters_container/SupportersContainer";
|
||||
import { useSupporters } from "@logics_configs";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export const Supporters = () => {
|
||||
const { asyncFetchSupportersData } = useSupporters();
|
||||
|
||||
useEffect(() => {
|
||||
asyncFetchSupportersData();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<SupportUsContainer />
|
||||
<div className={styles.supportersWrapper}>
|
||||
<SupportersContainer />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
.container {
|
||||
display: flex;
|
||||
gap: 3.2rem;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
.supportersWrapper {
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.8s ease-in-out 1.6s forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import top_img from "@images/supporters/patreon_1600x400px.png";
|
||||
import fanbox_logo from "@images/supporters/fanbox_logo.png";
|
||||
import kofi_logo from "@images/supporters/kofi_logo.png";
|
||||
import patreon_logo from "@images/supporters/patreon_logo.png";
|
||||
import styles from "./SupportUsContainer.module.scss";
|
||||
import clsx from "clsx";
|
||||
|
||||
export const SupportUsContainer = () => {
|
||||
return (
|
||||
<div id="support_us_container" className={styles.support_us_container}>
|
||||
<img className={styles.top_img} src={top_img} />
|
||||
<div className={styles.support_buttons_wrapper}>
|
||||
<div className={styles.support_us_button_wrapper}>
|
||||
<a className={styles.support_button} href="https://vrct-dev.fanbox.cc" target="_blank" rel="noreferrer">
|
||||
<img
|
||||
src={fanbox_logo}
|
||||
className={clsx(styles.support_img, styles.fanbox_logo)}
|
||||
/>
|
||||
<div className={styles.spiral_top}></div>
|
||||
<div className={styles.spiral_bottom}></div>
|
||||
</a>
|
||||
<a className={styles.support_button} href="https://ko-fi.com/vrct_dev" target="_blank" rel="noreferrer">
|
||||
<img
|
||||
src={kofi_logo}
|
||||
className={clsx(styles.support_img, styles.kofi_logo)}
|
||||
/>
|
||||
<div className={styles.spiral_top}></div>
|
||||
<div className={styles.spiral_bottom}></div>
|
||||
</a>
|
||||
<a className={styles.support_button} href="https://www.patreon.com/vrct_dev" target="_blank" rel="noreferrer">
|
||||
<img
|
||||
src={patreon_logo}
|
||||
className={clsx(styles.support_img, styles.patreon_logo)}
|
||||
/>
|
||||
<div className={styles.spiral_top}></div>
|
||||
<div className={styles.spiral_bottom}></div>
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.lines_container}>
|
||||
<div className={styles.line_basic}></div>
|
||||
<div className={styles.line_fuwa}></div>
|
||||
<div className={styles.line_mochi}></div>
|
||||
<div className={styles.line_mogu}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,220 @@
|
||||
$progress_ease: cubic-bezier(0, 1, 0.75, 1);
|
||||
// Duplicated
|
||||
|
||||
@keyframes revealTopImg {
|
||||
0% {
|
||||
clip-path: inset(0 50% 0 50%);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
clip-path: inset(0 0 0 0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounceIn {
|
||||
0% {
|
||||
transform: translateY(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
60% {
|
||||
transform: translateY(-10%);
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
transform: translateY(10%);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes expandWidth {
|
||||
0% {
|
||||
transform: scaleX(0);
|
||||
}
|
||||
100% {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
|
||||
.support_us_container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1.4rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.support_buttons_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
|
||||
.top_img {
|
||||
width: 100%;
|
||||
animation: revealTopImg 0.8s ease forwards;
|
||||
}
|
||||
|
||||
.lines_container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 3.6rem;
|
||||
}
|
||||
|
||||
.line_basic,
|
||||
.line_fuwa,
|
||||
.line_mochi,
|
||||
.line_mogu {
|
||||
width: 8.6rem;
|
||||
height: 0.2rem;
|
||||
transform: scaleX(0);
|
||||
transform-origin: left center;
|
||||
}
|
||||
|
||||
.line_basic {
|
||||
background-color: var(--dark_800_color);
|
||||
animation: expandWidth 1s $progress_ease 0.4s forwards;
|
||||
}
|
||||
|
||||
.line_fuwa {
|
||||
background-color: var(--supporters_color_fuwa);
|
||||
animation: expandWidth 1s $progress_ease 0.6s forwards;
|
||||
}
|
||||
|
||||
.line_mochi {
|
||||
background-color: var(--received_300_color);
|
||||
animation: expandWidth 1s $progress_ease 0.8s forwards;
|
||||
}
|
||||
|
||||
.line_mogu {
|
||||
background-color: var(--dark_basic_text_color);
|
||||
animation: expandWidth 1s $progress_ease 1s forwards;
|
||||
}
|
||||
|
||||
.support_us_button_wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
column-gap: 3.6rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.support_button {
|
||||
position: relative;
|
||||
padding: 1.2rem 1.6rem;
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.support_button:nth-child(1) {
|
||||
animation: bounceIn 0.4s ease-out 1.2s forwards;
|
||||
}
|
||||
.support_button:nth-child(2) {
|
||||
animation: bounceIn 0.4s ease-out 1.4s forwards;
|
||||
}
|
||||
.support_button:nth-child(3) {
|
||||
animation: bounceIn 0.4s ease-out 1.6s forwards;
|
||||
}
|
||||
|
||||
|
||||
.support_img {
|
||||
&.fanbox_logo {
|
||||
height: 1.8rem;
|
||||
}
|
||||
&.kofi_logo, &.patreon_logo {
|
||||
height: 2.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.spiral_top::before,
|
||||
.spiral_top::after,
|
||||
.spiral_bottom::before,
|
||||
.spiral_bottom::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
transition-duration: 0.3s;
|
||||
}
|
||||
|
||||
.spiral_top::before {
|
||||
background: var(--dark_800_color);
|
||||
box-shadow: 0 0 0.4rem 0 var(--dark_800_color);
|
||||
}
|
||||
.spiral_top::after {
|
||||
background: var(--supporters_color_fuwa);
|
||||
box-shadow: 0 0 0.4rem 0 var(--supporters_color_fuwa);
|
||||
}
|
||||
.spiral_bottom::before {
|
||||
background: var(--received_300_color);
|
||||
box-shadow: 0 0 0.4rem 0 var(--received_300_color);
|
||||
}
|
||||
.spiral_bottom::after {
|
||||
background: var(--dark_basic_text_color);
|
||||
box-shadow: 0 0 0.4rem 0 var(--dark_basic_text_color);
|
||||
}
|
||||
|
||||
.spiral_top::before {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0.1rem;
|
||||
height: 0;
|
||||
}
|
||||
.spiral_top::after {
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 0;
|
||||
height: 0.1rem;
|
||||
}
|
||||
.spiral_bottom::before {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0.1rem;
|
||||
}
|
||||
.spiral_bottom::after {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 0.1rem;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
|
||||
.support_button:hover .spiral_top::before {
|
||||
height: 100%;
|
||||
}
|
||||
.support_button:hover .spiral_top::after {
|
||||
width: 100%;
|
||||
}
|
||||
.support_button:hover .spiral_bottom::before {
|
||||
width: 100%;
|
||||
}
|
||||
.support_button:hover .spiral_bottom::after {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
.supporters_container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.vrct_supporters_title {
|
||||
height: 6rem;
|
||||
}
|
||||
|
||||
.vrct_supporters_desc {
|
||||
font-size: 1.4rem;
|
||||
text-align: start;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import styles from "./SupportersContainer.module.scss";
|
||||
import { SupportersWrapper } from "./supporters_wrapper/SupportersWrapper";
|
||||
import { useSupporters } from "@logics_configs";
|
||||
import { supporters_images_url } from "@ui_configs";
|
||||
import vrct_supporters_title from "@images/supporters/vrct_supporters_title.png";
|
||||
|
||||
export const SupportersContainer = () => {
|
||||
const { currentSupportersData } = useSupporters();
|
||||
|
||||
if (currentSupportersData.state === "error")
|
||||
return <div>Failed to retrieve data.</div>;
|
||||
|
||||
if (currentSupportersData.state === "pending" || currentSupportersData.data === null)
|
||||
return <div>Loading...</div>;
|
||||
|
||||
const supporters_settings = currentSupportersData.data.supporters_settings;
|
||||
const last_updated_local_date = new Date(supporters_settings.last_updated_utc_date).toLocaleString();
|
||||
|
||||
return (
|
||||
<div className={styles.supporters_container}>
|
||||
<div className={styles.vrct_supporters_title_wrapper}>
|
||||
<img className={styles.vrct_supporters_title} src={vrct_supporters_title}/>
|
||||
<img className={styles.calc_period} src={`${supporters_images_url}/calc_period_label.png`}/>
|
||||
<p className={styles.last_updated_local_date}>{`Last updated date: ${last_updated_local_date}`}</p>
|
||||
</div>
|
||||
<SupportersWrapper />
|
||||
<p className={styles.vrct_supporters_desc_end}>{`みなさんのおかげで、みしゃ社長は布団で寝ることを許され(in開発室) しいなは喜び庭駆け回っています!!!ふわもちもぐもぐです!ありがとうございます。これからもまだまだ進化するVRCTをどうかよろしくお願いします!\nThanks to everyone, Misha has been granted the privilege of sleeping in a proper bed (in the development room), and Shiina is so happy, running around the yard! Fuwa-mochi-mogu-mogu! Thank you so much! We hope you'll continue to support the ever-evolving VRCT!`}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
.supporters_container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.vrct_supporters_title_wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
width: 100%;
|
||||
}
|
||||
.vrct_supporters_title {
|
||||
height: 4.2rem;
|
||||
}
|
||||
.calc_period {
|
||||
height: 1.6rem;
|
||||
}
|
||||
.last_updated_local_date {
|
||||
font-size: 1.2rem;
|
||||
color: var(--dark_600_color);
|
||||
width: 100%;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.vrct_supporters_desc_end {
|
||||
font-size: 1.4rem;
|
||||
margin-top: 2rem;
|
||||
color: var(--dark_300_color);
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import clsx from "clsx";
|
||||
import Tooltip, { tooltipClasses } from '@mui/material/Tooltip';
|
||||
import ArrowLeftSvg from "@images/arrow_left.svg?react";
|
||||
import styles from "./SupportersWrapper.module.scss";
|
||||
import { shuffleArray, randomIntMinMax, randomMinMax } from "@utils";
|
||||
|
||||
import {
|
||||
useSettingBoxScrollPosition,
|
||||
useSupporters,
|
||||
} from "@logics_configs";
|
||||
|
||||
import { supporters_images_url } from "@ui_configs";
|
||||
|
||||
const SHUFFLE_INTERVAL_TIME = 20000;
|
||||
|
||||
const and_you_data = {
|
||||
supporter_id: "and_you",
|
||||
};
|
||||
|
||||
|
||||
const image_sets = {
|
||||
supporter_cards: `${supporters_images_url}/supporter_cards/`,
|
||||
chato_expressions: `${supporters_images_url}/chato_expressions/`,
|
||||
supporters_labels: `${supporters_images_url}/supporters_labels/`,
|
||||
supporters_icons: `${supporters_images_url}/supporters_icons/`,
|
||||
};
|
||||
|
||||
const getSupporterCard = (plan_name) => {
|
||||
const card_map = {
|
||||
"mogu_2000": "mogu_card",
|
||||
"mochi_1000": "mochi_card",
|
||||
"fuwa_500": "fuwa_card",
|
||||
"basic_300": "basic_card",
|
||||
};
|
||||
if (!card_map[plan_name]) return `${image_sets.supporter_cards}basic_card.png`;
|
||||
|
||||
return `${image_sets.supporter_cards}${card_map[plan_name]}.png`;
|
||||
};
|
||||
|
||||
const getChatoExpressionsPath = (file_name) => `${image_sets.chato_expressions}${file_name}.png`;
|
||||
const getSupportersLabelsPath = (file_name) => `${image_sets.supporters_labels}${file_name}.png`;
|
||||
const getSupportersIconsPath = (file_name) => `${image_sets.supporters_icons}${file_name}.png`;
|
||||
|
||||
|
||||
export const SupportersWrapper = () => {
|
||||
const { saveScrollPosition, restoreScrollPosition } = useSettingBoxScrollPosition();
|
||||
const { currentSupportersData } = useSupporters();
|
||||
|
||||
const [json_data, setJsonData] = useState();
|
||||
const [supportersData, setSupportersData] = useState([]);
|
||||
const [chatoExpressions, setChatoExpressions] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
setJsonData(currentSupportersData.data);
|
||||
}, [currentSupportersData.data]);
|
||||
|
||||
const supporters_settings = currentSupportersData.data.supporters_settings;
|
||||
const calc_support_period = supporters_settings.calc_support_period;
|
||||
const chato_ex_count = supporters_settings.chato_ex_count;
|
||||
|
||||
const recalcAndUpdateSupporters = useCallback(() => {
|
||||
if (!json_data) return;
|
||||
|
||||
const grouped_data = {
|
||||
mogu_2000: [],
|
||||
mochi_1000: [],
|
||||
fuwa_500: [],
|
||||
basic_300: [],
|
||||
former_supporter: [],
|
||||
and_you: [],
|
||||
};
|
||||
|
||||
json_data.supporters_data.forEach((supporter) => {
|
||||
const value = supporter.highest_plan_during_the_period || "former_supporter";
|
||||
if (grouped_data[value]) {
|
||||
grouped_data[value].push(supporter);
|
||||
} else {
|
||||
grouped_data["former_supporter"].push(supporter);
|
||||
}
|
||||
});
|
||||
|
||||
const newSupportersData = [
|
||||
...shuffleArray(grouped_data["mogu_2000"]),
|
||||
...shuffleArray(grouped_data["mochi_1000"]),
|
||||
...shuffleArray(grouped_data["fuwa_500"]),
|
||||
...shuffleArray(grouped_data["basic_300"]),
|
||||
...shuffleArray(grouped_data["former_supporter"]),
|
||||
and_you_data,
|
||||
];
|
||||
|
||||
setSupportersData(newSupportersData);
|
||||
|
||||
setChatoExpressions(
|
||||
newSupportersData.map(() =>
|
||||
getChatoExpressionsPath(
|
||||
`chato_expression_${randomIntMinMax(1, chato_ex_count)}`
|
||||
)
|
||||
)
|
||||
);
|
||||
}, [json_data]);
|
||||
|
||||
useEffect(() => {
|
||||
recalcAndUpdateSupporters();
|
||||
}, [json_data, recalcAndUpdateSupporters]);
|
||||
|
||||
const shuffleSupporters = useCallback(() => {
|
||||
if (!json_data) return;
|
||||
saveScrollPosition();
|
||||
recalcAndUpdateSupporters();
|
||||
setTimeout(() => restoreScrollPosition(), 0);
|
||||
}, [json_data, recalcAndUpdateSupporters, saveScrollPosition, restoreScrollPosition]);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
shuffleSupporters();
|
||||
}, SHUFFLE_INTERVAL_TIME);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [shuffleSupporters]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<ProgressBar />
|
||||
<div className={styles.supporters_wrapper}>
|
||||
<SupporterCardsComponent
|
||||
supportersData={supportersData}
|
||||
chatoExpressions={chatoExpressions}
|
||||
calc_support_period={calc_support_period}
|
||||
/>
|
||||
</div>
|
||||
<ProgressBar />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AndYouIcon = () => {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.and_you_container}>
|
||||
<div className={styles.and_you_1}></div>
|
||||
<div className={styles.and_you_2}></div>
|
||||
</div>
|
||||
<p className={styles.and_you_fanbox_link_text}>
|
||||
FANBOX Ko-fi Patreon
|
||||
</p>
|
||||
<ArrowLeftSvg className={styles.arrow_left_svg} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SupporterCardsComponent = ({ supportersData, chatoExpressions, calc_support_period }) => {
|
||||
return supportersData.map((item, index) => {
|
||||
const target_plan = item.highest_plan_during_the_period;
|
||||
|
||||
const img_src = getSupporterCard(target_plan);
|
||||
|
||||
const is_and_you = item.supporter_id === "and_you";
|
||||
|
||||
const random_delay = `${randomMinMax(0.1, 6).toFixed(1)}s`;
|
||||
|
||||
const supporter_image_wrapper_classname = clsx(
|
||||
styles.supporter_image_wrapper,
|
||||
{
|
||||
[styles.mogu_image]: target_plan === "mogu_2000",
|
||||
}
|
||||
);
|
||||
|
||||
return is_and_you ? (
|
||||
<a href="#support_us_container" key={item.supporter_id}>
|
||||
<div className={styles.supporter_image_container}>
|
||||
<div
|
||||
className={supporter_image_wrapper_classname}
|
||||
style={{ "--delay": random_delay }}
|
||||
>
|
||||
<img
|
||||
className={styles.supporter_image}
|
||||
src={img_src}
|
||||
alt="supporter"
|
||||
/>
|
||||
<SupporterLabelComponent
|
||||
target_plan={target_plan}
|
||||
item={item}
|
||||
chatoExpressions={chatoExpressions}
|
||||
/>
|
||||
<AndYouIcon />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
) : img_src ? (
|
||||
<div key={item.supporter_id} className={styles.supporter_image_container}>
|
||||
<div
|
||||
className={supporter_image_wrapper_classname}
|
||||
style={{ "--delay": random_delay }}
|
||||
>
|
||||
<img
|
||||
className={styles.supporter_image}
|
||||
src={img_src}
|
||||
alt="supporter"
|
||||
/>
|
||||
<SupporterLabelComponent
|
||||
target_plan={target_plan}
|
||||
item={item}
|
||||
chato_src={chatoExpressions[index]}
|
||||
index={index}
|
||||
/>
|
||||
</div>
|
||||
<SupporterPeriodContainer settings={item} calc_support_period={calc_support_period}/>
|
||||
</div>
|
||||
) : null;
|
||||
});
|
||||
};
|
||||
|
||||
const SupporterLabelComponent = ({ item, target_plan, chato_src }) => {
|
||||
const is_icon_plan = ["mogu_2000", "mochi_1000"].includes(
|
||||
target_plan
|
||||
);
|
||||
|
||||
const supporter_label_component_classname = clsx(
|
||||
styles.supporter_label_component,
|
||||
{
|
||||
[styles.is_icon_plan]: is_icon_plan,
|
||||
}
|
||||
);
|
||||
|
||||
const is_and_you = item.supporter_id === "and_you";
|
||||
const is_default_icon = item.supporter_icon_id === "";
|
||||
|
||||
const file_name = is_and_you ? "and_you" : `supporter_${item.supporter_id}`;
|
||||
const label_img_src = getSupportersLabelsPath(file_name);
|
||||
const icon_img_src = getSupportersIconsPath(
|
||||
`supporter_icon_${item.supporter_icon_id}`
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={supporter_label_component_classname}>
|
||||
{is_icon_plan && (
|
||||
<div className={styles.supporter_icon_wrapper}>
|
||||
{is_default_icon ? (
|
||||
<img
|
||||
className={styles.default_chato_expression_image}
|
||||
src={chato_src}
|
||||
alt="chato expression"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
className={styles.supporter_icon}
|
||||
src={icon_img_src}
|
||||
alt="supporter icon"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
className={styles.supporter_label_image}
|
||||
src={label_img_src}
|
||||
alt="supporter label"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SupporterPeriodContainer = ({ settings, calc_support_period }) => {
|
||||
const period_data = extractKeys(settings, calc_support_period);
|
||||
const offset = {
|
||||
popper: {
|
||||
sx: {
|
||||
[`&.${tooltipClasses.popper}[data-popper-placement*="top"] .${tooltipClasses.tooltip}`]: { marginBottom: "0.2em" },
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.supporter_period_container}>
|
||||
<div className={styles.supporter_period_wrapper}>
|
||||
{Object.entries(period_data).map(([key, item], index) => {
|
||||
if (item === "") return null;
|
||||
const period_box_class_name = clsx(styles.period_box, {
|
||||
[styles.mogu_bar]: item === "mogu_2000",
|
||||
[styles.mochi_bar]: item === "mochi_1000",
|
||||
[styles.fuwa_bar]: item === "fuwa_500",
|
||||
[styles.basic_bar]: item === "basic_300",
|
||||
});
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
key={index}
|
||||
title={
|
||||
<p className={styles.tooltip_period_label}>{key}</p>
|
||||
}
|
||||
placement="top"
|
||||
slotProps={offset}
|
||||
>
|
||||
<div className={styles.period_box_wrapper}>
|
||||
<div className={period_box_class_name}></div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const extractKeys = (data, keys_to_extract) => {
|
||||
const result = {};
|
||||
for (const key of keys_to_extract) {
|
||||
if (key in data) {
|
||||
result[key] = data[key];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
const ProgressBar = () => {
|
||||
const [is_active, setIsActive] = useState(false);
|
||||
useEffect(() => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
setIsActive(true);
|
||||
});
|
||||
});
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setIsActive(false);
|
||||
setTimeout(() => setIsActive(true), 50);
|
||||
}, SHUFFLE_INTERVAL_TIME);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.progress_bar, {
|
||||
[styles.progress_bar_active]: is_active,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user