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
|
.terraformrc
|
||||||
terraform.rc
|
terraform.rc
|
||||||
|
/.eslintcache
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const store = new Store({
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
interval: 30000,
|
interval: 30000,
|
||||||
},
|
},
|
||||||
|
darkMode: false,
|
||||||
esApiKey: "",
|
esApiKey: "",
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ConfigProvider, Layout } from "antd";
|
import { ConfigProvider, Layout, theme } from "antd";
|
||||||
import { FC, useEffect } from "react";
|
import { FC, useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { ErrorBoundary } from "react-error-boundary";
|
import { ErrorBoundary } from "react-error-boundary";
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import { HashRouter, Route, Routes, useNavigate } from "react-router";
|
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 UpdateAvailable from "./components/UpdateAvailable/UpdateAvailable";
|
||||||
import reduxStore from "./redux/redux-store";
|
import reduxStore from "./redux/redux-store";
|
||||||
import { NotificationProvider } from "./util/notificationContext";
|
import { NotificationProvider } from "./util/notificationContext";
|
||||||
|
import { ThemeModeContext } from "./util/themeModeContext";
|
||||||
import ipcTypes from "../../util/ipcTypes.json";
|
import ipcTypes from "../../util/ipcTypes.json";
|
||||||
|
|
||||||
|
const { darkAlgorithm, defaultAlgorithm } = theme;
|
||||||
|
|
||||||
const ScrubHistoryNavigationBridge: FC = () => {
|
const ScrubHistoryNavigationBridge: FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -34,53 +37,112 @@ const ScrubHistoryNavigationBridge: FC = () => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const App: FC = () => {
|
const AppShell: FC = () => {
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfigProvider
|
<Provider store={reduxStore}>
|
||||||
theme={{
|
<HashRouter>
|
||||||
token: {
|
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
||||||
borderRadius: 8,
|
<NotificationProvider>
|
||||||
},
|
<ScrubHistoryNavigationBridge />
|
||||||
components: {
|
<Layout
|
||||||
Card: {
|
style={{
|
||||||
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)",
|
height: "100vh",
|
||||||
},
|
minHeight: 0,
|
||||||
},
|
overflow: "hidden",
|
||||||
}}
|
background: token.colorBgLayout,
|
||||||
>
|
}}
|
||||||
<Provider store={reduxStore}>
|
>
|
||||||
<HashRouter>
|
<Layout.Content
|
||||||
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
|
||||||
<NotificationProvider>
|
|
||||||
<ScrubHistoryNavigationBridge />
|
|
||||||
<Layout
|
|
||||||
style={{
|
style={{
|
||||||
height: "100vh",
|
height: "100%",
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
overflow: "hidden",
|
overflow: "auto",
|
||||||
background: "#f0f2f5",
|
padding: "24px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Layout.Content
|
<UpdateAvailable />
|
||||||
style={{
|
<Routes>
|
||||||
height: "100%",
|
<Route path="/" element={<Home />} />
|
||||||
minHeight: 0,
|
<Route path="/settings" element={<Settings />} />
|
||||||
overflow: "auto",
|
</Routes>
|
||||||
padding: "24px",
|
</Layout.Content>
|
||||||
}}
|
</Layout>
|
||||||
>
|
</NotificationProvider>
|
||||||
<UpdateAvailable />
|
</ErrorBoundary>
|
||||||
<Routes>
|
</HashRouter>
|
||||||
<Route path="/" element={<Home />} />
|
</Provider>
|
||||||
<Route path="/settings" element={<Settings />} />
|
);
|
||||||
</Routes>
|
};
|
||||||
</Layout.Content>
|
|
||||||
</Layout>
|
const App: FC = () => {
|
||||||
</NotificationProvider>
|
const [isDarkMode, setIsDarkMode] = useState<boolean>(false);
|
||||||
</ErrorBoundary>
|
|
||||||
</HashRouter>
|
useEffect(() => {
|
||||||
</Provider>
|
let cancelled = false;
|
||||||
</ConfigProvider>
|
|
||||||
|
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 { ArrowLeftOutlined } from "@ant-design/icons";
|
||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
import { useNavigate } from "react-router";
|
import { useNavigate } from "react-router";
|
||||||
|
import SettingsAppearance from "./Settings.Appearance";
|
||||||
import SettingsConfig from "./Settings.Config";
|
import SettingsConfig from "./Settings.Config";
|
||||||
import SettingsWatcher from "./Settings.Watcher";
|
import SettingsWatcher from "./Settings.Watcher";
|
||||||
|
|
||||||
@@ -10,20 +11,30 @@ const Settings: FC = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space orientation="vertical" size="large" style={{ width: "100%" }}>
|
<div
|
||||||
<Button
|
style={{
|
||||||
type="text"
|
maxWidth: "1400px",
|
||||||
icon={<ArrowLeftOutlined />}
|
height: "100%",
|
||||||
onClick={() => navigate("/")}
|
margin: "0 auto",
|
||||||
style={{ alignSelf: "flex-start" }}
|
}}
|
||||||
>
|
>
|
||||||
Back
|
<Space orientation="vertical" size="large" style={{ width: "100%" }}>
|
||||||
</Button>
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<ArrowLeftOutlined />}
|
||||||
|
onClick={() => navigate("/")}
|
||||||
|
style={{ alignSelf: "flex-start" }}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
|
||||||
<SettingsWatcher />
|
<SettingsAppearance />
|
||||||
|
|
||||||
<SettingsConfig />
|
<SettingsWatcher />
|
||||||
</Space>
|
|
||||||
|
<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": {
|
"translation": {
|
||||||
"auth": {
|
"auth": {
|
||||||
"labels": {
|
"labels": {
|
||||||
"welcome": "Hi {{name}}"
|
"welcome": "Hi {{name}}"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"error": "The username and password combination provided is not valid.",
|
"error": "The username and password combination provided is not valid.",
|
||||||
"login": "Log In",
|
"login": "Log In",
|
||||||
"resetpassword": "Reset Password"
|
"resetpassword": "Reset Password"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"clear_all": "Clear All"
|
"clear_all": "Clear All"
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"estimate_scrub_history": "Scrub History",
|
"estimate_scrub_history": "Scrub History",
|
||||||
"last_processed": "Last Processed at",
|
"last_processed": "Last Processed at",
|
||||||
"scrub_results": "Scrub Results",
|
"scrub_results": "Scrub Results",
|
||||||
"total_estimates": "Total Estimates"
|
"total_estimates": "Total Estimates"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"errorboundary": "Uh oh - we've hit an error.",
|
"errorboundary": "Uh oh - we've hit an error.",
|
||||||
"notificationtitle": "Error Encountered"
|
"notificationtitle": "Error Encountered"
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"signout": "Sign Out"
|
"signout": "Sign Out"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"actions": {
|
"actions": {
|
||||||
"addpath": "Add path",
|
"addpath": "Add path",
|
||||||
"startwatcher": "Start Watcher",
|
"startwatcher": "Start Watcher",
|
||||||
"stopwatcher": "Stop Watcher\n"
|
"stopwatcher": "Stop Watcher\n"
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
"duplicatePath": "The selected directory is already used in another configuration."
|
"duplicatePath": "The selected directory is already used in another configuration."
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"addPaintScalePath": "Add Paint Scale Path",
|
"addPaintScalePath": "Add Paint Scale Path",
|
||||||
"config": "Configuration",
|
"appearance": "Appearance",
|
||||||
"emsOutFilePath": "EMS Out File Path (Parts Order, etc.)",
|
"config": "Configuration",
|
||||||
"esApiKey": "Estimate Scrubber API Key",
|
"dark": "Dark",
|
||||||
"invalidPath": "Path not set or invalid",
|
"darkMode": "Dark Mode",
|
||||||
"paintScalePath": "Paint Scale Path",
|
"darkModeDescription": "Use the dark application theme.",
|
||||||
"paintScaleSettingsInput": "BSMS To Paint Scale",
|
"emsOutFilePath": "EMS Out File Path (Parts Order, etc.)",
|
||||||
"paintScaleSettingsOutput": "Paint Scale To BSMS",
|
"esApiKey": "Estimate Scrubber API Key",
|
||||||
"paintScaleType": "Paint Scale Type",
|
"invalidPath": "Path not set or invalid",
|
||||||
"pollingInterval": "Polling Interval (m)",
|
"light": "Light",
|
||||||
"pollinginterval": "Polling Interval (ms)",
|
"paintScalePath": "Paint Scale Path",
|
||||||
"ppcfilepath": "Parts Price Change File Path",
|
"paintScaleSettingsInput": "BSMS To Paint Scale",
|
||||||
"remove": "Remove",
|
"paintScaleSettingsOutput": "Paint Scale To BSMS",
|
||||||
"selectPaintScaleType": "Select Paint Scale Type",
|
"paintScaleType": "Paint Scale Type",
|
||||||
"started": "Started",
|
"pollingInterval": "Polling Interval (m)",
|
||||||
"stopped": "Stopped",
|
"pollinginterval": "Polling Interval (ms)",
|
||||||
"validPath": "Valid path",
|
"ppcfilepath": "Parts Price Change File Path",
|
||||||
"watchedpaths": "Watched Paths",
|
"remove": "Remove",
|
||||||
"watchermodepolling": "Polling",
|
"selectPaintScaleType": "Select Paint Scale Type",
|
||||||
"watchermoderealtime": "Real Time",
|
"started": "Started",
|
||||||
"watcherstatus": "Watcher Status"
|
"stopped": "Stopped",
|
||||||
},
|
"validPath": "Valid path",
|
||||||
"validation": {
|
"watchedpaths": "Watched Paths",
|
||||||
"esApiKeyRequired": "Estimate Scrubber API Key is required."
|
"watchermodepolling": "Polling",
|
||||||
}
|
"watchermoderealtime": "Real Time",
|
||||||
},
|
"watcherstatus": "Watcher Status"
|
||||||
"title": {
|
},
|
||||||
"imex": "ImEX Online",
|
"validation": {
|
||||||
"rome": "Rome Online"
|
"esApiKeyRequired": "Estimate Scrubber API Key is required."
|
||||||
},
|
}
|
||||||
"updates": {
|
},
|
||||||
"apply": "Apply Update",
|
"title": {
|
||||||
"applying": "Applying update",
|
"imex": "ImEX Online",
|
||||||
"available": "An update is available.",
|
"rome": "Rome Online"
|
||||||
"download": "Download Update",
|
},
|
||||||
"downloading": "An update is downloading."
|
"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