Merge branch 'ui' into for_webui

This commit is contained in:
Sakamoto Shiina
2024-12-09 10:49:24 +09:00
21 changed files with 353 additions and 181 deletions

View File

@@ -1,13 +1,27 @@
import { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { useStartPython } from "@logics/useStartPython";
import {
useWindow,
} from "@logics_common";
// import React from "react";
import {
StartPythonController,
UiLanguageController,
ConfigPageCloseTriggerController,
UiSizeController,
FontFamilyController,
TransparencyController,
} from "./_app_controllers/index.js";
import { WindowTitleBar } from "./window_title_bar/WindowTitleBar";
import { MainPage } from "./main_page/MainPage";
import { ConfigPage } from "./config_page/ConfigPage";
import { SplashComponent } from "./splash_component/SplashComponent";
import { UpdatingComponent } from "./updating_component/UpdatingComponent";
import { ModalController } from "./modal_controller/ModalController";
import { SnackbarController } from "./snackbar_controller/SnackbarController";
import styles from "./App.module.scss";
import { useIsBackendReady, useIsSoftwareUpdating } from "@logics_common";
@@ -18,9 +32,9 @@ export const App = () => {
return (
<div className={styles.container}>
<StartPythonFacadeComponent />
<StartPythonController />
<UiLanguageController />
<ConfigPageCloseTrigger />
<ConfigPageCloseTriggerController />
<UiSizeController />
<FontFamilyController />
<TransparencyController />
@@ -45,160 +59,11 @@ const Contents = () => {
<ConfigPage />
<MainPage />
<ModalController />
<SnackbarController />
</div>
:
<UpdatingComponent />
}
</>
);
};
import {
useWindow,
useVolume,
useIsOpenedConfigPage,
} from "@logics_common";
import {
useUiLanguage,
useUiScaling,
useSelectedFontFamily,
useTransparency,
} from "@logics_configs";
import {
useMainFunction,
} from "@logics_main";
const StartPythonFacadeComponent = () => {
const { asyncStartPython } = useStartPython();
const hasRunRef = useRef(false);
const { asyncFetchFonts } = useAsyncFetchFonts();
useEffect(() => {
if (!hasRunRef.current) {
asyncStartPython().then(() => {
startFeedingToWatchDog();
asyncFetchFonts();
}).catch((err) => {
console.error(err);
});
}
return () => hasRunRef.current = true;
}, []);
return null;
};
const UiLanguageController = () => {
const { currentUiLanguage } = useUiLanguage();
const { i18n } = useTranslation();
useEffect(() => {
i18n.changeLanguage(currentUiLanguage.data);
}, [currentUiLanguage.data]);
return null;
};
import { useStore_MainFunctionsStateMemory } from "@store";
const ConfigPageCloseTrigger = () => {
const { currentIsOpenedConfigPage } = useIsOpenedConfigPage();
const { currentMainFunctionsStateMemory, updateMainFunctionsStateMemory} = useStore_MainFunctionsStateMemory();
const {
currentTranscriptionSendStatus,
setTranscriptionSend,
currentTranscriptionReceiveStatus,
setTranscriptionReceive,
} = useMainFunction();
const {
currentMicThresholdCheckStatus,
volumeCheckStop_Mic,
currentSpeakerThresholdCheckStatus,
volumeCheckStop_Speaker,
} = useVolume();
const memorizeLatestMainFunctionsState = () => {
updateMainFunctionsStateMemory({
transcription_send: currentTranscriptionSendStatus.data,
transcription_receive: currentTranscriptionReceiveStatus.data,
});
};
const restoreMainFunctionState = () => {
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();
if (currentTranscriptionSendStatus.data === true) setTranscriptionSend(false);
if (currentTranscriptionReceiveStatus.data === true) setTranscriptionReceive(false);
} else if (currentIsOpenedConfigPage.data === false) { // When config page is closed.
if (currentMicThresholdCheckStatus.data === true) volumeCheckStop_Mic();
if (currentSpeakerThresholdCheckStatus.data === true) volumeCheckStop_Speaker();
restoreMainFunctionState();
}
}, [currentIsOpenedConfigPage.data]);
return null;
};
import React from "react";
const UiSizeController = () => {
const { currentUiScaling } = useUiScaling();
const font_size = 62.5 * currentUiScaling.data / 100;
useEffect(() => {
document.documentElement.style.setProperty("font-size", `${font_size}%`);
}, [currentUiScaling.data]);
return null;
};
const FontFamilyController = () => {
const { currentSelectedFontFamily } = useSelectedFontFamily();
useEffect(() => {
document.documentElement.style.setProperty("--font_family", currentSelectedFontFamily.data);
}, [currentSelectedFontFamily.data]);
return null;
};
import { useStore_SelectableFontFamilyList } from "@store";
import { arrayToObject } from "@utils";
import { invoke } from "@tauri-apps/api/tauri";
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 TransparencyController = () => {
const { currentTransparency } = useTransparency();
useEffect(() => {
document.documentElement.style.setProperty("opacity", `${currentTransparency.data / 100}`);
}, [currentTransparency.data]);
return null;
};
import { useStdoutToPython } from "@logics/useStdoutToPython";
const startFeedingToWatchDog = () => {
const { asyncStdoutToPython } = useStdoutToPython();
setInterval(() => {
asyncStdoutToPython("/run/feed_watchdog");
}, 20000); // 20000ミリ秒 = 20秒
};

View File

@@ -0,0 +1,55 @@
import { useEffect } from "react";
import {
useVolume,
useIsOpenedConfigPage,
} from "@logics_common";
import {
useMainFunction,
} from "@logics_main";
import { useStore_MainFunctionsStateMemory } from "@store";
export const ConfigPageCloseTriggerController = () => {
const { currentIsOpenedConfigPage } = useIsOpenedConfigPage();
const { currentMainFunctionsStateMemory, updateMainFunctionsStateMemory} = useStore_MainFunctionsStateMemory();
const {
currentTranscriptionSendStatus,
setTranscriptionSend,
currentTranscriptionReceiveStatus,
setTranscriptionReceive,
} = useMainFunction();
const {
currentMicThresholdCheckStatus,
volumeCheckStop_Mic,
currentSpeakerThresholdCheckStatus,
volumeCheckStop_Speaker,
} = useVolume();
const memorizeLatestMainFunctionsState = () => {
updateMainFunctionsStateMemory({
transcription_send: currentTranscriptionSendStatus.data,
transcription_receive: currentTranscriptionReceiveStatus.data,
});
};
const restoreMainFunctionState = () => {
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();
if (currentTranscriptionSendStatus.data === true) setTranscriptionSend(false);
if (currentTranscriptionReceiveStatus.data === true) setTranscriptionReceive(false);
} else if (currentIsOpenedConfigPage.data === false) { // When config page is closed.
if (currentMicThresholdCheckStatus.data === true) volumeCheckStop_Mic();
if (currentSpeakerThresholdCheckStatus.data === true) volumeCheckStop_Speaker();
restoreMainFunctionState();
}
}, [currentIsOpenedConfigPage.data]);
return null;
};

View File

@@ -0,0 +1,11 @@
import { useEffect } from "react";
import { useSelectedFontFamily } from "@logics_configs";
export const FontFamilyController = () => {
const { currentSelectedFontFamily } = useSelectedFontFamily();
useEffect(() => {
document.documentElement.style.setProperty("--font_family", currentSelectedFontFamily.data);
}, [currentSelectedFontFamily.data]);
return null;
};

View File

@@ -0,0 +1,48 @@
import { invoke } from "@tauri-apps/api/tauri";
import { useEffect, useRef } from "react";
import { useStartPython } from "@logics/useStartPython";
import { useStdoutToPython } from "@logics/useStdoutToPython";
import { useStore_SelectableFontFamilyList } from "@store";
import { arrayToObject } from "@utils";
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 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秒
};

View File

@@ -0,0 +1,11 @@
import { useEffect } from "react";
import { useTransparency } from "@logics_configs";
export const TransparencyController = () => {
const { currentTransparency } = useTransparency();
useEffect(() => {
document.documentElement.style.setProperty("opacity", `${currentTransparency.data / 100}`);
}, [currentTransparency.data]);
return null;
};

View File

@@ -0,0 +1,14 @@
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useUiLanguage } from "@logics_configs";
export const UiLanguageController = () => {
const { currentUiLanguage } = useUiLanguage();
const { i18n } = useTranslation();
useEffect(() => {
i18n.changeLanguage(currentUiLanguage.data);
}, [currentUiLanguage.data]);
return null;
};

View File

@@ -0,0 +1,13 @@
import { useEffect } from "react";
import { useUiScaling } from "@logics_configs";
export const UiSizeController = () => {
const { currentUiScaling } = useUiScaling();
const font_size = 62.5 * currentUiScaling.data / 100;
useEffect(() => {
document.documentElement.style.setProperty("font-size", `${font_size}%`);
}, [currentUiScaling.data]);
return null;
};

View File

@@ -0,0 +1,6 @@
export { StartPythonController } from "./StartPythonController";
export { UiLanguageController } from "./UiLanguageController";
export { ConfigPageCloseTriggerController } from "./ConfigPageCloseTriggerController";
export { UiSizeController } from "./UiSizeController";
export { FontFamilyController } from "./FontFamilyController";
export { TransparencyController } from "./TransparencyController";

View File

@@ -1,3 +1,4 @@
import clsx from "clsx";
import React, { useRef, forwardRef, useImperativeHandle } from "react";
import styles from "./_Entry.module.scss";
@@ -9,6 +10,9 @@ const _Entry = forwardRef((props, ref) => {
inputRef.current.focus();
}
}));
const input_class_names = clsx(styles.entry_input_area, {
[styles.is_disabled]: props.is_disabled
});
return (
<div className={styles.entry_container}>
@@ -18,7 +22,7 @@ const _Entry = forwardRef((props, ref) => {
>
<input
ref={inputRef}
className={styles.entry_input_area}
className={input_class_names}
value={props.ui_variable === null ? "" : props.ui_variable}
onChange={(e) => props.onChange(e)}
/>

View File

@@ -16,4 +16,9 @@
height: 100%;
font-size: 1.4rem;
resize: none;
color: var(--dark_basic_text_color);
&.is_disabled {
color: var(--dark_500_color);
pointer-events: none;
}
}

View File

@@ -1,9 +1,11 @@
import styles from "./DeeplAuthKey.module.scss";
import { useTranslation } from "react-i18next";
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 } = useTranslation();
@@ -22,11 +24,28 @@ export const DeeplAuthKey = (props) => {
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}/>
<button className={styles.save_button} onClick={saveAuthKey}>Save</button>
<_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}>Save</p>
}
</button>
{is_editable
? null
:

View File

@@ -44,17 +44,25 @@
.save_button {
padding: 0.8rem 1.2rem;
background-color: var(--primary_600_color);
color: var(--dark_basic_text_color);
font-size: 1.4rem;
border-radius: 0.4rem;
text-align: center;
flex-shrink: 0;
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 {
color: var(--dark_basic_text_color);
font-size: 1.4rem;
}
.open_webpage_button_wrapper {

View File

@@ -98,9 +98,9 @@ const CTranslation2ComputeDevice_Box = () => {
};
const DeeplAuthKey_Box = () => {
const [input_value, seInputValue] = useState("");
const { t } = useTranslation();
const { currentDeepLAuthKey, setDeepLAuthKey, deleteDeepLAuthKey } = useDeepLAuthKey();
const [input_value, seInputValue] = useState(currentDeepLAuthKey.data);
const onChangeFunction = (value) => {
seInputValue(value);
@@ -124,6 +124,7 @@ const DeeplAuthKey_Box = () => {
{translator: t("main_page.translator")}
)}
variable={input_value}
state={currentDeepLAuthKey.state}
onChangeFunction={onChangeFunction}
saveFunction={saveFunction}
/>
@@ -169,4 +170,4 @@ const findKeyByDeviceValue = (devices, target_value) => {
}
}
return null;
};
};

View File

@@ -14,7 +14,7 @@ export const MessageContainer = () => {
const { currentUiScaling } = useUiScaling();
const [is_hovered, setIsHovered] = useState(false);
const [message_box_height_in_rem, setMessageBoxHeightInRem] = useState(10);
const FONT_SIZE_STANDARD = 10 * currentUiScaling.data / 100; // 10px = 1rem
const FONT_SIZE_STANDARD = 10 * currentUiScaling.data / 100; // 10px = 1rem
const { currentIsAppliedInitMessageBoxHeight, updateIsAppliedInitMessageBoxHeight } = useStore_IsAppliedInitMessageBoxHeight();
const container_ref = useRef(null);
@@ -23,9 +23,10 @@ export const MessageContainer = () => {
const asyncSetMessageBoxHeightInRem = async (data) => {
const minimized = await appWindow.isMinimized();
if (minimized === true) return; // don't save while the window is minimized.
if (minimized) return; // don't save while the window is minimized.
setMessageBoxHeightInRem(data);
};
const calculateMessageBoxRatioAndHeight = () => {
if (!currentIsAppliedInitMessageBoxHeight.data) {
asyncSetMessageInputBoxRatio(currentMessageInputBoxRatio.data);
@@ -33,7 +34,7 @@ export const MessageContainer = () => {
return;
}
if (log_box_ref.current && message_box_wrapper_ref.current) {
if (log_box_ref.current && message_box_wrapper_ref.current && container_ref.current) {
const container_height = container_ref.current.offsetHeight;
const container_padding_bottom = parseFloat(window.getComputedStyle(container_ref.current).paddingBottom);
const total_height = container_height - container_padding_bottom;
@@ -43,6 +44,8 @@ export const MessageContainer = () => {
asyncSetMessageInputBoxRatio(message_box_ratio);
asyncSetMessageBoxHeightInRem(convertRatioToRem(message_box_ratio));
} else {
console.warn("References not ready for calculation");
}
};
@@ -53,24 +56,26 @@ export const MessageContainer = () => {
});
useEffect(() => {
// Note: I thought the part "1.4" is message box bottom padding + (message box separator height/2)
// but it should be fixed at 1.4. Idk why, tho.
asyncSetMessageBoxHeightInRem((position / FONT_SIZE_STANDARD) - 1.4);
}, [position]);
useEffect(() => {
asyncSetMessageBoxHeightInRem(convertRatioToRem(currentMessageInputBoxRatio.data));
}, [currentMessageInputBoxRatio.data]);
const convertRatioToRem = (ratio) => {
if (!container_ref.current) return 0;
const container_height = container_ref.current.offsetHeight;
const container_padding_bottom = parseFloat(window.getComputedStyle(container_ref.current).paddingBottom);
const total_height = container_height - container_padding_bottom;
if (total_height === 0) return 0;
return ((ratio / 100) * total_height / FONT_SIZE_STANDARD);
return total_height === 0 ? 0 : ((ratio / 100) * total_height / FONT_SIZE_STANDARD);
};
useEffect(() => {
calculateMessageBoxRatioAndHeight();
updateIsAppliedInitMessageBoxHeight(true); // Ensure this happens after initial calculation
}, []);
useEffect(() => {
let resizeTimeout;
@@ -86,10 +91,6 @@ export const MessageContainer = () => {
};
}, []);
useEffect(() => {
updateIsAppliedInitMessageBoxHeight(true);
}, []);
return (
<div className={styles.container} ref={container_ref}>
<div className={styles.log_box_resize_wrapper}
@@ -98,7 +99,7 @@ export const MessageContainer = () => {
onMouseLeave={() => setIsHovered(false)}
>
<LogBox />
<MessageLogSettingsContainer to_visible_toggle_bar={is_hovered}/>
<MessageLogSettingsContainer to_visible_toggle_bar={is_hovered} />
</div>
<Separator {...separatorProps} onDragStart={calculateMessageBoxRatioAndHeight} />
<div
@@ -112,10 +113,8 @@ export const MessageContainer = () => {
);
};
const Separator = ({ onDragStart, ...props }) => {
return (
<div tabIndex={0} className={styles.separator} {...props} onDragStart={onDragStart}>
<span className={styles.separator_line}></span>
</div>
);
};
const Separator = ({ onDragStart, ...props }) => (
<div tabIndex={0} className={styles.separator} {...props} onDragStart={onDragStart}>
<span className={styles.separator_line}></span>
</div>
);

View File

@@ -0,0 +1,39 @@
import { clsx } from "clsx";
import Snackbar from "@mui/material/Snackbar";
import Slide from "@mui/material/Slide";
import styles from "./SnackbarController.module.scss";
import { useNotificationStatus } from "@logics_common";
export const SnackbarController = () => {
const { currentNotificationStatus, closeNotification } = useNotificationStatus();
const handleClose = (event, reason) => {
closeNotification(event, reason);
};
const snackbar_classname = clsx(styles.snackbar_content, {
[styles.is_success]: currentNotificationStatus.data.status === "success",
[styles.is_error]: currentNotificationStatus.data.status === "error",
});
return (
<div>
<Snackbar
open={currentNotificationStatus.data.is_open}
onClose={handleClose}
TransitionComponent={SlideTransition}
key={currentNotificationStatus.data.key}
autoHideDuration={5000}
>
<div className={snackbar_classname}>
<p className={styles.snackbar_message}>{currentNotificationStatus.data.message}</p>
</div>
</Snackbar>
</div>
);
};
const SlideTransition = (props) => {
return <Slide {...props} direction="up" />;
};

View File

@@ -0,0 +1,16 @@
.snackbar_content {
width: 100%;
height: 100%;
padding: 2rem;
color: #fff;
&.is_success {
background-color: #368777;
}
&.is_error {
background-color: #bb4448;
}
}
.snackbar_message {
font-size: 1.4rem;
}

View File

@@ -5,6 +5,7 @@ export { useWindow } from "./useWindow";
export { useIsOpenedConfigPage } from "./useIsOpenedConfigPage";
export { useIsSoftwareUpdateAvailable } from "./useIsSoftwareUpdateAvailable";
export { useIsSoftwareUpdating } from "./useIsSoftwareUpdating";
export { useNotificationStatus } from "./useNotificationStatus";
export { useOpenFolder } from "./useOpenFolder";
export { useMessage } from "./useMessage";
export { useUpdateSoftware } from "./useUpdateSoftware";

View File

@@ -0,0 +1,42 @@
import { useStore_NotificationStatus } from "@store";
export const useNotificationStatus = () => {
const { currentNotificationStatus, updateNotificationStatus } = useStore_NotificationStatus();
const generateRandomKey = () => Math.random();
const showNotification_Error = (message) => {
updateNotificationStatus({
status: "error",
is_open: true,
key: generateRandomKey(),
message: message,
});
};
const showNotification_Success = (message) => {
updateNotificationStatus({
status: "success",
is_open: true,
key: generateRandomKey(),
message: message,
});
};
const closeNotification = (event, reason) => {
if (reason === "clickaway") return;
updateNotificationStatus((prev) => ({
...prev.data,
is_open: false,
}));
};
return {
currentNotificationStatus,
updateNotificationStatus,
showNotification_Error,
showNotification_Success,
closeNotification,
};
};

View File

@@ -2,6 +2,9 @@ import { translator_status } from "@ui_configs";
import { arrayToObject } from "@utils";
import {
useNotificationStatus,
useComputeMode,
useInitProgress,
useIsBackendReady,
@@ -167,6 +170,10 @@ export const useReceiveRoutes = () => {
const { updateOscIpAddress } = useOscIpAddress();
const { updateOscPort } = useOscPort();
const { showNotification_Success, showNotification_Error } = useNotificationStatus();
const routes = {
// Common
"/run/feed_watchdog": () => {},
@@ -494,6 +501,7 @@ export const useReceiveRoutes = () => {
const error_route = error_routes[parsed_data.endpoint];
(error_route) ? error_route(parsed_data.result.data) : console.error(`Invalid endpoint: ${parsed_data.endpoint}\nresult: ${JSON.stringify(parsed_data.result)}`);
console.error(`status 400: ${JSON.stringify(parsed_data.result)}`);
showNotification_Error(parsed_data.result.message);
break;
case 348:

View File

@@ -115,6 +115,12 @@ export const { atomInstance: Atom_IsSoftwareUpdateAvailable, useHook: useStore_I
export const { atomInstance: Atom_InitProgress, useHook: useStore_InitProgress } = createAtomWithHook(0, "InitProgress");
export const { atomInstance: Atom_IsBreakPoint, useHook: useStore_IsBreakPoint } = createAtomWithHook(false, "IsBreakPoint");
export const { atomInstance: Atom_IsSoftwareUpdating, useHook: useStore_IsSoftwareUpdating } = createAtomWithHook(false, "IsSoftwareUpdating");
export const { atomInstance: Atom_NotificationStatus, useHook: useStore_NotificationStatus } = createAtomWithHook({
status: "",
is_open: false,
key: 0,
message: "",
}, "NotificationStatus");
// Main Page
// Functions