feature/IO-3702-ESPD-UI-AND-FIXES - Stage 6 (Dark Mode)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -51,3 +51,4 @@ override.tf.json
|
||||
|
||||
.terraformrc
|
||||
terraform.rc
|
||||
/.eslintcache
|
||||
|
||||
@@ -10,6 +10,7 @@ const store = new Store({
|
||||
enabled: false,
|
||||
interval: 30000,
|
||||
},
|
||||
darkMode: false,
|
||||
esApiKey: "",
|
||||
},
|
||||
app: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ConfigProvider, Layout } from "antd";
|
||||
import { FC, useEffect } from "react";
|
||||
import { ConfigProvider, Layout, theme } from "antd";
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { Provider } from "react-redux";
|
||||
import { HashRouter, Route, Routes, useNavigate } from "react-router";
|
||||
@@ -9,8 +9,11 @@ import Settings from "./components/Settings/Settings";
|
||||
import UpdateAvailable from "./components/UpdateAvailable/UpdateAvailable";
|
||||
import reduxStore from "./redux/redux-store";
|
||||
import { NotificationProvider } from "./util/notificationContext";
|
||||
import { ThemeModeContext } from "./util/themeModeContext";
|
||||
import ipcTypes from "../../util/ipcTypes.json";
|
||||
|
||||
const { darkAlgorithm, defaultAlgorithm } = theme;
|
||||
|
||||
const ScrubHistoryNavigationBridge: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -34,53 +37,112 @@ const ScrubHistoryNavigationBridge: FC = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const App: FC = () => {
|
||||
const AppShell: FC = () => {
|
||||
const { token } = theme.useToken();
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
components: {
|
||||
Card: {
|
||||
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Provider store={reduxStore}>
|
||||
<HashRouter>
|
||||
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
||||
<NotificationProvider>
|
||||
<ScrubHistoryNavigationBridge />
|
||||
<Layout
|
||||
<Provider store={reduxStore}>
|
||||
<HashRouter>
|
||||
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
||||
<NotificationProvider>
|
||||
<ScrubHistoryNavigationBridge />
|
||||
<Layout
|
||||
style={{
|
||||
height: "100vh",
|
||||
minHeight: 0,
|
||||
overflow: "hidden",
|
||||
background: token.colorBgLayout,
|
||||
}}
|
||||
>
|
||||
<Layout.Content
|
||||
style={{
|
||||
height: "100vh",
|
||||
height: "100%",
|
||||
minHeight: 0,
|
||||
overflow: "hidden",
|
||||
background: "#f0f2f5",
|
||||
overflow: "auto",
|
||||
padding: "24px",
|
||||
}}
|
||||
>
|
||||
<Layout.Content
|
||||
style={{
|
||||
height: "100%",
|
||||
minHeight: 0,
|
||||
overflow: "auto",
|
||||
padding: "24px",
|
||||
}}
|
||||
>
|
||||
<UpdateAvailable />
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Routes>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</NotificationProvider>
|
||||
</ErrorBoundary>
|
||||
</HashRouter>
|
||||
</Provider>
|
||||
</ConfigProvider>
|
||||
<UpdateAvailable />
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Routes>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</NotificationProvider>
|
||||
</ErrorBoundary>
|
||||
</HashRouter>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const App: FC = () => {
|
||||
const [isDarkMode, setIsDarkMode] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
window.electron.ipcRenderer
|
||||
.invoke(ipcTypes.toMain.settings.get, "darkMode")
|
||||
.then((value: unknown) => {
|
||||
if (!cancelled) {
|
||||
setIsDarkMode(value === true);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to load dark mode setting", error);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.dataset.theme = isDarkMode ? "dark" : "light";
|
||||
document.documentElement.style.colorScheme = isDarkMode ? "dark" : "light";
|
||||
}, [isDarkMode]);
|
||||
|
||||
const updateDarkMode = useCallback((enabled: boolean): void => {
|
||||
setIsDarkMode(enabled);
|
||||
window.electron.ipcRenderer
|
||||
.invoke(ipcTypes.toMain.settings.set, "darkMode", enabled)
|
||||
.catch((error) => {
|
||||
console.error("Failed to save dark mode setting", error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const themeModeContextValue = useMemo(
|
||||
() => ({
|
||||
isDarkMode,
|
||||
setDarkMode: updateDarkMode,
|
||||
}),
|
||||
[isDarkMode, updateDarkMode],
|
||||
);
|
||||
|
||||
const appTheme = useMemo(
|
||||
() => ({
|
||||
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
|
||||
token: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
components: {
|
||||
Card: {
|
||||
boxShadow: isDarkMode
|
||||
? "0 2px 8px rgba(0, 0, 0, 0.45)"
|
||||
: "0 2px 8px rgba(0, 0, 0, 0.1)",
|
||||
},
|
||||
},
|
||||
}),
|
||||
[isDarkMode],
|
||||
);
|
||||
|
||||
return (
|
||||
<ThemeModeContext.Provider value={themeModeContextValue}>
|
||||
<ConfigProvider theme={appTheme}>
|
||||
<AppShell />
|
||||
</ConfigProvider>
|
||||
</ThemeModeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
33
src/renderer/src/components/Settings/Settings.Appearance.tsx
Normal file
33
src/renderer/src/components/Settings/Settings.Appearance.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Card, Flex, Space, Switch, Typography } from "antd";
|
||||
import { FC } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useThemeMode } from "@renderer/util/themeModeContext";
|
||||
|
||||
const SettingsAppearance: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { isDarkMode, setDarkMode } = useThemeMode();
|
||||
|
||||
return (
|
||||
<Card title={t("settings.labels.appearance")}>
|
||||
<Flex align="center" justify="space-between" gap="large" wrap>
|
||||
<Space orientation="vertical" size={2}>
|
||||
<Typography.Text strong>
|
||||
{t("settings.labels.darkMode")}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary">
|
||||
{t("settings.labels.darkModeDescription")}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
|
||||
<Switch
|
||||
checked={isDarkMode}
|
||||
checkedChildren={t("settings.labels.dark")}
|
||||
unCheckedChildren={t("settings.labels.light")}
|
||||
onChange={setDarkMode}
|
||||
/>
|
||||
</Flex>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsAppearance;
|
||||
@@ -3,6 +3,7 @@ import { Button, Space } from "antd";
|
||||
import { ArrowLeftOutlined } from "@ant-design/icons";
|
||||
import { FC } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import SettingsAppearance from "./Settings.Appearance";
|
||||
import SettingsConfig from "./Settings.Config";
|
||||
import SettingsWatcher from "./Settings.Watcher";
|
||||
|
||||
@@ -10,20 +11,30 @@ const Settings: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Space orientation="vertical" size="large" style={{ width: "100%" }}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate("/")}
|
||||
style={{ alignSelf: "flex-start" }}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<div
|
||||
style={{
|
||||
maxWidth: "1400px",
|
||||
height: "100%",
|
||||
margin: "0 auto",
|
||||
}}
|
||||
>
|
||||
<Space orientation="vertical" size="large" style={{ width: "100%" }}>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate("/")}
|
||||
style={{ alignSelf: "flex-start" }}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<SettingsWatcher />
|
||||
<SettingsAppearance />
|
||||
|
||||
<SettingsConfig />
|
||||
</Space>
|
||||
<SettingsWatcher />
|
||||
|
||||
<SettingsConfig />
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
18
src/renderer/src/util/themeModeContext.ts
Normal file
18
src/renderer/src/util/themeModeContext.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
export type ThemeModeContextValue = {
|
||||
isDarkMode: boolean;
|
||||
setDarkMode: (enabled: boolean) => void;
|
||||
};
|
||||
|
||||
export const ThemeModeContext = createContext<ThemeModeContextValue | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
export function useThemeMode(): ThemeModeContextValue {
|
||||
const context = useContext(ThemeModeContext);
|
||||
if (!context) {
|
||||
throw new Error("useThemeMode must be used within ThemeModeContext.");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -1,83 +1,88 @@
|
||||
{
|
||||
"translation": {
|
||||
"auth": {
|
||||
"labels": {
|
||||
"welcome": "Hi {{name}}"
|
||||
},
|
||||
"login": {
|
||||
"error": "The username and password combination provided is not valid.",
|
||||
"login": "Log In",
|
||||
"resetpassword": "Reset Password"
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"actions": {
|
||||
"clear_all": "Clear All"
|
||||
},
|
||||
"labels": {
|
||||
"dashboard": "Dashboard",
|
||||
"estimate_scrub_history": "Scrub History",
|
||||
"last_processed": "Last Processed at",
|
||||
"scrub_results": "Scrub Results",
|
||||
"total_estimates": "Total Estimates"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"errorboundary": "Uh oh - we've hit an error.",
|
||||
"notificationtitle": "Error Encountered"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"settings": "Settings",
|
||||
"signout": "Sign Out"
|
||||
},
|
||||
"settings": {
|
||||
"actions": {
|
||||
"addpath": "Add path",
|
||||
"startwatcher": "Start Watcher",
|
||||
"stopwatcher": "Stop Watcher\n"
|
||||
},
|
||||
"errors": {
|
||||
"duplicatePath": "The selected directory is already used in another configuration."
|
||||
},
|
||||
"labels": {
|
||||
"actions": "Actions",
|
||||
"addPaintScalePath": "Add Paint Scale Path",
|
||||
"config": "Configuration",
|
||||
"emsOutFilePath": "EMS Out File Path (Parts Order, etc.)",
|
||||
"esApiKey": "Estimate Scrubber API Key",
|
||||
"invalidPath": "Path not set or invalid",
|
||||
"paintScalePath": "Paint Scale Path",
|
||||
"paintScaleSettingsInput": "BSMS To Paint Scale",
|
||||
"paintScaleSettingsOutput": "Paint Scale To BSMS",
|
||||
"paintScaleType": "Paint Scale Type",
|
||||
"pollingInterval": "Polling Interval (m)",
|
||||
"pollinginterval": "Polling Interval (ms)",
|
||||
"ppcfilepath": "Parts Price Change File Path",
|
||||
"remove": "Remove",
|
||||
"selectPaintScaleType": "Select Paint Scale Type",
|
||||
"started": "Started",
|
||||
"stopped": "Stopped",
|
||||
"validPath": "Valid path",
|
||||
"watchedpaths": "Watched Paths",
|
||||
"watchermodepolling": "Polling",
|
||||
"watchermoderealtime": "Real Time",
|
||||
"watcherstatus": "Watcher Status"
|
||||
},
|
||||
"validation": {
|
||||
"esApiKeyRequired": "Estimate Scrubber API Key is required."
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"imex": "ImEX Online",
|
||||
"rome": "Rome Online"
|
||||
},
|
||||
"updates": {
|
||||
"apply": "Apply Update",
|
||||
"applying": "Applying update",
|
||||
"available": "An update is available.",
|
||||
"download": "Download Update",
|
||||
"downloading": "An update is downloading."
|
||||
}
|
||||
}
|
||||
"translation": {
|
||||
"auth": {
|
||||
"labels": {
|
||||
"welcome": "Hi {{name}}"
|
||||
},
|
||||
"login": {
|
||||
"error": "The username and password combination provided is not valid.",
|
||||
"login": "Log In",
|
||||
"resetpassword": "Reset Password"
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"actions": {
|
||||
"clear_all": "Clear All"
|
||||
},
|
||||
"labels": {
|
||||
"dashboard": "Dashboard",
|
||||
"estimate_scrub_history": "Scrub History",
|
||||
"last_processed": "Last Processed at",
|
||||
"scrub_results": "Scrub Results",
|
||||
"total_estimates": "Total Estimates"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"errorboundary": "Uh oh - we've hit an error.",
|
||||
"notificationtitle": "Error Encountered"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"settings": "Settings",
|
||||
"signout": "Sign Out"
|
||||
},
|
||||
"settings": {
|
||||
"actions": {
|
||||
"addpath": "Add path",
|
||||
"startwatcher": "Start Watcher",
|
||||
"stopwatcher": "Stop Watcher\n"
|
||||
},
|
||||
"errors": {
|
||||
"duplicatePath": "The selected directory is already used in another configuration."
|
||||
},
|
||||
"labels": {
|
||||
"actions": "Actions",
|
||||
"addPaintScalePath": "Add Paint Scale Path",
|
||||
"appearance": "Appearance",
|
||||
"config": "Configuration",
|
||||
"dark": "Dark",
|
||||
"darkMode": "Dark Mode",
|
||||
"darkModeDescription": "Use the dark application theme.",
|
||||
"emsOutFilePath": "EMS Out File Path (Parts Order, etc.)",
|
||||
"esApiKey": "Estimate Scrubber API Key",
|
||||
"invalidPath": "Path not set or invalid",
|
||||
"light": "Light",
|
||||
"paintScalePath": "Paint Scale Path",
|
||||
"paintScaleSettingsInput": "BSMS To Paint Scale",
|
||||
"paintScaleSettingsOutput": "Paint Scale To BSMS",
|
||||
"paintScaleType": "Paint Scale Type",
|
||||
"pollingInterval": "Polling Interval (m)",
|
||||
"pollinginterval": "Polling Interval (ms)",
|
||||
"ppcfilepath": "Parts Price Change File Path",
|
||||
"remove": "Remove",
|
||||
"selectPaintScaleType": "Select Paint Scale Type",
|
||||
"started": "Started",
|
||||
"stopped": "Stopped",
|
||||
"validPath": "Valid path",
|
||||
"watchedpaths": "Watched Paths",
|
||||
"watchermodepolling": "Polling",
|
||||
"watchermoderealtime": "Real Time",
|
||||
"watcherstatus": "Watcher Status"
|
||||
},
|
||||
"validation": {
|
||||
"esApiKeyRequired": "Estimate Scrubber API Key is required."
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"imex": "ImEX Online",
|
||||
"rome": "Rome Online"
|
||||
},
|
||||
"updates": {
|
||||
"apply": "Apply Update",
|
||||
"applying": "Applying update",
|
||||
"available": "An update is available.",
|
||||
"download": "Download Update",
|
||||
"downloading": "An update is downloading."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user