mirror of
https://github.com/Vomitblood/stort.git
synced 2024-11-26 13:55:27 +08:00
settings
This commit is contained in:
parent
6e1be3513b
commit
6e0c1d2b85
|
@ -17,6 +17,8 @@
|
|||
"@mui/icons-material": "^5.16.5",
|
||||
"@mui/material": "^5.16.5",
|
||||
"@tauri-apps/api": "^1.6.0",
|
||||
"@types/lodash": "^4.17.7",
|
||||
"lodash": "^4.17.21",
|
||||
"next": "14.2.5",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
|
|
|
@ -17,7 +17,7 @@ tauri-build = { version = "1.5.3", features = [] }
|
|||
[dependencies]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1.7.0", features = [ "window-all", "process-all", "notification-all", "dialog-all"] }
|
||||
tauri = { version = "1.7.0", features = [ "path-all", "window-all", "process-all", "notification-all", "dialog-all"] }
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||
|
|
|
@ -23,6 +23,9 @@
|
|||
"notification": {
|
||||
"all": true
|
||||
},
|
||||
"path": {
|
||||
"all": true
|
||||
},
|
||||
"process": {
|
||||
"all": true
|
||||
},
|
||||
|
|
99
src/components/Generic/FloatingDialog.tsx
Normal file
99
src/components/Generic/FloatingDialog.tsx
Normal file
|
@ -0,0 +1,99 @@
|
|||
import { Close, UnfoldLess, UnfoldMore } from "@mui/icons-material";
|
||||
import { Box, IconButton, Modal, Tooltip, Typography } from "@mui/material";
|
||||
import { FC, ReactNode } from "react";
|
||||
|
||||
interface FloatingDialog {
|
||||
sx?: any;
|
||||
openState: boolean;
|
||||
maximisedState: boolean;
|
||||
setMaximisedState: (state: boolean) => void;
|
||||
toggleOpen: () => void;
|
||||
close: () => void;
|
||||
actionButtons?: ReactNode;
|
||||
body: ReactNode;
|
||||
bottomBar?: ReactNode;
|
||||
openButton: ReactNode;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const FloatingDialog: FC<FloatingDialog> = ({
|
||||
sx,
|
||||
openState,
|
||||
maximisedState,
|
||||
setMaximisedState,
|
||||
toggleOpen,
|
||||
close,
|
||||
actionButtons,
|
||||
body,
|
||||
bottomBar,
|
||||
openButton,
|
||||
title,
|
||||
}) => {
|
||||
const { settings } = useSettings();
|
||||
|
||||
return (
|
||||
<>
|
||||
{openButton}
|
||||
|
||||
<Modal onClose={close} open={openState}>
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: "background.paper",
|
||||
borderRadius: maximisedState
|
||||
? "0px"
|
||||
: settings.display.radius + "px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: maximisedState
|
||||
? "100%"
|
||||
: settings.display.window_height + "%",
|
||||
left: "50%",
|
||||
maxHeight: maximisedState ? "100vh" : "96vh",
|
||||
maxWidth: maximisedState ? "100vw" : "96vw",
|
||||
p: 2,
|
||||
position: "absolute" as "absolute",
|
||||
top: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
transition: "all ease-in-out",
|
||||
transitionDuration: settings.display.transition_duration + "ms",
|
||||
width: maximisedState
|
||||
? "100vw"
|
||||
: settings.display.window_width + "px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
px: 1,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6">{title}</Typography>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
|
||||
{actionButtons}
|
||||
|
||||
<Tooltip title={maximisedState ? "Minimise" : "Maximise"}>
|
||||
<IconButton
|
||||
onClick={(event) => {
|
||||
setMaximisedState(!maximisedState);
|
||||
}}
|
||||
sx={{
|
||||
mr: 1,
|
||||
}}
|
||||
>
|
||||
{maximisedState ? <UnfoldLess /> : <UnfoldMore />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Close">
|
||||
<IconButton onClick={close}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
{body}
|
||||
{bottomBar}
|
||||
</Box>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -1,5 +1,6 @@
|
|||
import { Box, Typography } from "@mui/material";
|
||||
import { Box, Button, IconButton, Typography } from "@mui/material";
|
||||
import { WindowButtons } from "./WindowButtons";
|
||||
import { BugReport } from "@mui/icons-material";
|
||||
|
||||
export const HeaderBar = () => {
|
||||
return (
|
||||
|
@ -12,7 +13,7 @@ export const HeaderBar = () => {
|
|||
borderBottom: "1px solid red",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
height: "64px",
|
||||
height: "48px",
|
||||
justifyContent: "space-between",
|
||||
p: 1,
|
||||
}}
|
||||
|
@ -37,7 +38,9 @@ export const HeaderBar = () => {
|
|||
flexDirection: "row",
|
||||
}}
|
||||
>
|
||||
<Typography>hello this is the right side |</Typography>
|
||||
<IconButton href="/testing">
|
||||
<BugReport />
|
||||
</IconButton>
|
||||
<WindowButtons />
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
0
src/components/HeaderBar/Settings.tsx
Normal file
0
src/components/HeaderBar/Settings.tsx
Normal file
207
src/contexts/SettingsContext.tsx
Normal file
207
src/contexts/SettingsContext.tsx
Normal file
|
@ -0,0 +1,207 @@
|
|||
import { merge } from "lodash";
|
||||
import {
|
||||
FC,
|
||||
ReactNode,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { defaultSettings, SettingsType } from "../lib/defaultSettings";
|
||||
import { logcat } from "../lib/logcatService";
|
||||
|
||||
// settings context
|
||||
type SettingsContextProps = {
|
||||
settings: SettingsType;
|
||||
fetchUserSettingsLocal: () => Promise<void>;
|
||||
fetchUserSettingsCloud: () => Promise<void>;
|
||||
updateSettingsLocal: (settings: SettingsType) => void;
|
||||
updateSettingsCloud: (settings: SettingsType) => Promise<void>;
|
||||
deleteSettingsLocal: () => void;
|
||||
deleteSettingsCloud: () => Promise<void>;
|
||||
settingsLoading: boolean;
|
||||
};
|
||||
|
||||
const SettingsContext = createContext<SettingsContextProps | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export const SettingsProvider: FC<{ children: ReactNode }> = ({ children }) => {
|
||||
logcat.log("Initializing settings...", "INFO");
|
||||
|
||||
// useStates
|
||||
const [settings, setSettings] = useState<SettingsType>(defaultSettings);
|
||||
const [settingsLoading, setSettingsLoading] = useState<boolean>(true);
|
||||
|
||||
// fetch user settings from localStorage
|
||||
const fetchUserSettingsLocal = async () => {
|
||||
try {
|
||||
logcat.log("Fetching user settings from localStorage...", "INFO");
|
||||
const userSettings = localStorage.getItem("userSettings");
|
||||
|
||||
if (userSettings) {
|
||||
// deep merge user settings with default settings
|
||||
const mergedSettings = merge(
|
||||
{},
|
||||
defaultSettings,
|
||||
JSON.parse(userSettings),
|
||||
);
|
||||
|
||||
setSettings(mergedSettings);
|
||||
|
||||
logcat.log("User settings fetched from localStorage", "INFO");
|
||||
logcat.log("User settings: " + userSettings, "DEBUG");
|
||||
} else {
|
||||
logcat.log(
|
||||
"User settings not found in localStorage, using default settings",
|
||||
"WARN",
|
||||
);
|
||||
}
|
||||
setSettingsLoading(false);
|
||||
} catch (error) {
|
||||
logcat.log("Error fetching user settings from localStorage", "ERROR");
|
||||
logcat.log(String(error), "ERROR");
|
||||
setSettingsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// fetch user settings from Firestore
|
||||
const fetchUserSettingsCloud = async () => {
|
||||
try {
|
||||
if (user) {
|
||||
logcat.log("Fetching user settings from firestore...", "INFO");
|
||||
const userSettings = await firestoreRetrieveDocumentField(
|
||||
`users/${user.uid}`,
|
||||
"settings",
|
||||
);
|
||||
|
||||
if (userSettings) {
|
||||
// deep merge user settings with default settings
|
||||
const mergedSettings = merge({}, defaultSettings, userSettings);
|
||||
|
||||
setSettings(mergedSettings);
|
||||
|
||||
logcat.log("User settings fetched from firestore", "INFO");
|
||||
logcat.log("User settings: " + JSON.stringify(userSettings), "DEBUG");
|
||||
|
||||
// and then save to localStorage
|
||||
updateSettingsLocal(mergedSettings);
|
||||
} else {
|
||||
logcat.log(
|
||||
"User settings not found in firestore, using default settings",
|
||||
"WARN",
|
||||
);
|
||||
}
|
||||
setSettingsLoading(false);
|
||||
} else {
|
||||
logcat.log("User not logged in, using default settings", "WARN");
|
||||
setSettingsLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
logcat.log("No such field", "ERROR");
|
||||
logcat.log(String(error), "ERROR");
|
||||
setSettingsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// push new settings to localStorage
|
||||
const updateSettingsLocal = (settings: SettingsType) => {
|
||||
try {
|
||||
// apply settings into state
|
||||
logcat.log("Applying settings...", "INFO");
|
||||
setSettings(settings);
|
||||
logcat.log("Settings applied", "INFO");
|
||||
|
||||
// save settings to localStorage
|
||||
logcat.log("Saving user settings to localStorage...", "INFO");
|
||||
localStorage.setItem("userSettings", JSON.stringify(settings));
|
||||
logcat.log("User settings saved to localStorage", "INFO");
|
||||
} catch (error) {
|
||||
logcat.log("Error saving user settings to localStorage", "ERROR");
|
||||
logcat.log(String(error), "ERROR");
|
||||
}
|
||||
};
|
||||
|
||||
// push new settings to firestore
|
||||
const updateSettingsCloud = async (settings: SettingsType) => {
|
||||
try {
|
||||
// apply settings into state
|
||||
logcat.log("Applying settings...", "INFO");
|
||||
setSettings(settings);
|
||||
logcat.log("Settings applied", "INFO");
|
||||
|
||||
// save settings to firestore
|
||||
logcat.log("Saving user settings to Firestore...", "INFO");
|
||||
|
||||
// push entire settings object to firestore
|
||||
if (user) {
|
||||
firestoreCommitUpdate(`users/${user.uid}`, "settings", settings);
|
||||
await firestorePushUpdates();
|
||||
|
||||
logcat.log("User settings saved to firestore", "INFO");
|
||||
}
|
||||
} catch (error) {
|
||||
logcat.log("Error saving user settings to firestore", "ERROR");
|
||||
logcat.log(String(error), "ERROR");
|
||||
}
|
||||
};
|
||||
|
||||
// delete settings from localStorage
|
||||
const deleteSettingsLocal = () => {
|
||||
try {
|
||||
logcat.log("Deleting user settings from localStorage...", "INFO");
|
||||
localStorage.removeItem("userSettings");
|
||||
logcat.log("User settings deleted from localStorage", "INFO");
|
||||
} catch (error) {
|
||||
logcat.log("Error deleting user settings from localStorage", "ERROR");
|
||||
logcat.log(String(error), "ERROR");
|
||||
}
|
||||
};
|
||||
|
||||
// delete settings from cloud
|
||||
const deleteSettingsCloud = async () => {
|
||||
try {
|
||||
logcat.log("Deleting user settings from Firestore...", "INFO");
|
||||
|
||||
if (user) {
|
||||
firestoreCommitDelete(`users/${user.uid}`, "settings");
|
||||
await firestorePushDeletes();
|
||||
|
||||
logcat.log("User settings deleted from Firestore", "INFO");
|
||||
}
|
||||
} catch (error) {
|
||||
logcat.log(String(error), "ERROR");
|
||||
throw new Error("Error deleting user settings from Firestore");
|
||||
}
|
||||
};
|
||||
|
||||
// fetch user settings from local on first load everytime
|
||||
useEffect(() => {
|
||||
fetchUserSettingsLocal();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SettingsContext.Provider
|
||||
value={{
|
||||
settings,
|
||||
fetchUserSettingsLocal,
|
||||
fetchUserSettingsCloud,
|
||||
updateSettingsLocal,
|
||||
updateSettingsCloud,
|
||||
deleteSettingsLocal,
|
||||
deleteSettingsCloud,
|
||||
settingsLoading,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SettingsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useSettings = () => {
|
||||
const context = useContext(SettingsContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("Please use useSettings only within a SettingsProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
24
src/lib/defaultSettings.ts
Normal file
24
src/lib/defaultSettings.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
export const defaultSettings = {
|
||||
display: {
|
||||
dark_mode: true as boolean,
|
||||
accent_color: "#8ab4f8" as string,
|
||||
transition_duration: 200 as number,
|
||||
radius: 8 as number,
|
||||
window_height: 60 as number,
|
||||
window_width: 400 as number,
|
||||
font_family: "monospace" as string,
|
||||
font_scaling: 100,
|
||||
},
|
||||
timings: {
|
||||
auto_refresh: false as boolean,
|
||||
auto_refresh_interval: 30 as number,
|
||||
eta_as_duration: true as boolean,
|
||||
military_time_format: true as boolean,
|
||||
timings_bus_expanded_items: 3 as number,
|
||||
timings_train_expanded_items: 3 as number,
|
||||
favourites_bus_expanded_items: 0 as number,
|
||||
favourites_train_expanded_items: 0 as number,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type SettingsType = typeof defaultSettings;
|
66
src/lib/logcatService.ts
Normal file
66
src/lib/logcatService.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
type LogLevel = "DEBUG" | "INFO" | "WARN" | "ERROR" | "CRITICAL";
|
||||
|
||||
export interface LogEntry {
|
||||
level: LogLevel;
|
||||
message: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export class LogcatServiceClass {
|
||||
private logs: LogEntry[] = [];
|
||||
private lastLogTimestamp: number | null = null;
|
||||
private listeners: (() => void)[] = [];
|
||||
|
||||
constructor() {
|
||||
this.log("LogcatService initialised", "INFO");
|
||||
}
|
||||
|
||||
addListener(callback: () => void) {
|
||||
this.listeners.push(callback);
|
||||
}
|
||||
|
||||
removeListener(callback: () => void) {
|
||||
this.listeners = this.listeners.filter((listener) => listener !== callback);
|
||||
}
|
||||
|
||||
private notifyListeners() {
|
||||
this.listeners.forEach((listener) => listener());
|
||||
}
|
||||
|
||||
log(message: string, level: LogLevel = "INFO"): void {
|
||||
const currentTimestamp = new Date().getTime();
|
||||
const timeSinceLastLog = this.lastLogTimestamp
|
||||
? currentTimestamp - this.lastLogTimestamp
|
||||
: 0;
|
||||
|
||||
const logEntry: LogEntry = { message, level, timestamp: timeSinceLastLog };
|
||||
this.logs.push(logEntry);
|
||||
|
||||
switch (level) {
|
||||
case "DEBUG":
|
||||
console.debug(`> [${level}] ${message} (+${timeSinceLastLog}ms)`);
|
||||
break;
|
||||
case "INFO":
|
||||
console.info(`> [${level}] ${message} (+${timeSinceLastLog}ms)`);
|
||||
break;
|
||||
case "WARN":
|
||||
console.warn(`> [${level}] ${message} (+${timeSinceLastLog}ms)`);
|
||||
break;
|
||||
case "ERROR":
|
||||
console.error(`> [${level}] ${message} (+${timeSinceLastLog}ms)`);
|
||||
break;
|
||||
case "CRITICAL":
|
||||
console.error(`> [${level}] ${message} (+${timeSinceLastLog}ms)`);
|
||||
break;
|
||||
}
|
||||
|
||||
this.lastLogTimestamp = currentTimestamp;
|
||||
this.notifyListeners();
|
||||
}
|
||||
|
||||
getLogs(): LogEntry[] {
|
||||
return this.logs;
|
||||
}
|
||||
}
|
||||
|
||||
export const logcat = new LogcatServiceClass();
|
|
@ -1,13 +0,0 @@
|
|||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
type Data = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export default function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<Data>,
|
||||
) {
|
||||
res.status(200).json({ name: "John Doe" });
|
||||
}
|
24
src/pages/testing.tsx
Normal file
24
src/pages/testing.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { BugReport } from "@mui/icons-material";
|
||||
import { Box, Button, IconButton } from "@mui/material";
|
||||
import { configDir } from "@tauri-apps/api/path";
|
||||
|
||||
export default function Testing() {
|
||||
const testing = async () => {
|
||||
console.log(await configDir());
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<IconButton href="/">
|
||||
<BugReport />
|
||||
</IconButton>
|
||||
<Button
|
||||
onClick={() => {
|
||||
testing();
|
||||
}}
|
||||
>
|
||||
Button
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
Loading…
Reference in a new issue