This commit is contained in:
Vomitblood 2024-08-01 01:47:08 +08:00
parent 6e1be3513b
commit 6e0c1d2b85
12 changed files with 432 additions and 17 deletions

BIN
bun.lockb

Binary file not shown.

View file

@ -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"

View file

@ -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.

View file

@ -23,6 +23,9 @@
"notification": {
"all": true
},
"path": {
"all": true
},
"process": {
"all": true
},

View 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>
</>
);
};

View file

@ -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>

View file

View 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;
};

View 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
View 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();

View file

@ -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
View 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>
);
}