diff --git a/bun.lockb b/bun.lockb index 06bf92c..a89fb6b 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 30be59f..2f64333 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7e361e4..2645023 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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. diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 4eb2ccf..f9dddaf 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -23,6 +23,9 @@ "notification": { "all": true }, + "path": { + "all": true + }, "process": { "all": true }, diff --git a/src/components/Generic/FloatingDialog.tsx b/src/components/Generic/FloatingDialog.tsx new file mode 100644 index 0000000..4144c16 --- /dev/null +++ b/src/components/Generic/FloatingDialog.tsx @@ -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 = ({ + sx, + openState, + maximisedState, + setMaximisedState, + toggleOpen, + close, + actionButtons, + body, + bottomBar, + openButton, + title, +}) => { + const { settings } = useSettings(); + + return ( + <> + {openButton} + + + + + {title} + + + {actionButtons} + + + { + setMaximisedState(!maximisedState); + }} + sx={{ + mr: 1, + }} + > + {maximisedState ? : } + + + + + + + + + {body} + {bottomBar} + + + + ); +}; diff --git a/src/components/HeaderBar/HeaderBar.tsx b/src/components/HeaderBar/HeaderBar.tsx index e80282f..88f0f86 100644 --- a/src/components/HeaderBar/HeaderBar.tsx +++ b/src/components/HeaderBar/HeaderBar.tsx @@ -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", }} > - hello this is the right side | + + + diff --git a/src/components/HeaderBar/Settings.tsx b/src/components/HeaderBar/Settings.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/contexts/SettingsContext.tsx b/src/contexts/SettingsContext.tsx new file mode 100644 index 0000000..abfa944 --- /dev/null +++ b/src/contexts/SettingsContext.tsx @@ -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; + fetchUserSettingsCloud: () => Promise; + updateSettingsLocal: (settings: SettingsType) => void; + updateSettingsCloud: (settings: SettingsType) => Promise; + deleteSettingsLocal: () => void; + deleteSettingsCloud: () => Promise; + settingsLoading: boolean; +}; + +const SettingsContext = createContext( + undefined, +); + +export const SettingsProvider: FC<{ children: ReactNode }> = ({ children }) => { + logcat.log("Initializing settings...", "INFO"); + + // useStates + const [settings, setSettings] = useState(defaultSettings); + const [settingsLoading, setSettingsLoading] = useState(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 ( + + {children} + + ); +}; + +export const useSettings = () => { + const context = useContext(SettingsContext); + if (context === undefined) { + throw new Error("Please use useSettings only within a SettingsProvider"); + } + return context; +}; diff --git a/src/lib/defaultSettings.ts b/src/lib/defaultSettings.ts new file mode 100644 index 0000000..a8094e6 --- /dev/null +++ b/src/lib/defaultSettings.ts @@ -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; diff --git a/src/lib/logcatService.ts b/src/lib/logcatService.ts new file mode 100644 index 0000000..f836c4f --- /dev/null +++ b/src/lib/logcatService.ts @@ -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(); diff --git a/src/pages/api/hello.ts b/src/pages/api/hello.ts deleted file mode 100644 index ea77e8f..0000000 --- a/src/pages/api/hello.ts +++ /dev/null @@ -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, -) { - res.status(200).json({ name: "John Doe" }); -} diff --git a/src/pages/testing.tsx b/src/pages/testing.tsx new file mode 100644 index 0000000..5dfe6c7 --- /dev/null +++ b/src/pages/testing.tsx @@ -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 ( + + + + + + + ); +}