changed config format from json to toml

This commit is contained in:
Vomitblood 2024-08-06 21:47:34 +08:00
parent 515ea481ed
commit 7567e53a85
19 changed files with 5333 additions and 212 deletions

18
.prettierrc Normal file
View file

@ -0,0 +1,18 @@
{
"arrowParens": "always",
"bracketSameLine": false,
"bracketSpacing": true,
"endOfLine": "lf",
"htmlWhitespaceSensitivity": "css",
"jsxBracketSameLine": false,
"jsxSingleQuote": true,
"printWidth": 120,
"proseWrap": "preserve",
"quoteProps": "consistent",
"semi": true,
"singleAttributePerLine": true,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "all",
"useTabs": false
}

2
.tool-versions Normal file
View file

@ -0,0 +1,2 @@
bun 1.1.21
nodejs 20.6.1

BIN
bun.lockb

Binary file not shown.

5030
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -17,16 +17,16 @@
"@mui/icons-material": "^5.16.6",
"@mui/material": "^5.16.6",
"@tauri-apps/api": "^1.6.0",
"@types/lodash": "^4.17.7",
"jotai": "^2.9.1",
"lodash": "^4.17.21",
"lowdb": "^7.0.1",
"next": "14.2.5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"smol-toml": "^1.3.0",
"zustand": "^4.5.4"
},
"devDependencies": {
"@types/lodash": "^4.17.7",
"@tauri-apps/cli": "^1.6.0",
"@types/node": "^20.14.14",
"@types/react": "^18.3.3",

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 = [ "path-all", "window-all", "process-all", "notification-all", "dialog-all"] }
tauri = { version = "1.7.0", features = [ "fs-all", "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

@ -20,6 +20,13 @@
"open": true,
"save": true
},
"fs": {
"all": true,
"scope": [
"$CONFIG/stort/",
"$CONFIG/stort/**"
]
},
"notification": {
"all": true
},

View file

@ -1,11 +1,23 @@
import { useEffect } from "react";
import { usePathStore } from "../../lib/store/zustand/path";
import Paths from "../../lib/path";
import { createDir, exists } from "@tauri-apps/api/fs";
export const Initialization = () => {
const setPaths = usePathStore((state) => state.setPaths);
useEffect(() => {
setPaths();
const initializePaths = async () => {
try {
await Paths.initialize();
} catch (error) {
console.error(`Failed to initialize paths: ${error}`);
}
};
const createDirectories = async () => {
const configDirectoryExists = await exists(Paths.getPath("configDirectory"));
if (!configDirectoryExists) await createDir(Paths.getPath("configDirectory"));
};
initializePaths().then(() => createDirectories());
});
return null;

View file

@ -1,45 +1,27 @@
import {
createContext,
FC,
ReactNode,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import { defaultSettings, SettingsType } from "../lib/defaultSettings";
import { createContext, FC, ReactNode, useContext, useEffect, useState } from "react";
import { logcat } from "../lib/logcatService";
import { LowDB } from "../lib/lowDB";
import { defaultSettings, readConfigFile, SettingsType, writeConfigFile } from "../lib/settings";
// settings context
type SettingsContextProps = {
settings: SettingsType;
settingsLoading: boolean;
updateSettings: (updates: SettingsType) => void;
resetSettings: () => void;
};
const SettingsContext = createContext<SettingsContextProps | undefined>(
undefined,
);
const SettingsContext = createContext<SettingsContextProps | undefined>(undefined);
export const SettingsProvider: FC<{ children: ReactNode }> = ({ children }) => {
logcat.log("Initializing settings...", "INFO");
// useStates
// states
const [settings, setSettings] = useState<SettingsType>(defaultSettings);
const [settingsLoading, setSettingsLoading] = useState<boolean>(true);
// initialize a new lowdb instance for settings outside of state
const settingsDB = useMemo(() => {
return new LowDB<SettingsType>("settings.json", defaultSettings);
}, []); // empty dependency array ensures this is only created once
// function to update settings
const updateSettings = async (updates: Partial<SettingsType>) => {
const updateSettings = async (newSettings: SettingsType) => {
try {
await settingsDB.updateData((data) => {
Object.assign(data, updates);
});
const newSettings = await settingsDB.readData();
await writeConfigFile(newSettings);
setSettings(newSettings);
logcat.log("Settings updated successfully", "INFO");
} catch (error) {
@ -47,13 +29,24 @@ export const SettingsProvider: FC<{ children: ReactNode }> = ({ children }) => {
}
};
// set settings state to default values
// and write default values to the settings file
const resetSettings = async () => {
try {
await writeConfigFile(defaultSettings);
setSettings(defaultSettings);
logcat.log("Settings reset successfully", "INFO");
} catch (error) {
logcat.log(`Failed to reset settings: ${error}`, "ERROR");
}
};
// fetch user settings from local on first load every time
useEffect(() => {
const fetchSettings = async () => {
try {
await settingsDB.init();
const storedSettings = await settingsDB.readData();
setSettings(storedSettings);
const existingSettings = await readConfigFile();
setSettings(existingSettings);
} catch (error) {
logcat.log(`Failed to load settings: ${error}`, "ERROR");
} finally {
@ -62,13 +55,15 @@ export const SettingsProvider: FC<{ children: ReactNode }> = ({ children }) => {
};
fetchSettings();
}, [settingsDB]);
}, []);
return (
<SettingsContext.Provider
value={{
settings,
settingsLoading,
updateSettings,
resetSettings,
}}
>
{children}

View file

@ -1,14 +0,0 @@
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,
},
} as const;
export type SettingsType = typeof defaultSettings;

View file

@ -1,53 +0,0 @@
import { merge } from "lodash";
import { Low } from "lowdb";
import { JSONFile } from "lowdb/node";
// define a generic interface for json structure
interface Database<T> {
data: T;
}
type UpdateCallback<T> = (data: T) => void;
export class LowDB<T> {
private db: Low<Database<T>>;
private defaultData: T;
constructor(filePath: string, defaultData: T) {
const adapter = new JSONFile<Database<T>>(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<T> {
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<void> {
this.db.data!.data = data;
await this.db.write();
}
// update a specific part of the json file
async updateData(callback: UpdateCallback<T>): Promise<void> {
await this.db.read();
callback(this.db.data!.data);
await this.db.write();
}
}

88
src/lib/path.ts Normal file
View file

@ -0,0 +1,88 @@
type PathsType = {
audioDirectory: string;
cacheDirectory: string;
configDirectory: string;
dataDirectory: string;
desktopDirectory: string;
documentDirectory: string;
downloadDirectory: string;
executableDirectory: string;
fontDirectory: string;
homeDirectory: string;
logDirectory: string;
pictureDirectory: string;
templateDirectory: string;
videoDirectory: string;
};
const programTrailingPath = "stort/";
class Paths {
private static instance: Paths;
paths: PathsType | null = null;
private constructor() {}
static getInstance(): Paths {
if (!Paths.instance) {
Paths.instance = new Paths();
}
return Paths.instance;
}
async initialize(): Promise<void> {
// avoid reinitialization if already doen
if (this.paths) return;
const {
audioDir,
cacheDir,
configDir,
dataDir,
desktopDir,
documentDir,
downloadDir,
executableDir,
fontDir,
homeDir,
logDir,
pictureDir,
templateDir,
videoDir,
} = await import("@tauri-apps/api/path");
try {
this.paths = {
audioDirectory: await audioDir(),
cacheDirectory: (await cacheDir()) + programTrailingPath,
configDirectory: (await configDir()) + programTrailingPath,
dataDirectory: (await dataDir()) + programTrailingPath,
desktopDirectory: await desktopDir(),
documentDirectory: await documentDir(),
downloadDirectory: await downloadDir(),
executableDirectory: await executableDir(),
fontDirectory: await fontDir(),
homeDirectory: await homeDir(),
logDirectory: await logDir(),
pictureDirectory: await pictureDir(),
templateDirectory: await templateDir(),
videoDirectory: await videoDir(),
};
// make paths immutable
Object.freeze(this.paths);
} catch (error) {
console.error("Error initializing paths:", error);
throw error;
}
}
getPath(key: keyof PathsType): string {
if (!this.paths) {
throw new Error("Paths are not initialized. Call initialize() first.");
}
return this.paths[key];
}
}
export default Paths.getInstance();

43
src/lib/settings.ts Normal file
View file

@ -0,0 +1,43 @@
import Paths from "./path";
import { readTomlFile, writeTomlFile } from "./utils/toml";
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,
},
};
export type SettingsType = typeof defaultSettings;
export const readConfigFile = async (): Promise<SettingsType> => {
let existingData: SettingsType = defaultSettings;
try {
// try to read the config file
const data = await readTomlFile<SettingsType>(Paths.getPath("configDirectory") + "config.toml");
if (data) {
existingData = data;
}
} catch (error) {
// file does not exist, called function will throw error
console.error(`Failed to read settings file: ${error}, using default settings.`);
existingData = defaultSettings;
}
// merge the existing data with the default settings
// existing data will overwrite the default settings for the properties that exist
const mergedSettings = { ...defaultSettings, ...existingData };
return mergedSettings;
};
export const writeConfigFile = async (settingsValues: SettingsType): Promise<void> => {
await writeTomlFile<SettingsType>(Paths.getPath("configDirectory") + "config.toml", settingsValues);
};

View file

@ -1,93 +0,0 @@
import { create } from "zustand";
type Paths = {
audioDirectory: string;
cacheDirectory: string;
configDirectory: string;
dataDirectory: string;
desktopDirectory: string;
documentDirectory: string;
downloadDirectory: string;
executableDirectory: string;
fontDirectory: string;
homeDirectory: string;
logDirectory: string;
pictureDirectory: string;
templateDirectory: string;
videoDirectory: string;
};
type PathStore = {
paths: Paths;
setPaths: () => Promise<void>;
};
const programTrailingPath = "stort/";
const initializePaths = async (): Promise<Paths> => {
try {
const {
audioDir,
cacheDir,
configDir,
dataDir,
desktopDir,
documentDir,
downloadDir,
executableDir,
fontDir,
homeDir,
logDir,
pictureDir,
templateDir,
videoDir,
} = await import("@tauri-apps/api/path");
return {
audioDirectory: await audioDir(),
cacheDirectory: (await cacheDir()) + programTrailingPath,
configDirectory: (await configDir()) + programTrailingPath,
dataDirectory: (await dataDir()) + programTrailingPath,
desktopDirectory: await desktopDir(),
documentDirectory: await documentDir(),
downloadDirectory: await downloadDir(),
executableDirectory: await executableDir(),
fontDirectory: await fontDir(),
homeDirectory: await homeDir(),
logDirectory: await logDir(),
pictureDirectory: await pictureDir(),
templateDirectory: await templateDir(),
videoDirectory: await videoDir(),
};
} catch (error) {
console.error("Error initializing paths:", error);
throw error;
}
};
export const usePathStore = create<PathStore>((set) => ({
paths: {
audioDirectory: "",
cacheDirectory: "",
configDirectory: "",
dataDirectory: "",
desktopDirectory: "",
documentDirectory: "",
downloadDirectory: "",
executableDirectory: "",
fontDirectory: "",
homeDirectory: "",
logDirectory: "",
pictureDirectory: "",
templateDirectory: "",
videoDirectory: "",
},
setPaths: async () => {
try {
const paths = await initializePaths();
set({ paths });
} catch (error) {
console.error("Failed to set paths:", error);
}
},
}));

5
src/lib/testing.ts Normal file
View file

@ -0,0 +1,5 @@
import { createDir } from "@tauri-apps/api/fs";
export const testing = async () => {
await createDir("/home/vomitblood/.config/stort/");
};

21
src/lib/utils/json.ts Normal file
View file

@ -0,0 +1,21 @@
import { readTextFile, writeTextFile } from "@tauri-apps/api/fs";
export const readJsonFile = async <T>(path: string): Promise<T | null> => {
try {
const contents = await readTextFile(path);
return JSON.parse(contents);
} catch (error) {
console.error(`Failed to read JSON file ${path}: ${error}`);
throw new Error(`Failed to read JSON file ${path}: ${error}`);
}
};
export const writeJsonFile = async <T>(path: string, data: T): Promise<void> => {
try {
const jsonString = JSON.stringify(data, null, 2);
await writeTextFile(path, jsonString);
} catch (error) {
console.error(`Error writing file ${path}: ${error}`);
throw new Error(`Error writing file ${path}: ${error}`);
}
};

22
src/lib/utils/toml.ts Normal file
View file

@ -0,0 +1,22 @@
import { readTextFile, writeTextFile } from "@tauri-apps/api/fs";
import { parse, stringify } from "smol-toml";
export const readTomlFile = async <T>(path: string): Promise<T | null> => {
try {
const contents = await readTextFile(path);
return parse(contents) as T;
} catch (error) {
console.error(`Failed to read JSON file ${path}: ${error}`);
throw new Error(`Failed to read JSON file ${path}: ${error}`);
}
};
export const writeTomlFile = async <T>(path: string, data: T): Promise<void> => {
try {
const tomlString = stringify(data);
await writeTextFile(path, tomlString);
} catch (error) {
console.error(`Error writing file ${path}: ${error}`);
throw new Error(`Error writing file ${path}: ${error}`);
}
};

View file

@ -6,6 +6,7 @@ import { Initialization } from "../components/Generic/Initialization";
import { UserThemeProvider } from "../contexts/ThemeContext";
import createEmotionCache from "../lib/createEmotionCache";
import "../styles/global.css";
import { SettingsProvider } from "../contexts/SettingsContext";
// Client-side cache, shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();
@ -20,14 +21,19 @@ export default function MyApp(props: MyAppProps) {
<CacheProvider value={emotionCache}>
<Head>
<title>Stort</title>
<meta name="viewport" content="initial-scale=1, width=device-width" />
<meta
name='viewport'
content='initial-scale=1, width=device-width'
/>
</Head>
<SettingsProvider>
<UserThemeProvider>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<Initialization />
<CssBaseline />
<Component {...pageProps} />
</UserThemeProvider>
</SettingsProvider>
</CacheProvider>
);
}

View file

@ -1,16 +1,16 @@
"use client";
import { BugReport } from "@mui/icons-material";
import { Box, Button, IconButton, TextField, Typography } from "@mui/material";
import { useRouter } from "next/router";
import { useState } from "react";
import { usePathStore } from "../lib/store/zustand/path";
import { useSettings } from "../contexts/SettingsContext";
import { testing } from "../lib/testing";
export default function Testing() {
// contexts
const router = useRouter();
const { settings, settingsLoading, updateSettings, resetSettings } = useSettings();
const paths = usePathStore((state) => state.paths);
// states
const [text, setText] = useState("");
return (
@ -23,11 +23,43 @@ export default function Testing() {
<BugReport />
</IconButton>
<Button
onClick={async () => {
console.log(paths);
onClick={() => {
resetSettings();
}}
>
Button
reset settings
</Button>
<Button
onClick={() => {
updateSettings({
display: {
dark_mode: false 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,
},
});
}}
>
update settings
</Button>
<Button
onClick={() => {
console.log(settings);
}}
>
log settings
</Button>
<Button
onClick={() => {
console.log(testing());
}}
>
testing
</Button>
<Typography>{text}</Typography>
<TextField rows={10} />