diff --git a/bun.lockb b/bun.lockb index a89fb6b..d4f8a20 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 2f64333..0f0fcbd 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@tauri-apps/api": "^1.6.0", "@types/lodash": "^4.17.7", "lodash": "^4.17.21", + "lowdb": "^7.0.1", "next": "14.2.5", "react": "^18", "react-dom": "^18" diff --git a/src/components/HeaderBar/Settings.tsx b/src/components/HeaderBar/Settings/Settings.tsx similarity index 100% rename from src/components/HeaderBar/Settings.tsx rename to src/components/HeaderBar/Settings/Settings.tsx diff --git a/src/contexts/SettingsContext.tsx b/src/contexts/SettingsContext.tsx index abfa944..184e6d2 100644 --- a/src/contexts/SettingsContext.tsx +++ b/src/contexts/SettingsContext.tsx @@ -1,24 +1,19 @@ -import { merge } from "lodash"; import { + createContext, FC, ReactNode, - createContext, useContext, useEffect, + useMemo, useState, } from "react"; import { defaultSettings, SettingsType } from "../lib/defaultSettings"; import { logcat } from "../lib/logcatService"; +import { LowDB } from "../lib/lowDB"; // settings context type SettingsContextProps = { settings: SettingsType; - fetchUserSettingsLocal: () => Promise; - fetchUserSettingsCloud: () => Promise; - updateSettingsLocal: (settings: SettingsType) => void; - updateSettingsCloud: (settings: SettingsType) => Promise; - deleteSettingsLocal: () => void; - deleteSettingsCloud: () => Promise; settingsLoading: boolean; }; @@ -33,163 +28,46 @@ export const SettingsProvider: FC<{ children: ReactNode }> = ({ children }) => { const [settings, setSettings] = useState(defaultSettings); const [settingsLoading, setSettingsLoading] = useState(true); - // fetch user settings from localStorage - const fetchUserSettingsLocal = async () => { + // initialize a new lowdb instance for settings outside of state + const settingsDB = useMemo(() => { + return new LowDB("settings.json", defaultSettings); + }, []); // empty dependency array ensures this is only created once + + // function to update settings + const updateSettings = async (updates: Partial) => { 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); + await settingsDB.updateData((data) => { + Object.assign(data, updates); + }); + const newSettings = await settingsDB.readData(); + setSettings(newSettings); + logcat.log("Settings updated successfully", "INFO"); } catch (error) { - logcat.log("Error fetching user settings from localStorage", "ERROR"); - logcat.log(String(error), "ERROR"); - setSettingsLoading(false); + logcat.log(`Failed to update settings: ${error}`, "ERROR"); } }; - // 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 + // fetch user settings from local on first load every time useEffect(() => { - fetchUserSettingsLocal(); - }, []); + const fetchSettings = async () => { + try { + await settingsDB.init(); + const storedSettings = await settingsDB.readData(); + setSettings(storedSettings); + } catch (error) { + logcat.log(`Failed to load settings: ${error}`, "ERROR"); + } finally { + setSettingsLoading(false); + } + }; + + fetchSettings(); + }, [settingsDB]); return ( diff --git a/src/lib/defaultSettings.ts b/src/lib/defaultSettings.ts index a8094e6..0e6d4f5 100644 --- a/src/lib/defaultSettings.ts +++ b/src/lib/defaultSettings.ts @@ -9,16 +9,6 @@ export const defaultSettings = { 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/lowDB.ts b/src/lib/lowDB.ts new file mode 100644 index 0000000..ad2dcd6 --- /dev/null +++ b/src/lib/lowDB.ts @@ -0,0 +1,53 @@ +import { merge } from "lodash"; +import { Low } from "lowdb"; +import { JSONFile } from "lowdb/node"; + +// define a generic interface for json structure +interface Database { + data: T; +} + +type UpdateCallback = (data: T) => void; + +export class LowDB { + private db: Low>; + private defaultData: T; + + constructor(filePath: string, defaultData: T) { + const adapter = new JSONFile>(filePath); + this.db = new Low(adapter, { data: defaultData }); + this.defaultData = defaultData; + } + + // initialize the json file and merge with default data if needed + async init() { + await this.db.read(); + if (!this.db.data || !this.db.data.data) { + // initialize with default data + this.db.data = { data: { ...this.defaultData } }; + } else { + // merge existing data with default data to fill in missing properties + this.db.data.data = { ...this.defaultData, ...this.db.data.data }; + } + await this.db.write(); + } + + async readData(): Promise { + await this.db.read(); + // ensure that the data is merged with default values every time it is read + this.db.data!.data = merge({}, this.defaultData, this.db.data!.data); + return this.db.data!.data; + } + + async writeData(data: T): Promise { + this.db.data!.data = data; + await this.db.write(); + } + + // update a specific part of the json file + async updateData(callback: UpdateCallback): Promise { + await this.db.read(); + callback(this.db.data!.data); + await this.db.write(); + } +} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 6fb4c69..b91cc5f 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -18,8 +18,8 @@ export default function MyApp(props: MyAppProps) { return ( - WhensApp - + Stort + {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}