feat: add tabbed Settings, 10 themes with custom accent/text color pickers, main project directory selector, and export filename/directory controls

This commit is contained in:
Schwitzz
2026-10-30 14:06:46 -05:00
parent fa22a755ee
commit 1d209fedd2
8 changed files with 1660 additions and 648 deletions
+100 -2
View File
@@ -1,7 +1,15 @@
import { execFileSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import {
existsSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import { homedir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { basename, dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import Fastify from "fastify";
import cors from "@fastify/cors";
@@ -80,10 +88,21 @@ export async function buildApp() {
const systemdUserDir = join(homedir(), ".config", "systemd", "user");
const systemdServicePath = join(systemdUserDir, "pubscript-server.service");
const defaultProjectDir = join(homedir(), "Documents", "PubScript");
function expandTilde(inputPath: string): string {
const trimmed = inputPath.trim();
if (!trimmed) return defaultProjectDir;
if (trimmed === "~") return homedir();
if (trimmed.startsWith("~/")) return join(homedir(), trimmed.slice(2));
return resolve(trimmed);
}
interface PersistedServerSysConfig {
enabled: boolean;
port: number;
startOnBoot: boolean;
projectDirectory: string;
}
function loadPersistedServerSysConfig(): PersistedServerSysConfig {
@@ -100,6 +119,10 @@ export async function buildApp() {
typeof raw.startOnBoot === "boolean"
? raw.startOnBoot
: existsSync(autostartDesktopPath) || existsSync(systemdServicePath),
projectDirectory:
typeof raw.projectDirectory === "string" && raw.projectDirectory.trim()
? raw.projectDirectory.trim()
: defaultProjectDir,
};
}
} catch {
@@ -109,6 +132,7 @@ export async function buildApp() {
enabled: true,
port: config.port,
startOnBoot: existsSync(autostartDesktopPath) || existsSync(systemdServicePath),
projectDirectory: defaultProjectDir,
};
}
@@ -116,6 +140,11 @@ export async function buildApp() {
try {
mkdirSync(sysConfigDir, { recursive: true });
writeFileSync(sysConfigPath, JSON.stringify(cfg, null, 2), "utf8");
try {
mkdirSync(expandTilde(cfg.projectDirectory), { recursive: true });
} catch {
/* ignore if read-only */
}
const dataDir = join(homedir(), ".local", "share", "pubscript");
const dbFile = join(dataDir, "pubscript.sqlite");
@@ -197,6 +226,7 @@ export async function buildApp() {
port: persisted.port,
activePort: config.port,
startOnBoot: persisted.startOnBoot,
projectDirectory: persisted.projectDirectory,
autostartPath: autostartDesktopPath,
systemdServicePath,
};
@@ -210,11 +240,16 @@ export async function buildApp() {
if (!Number.isFinite(nextPort) || nextPort < 1024 || nextPort > 65535) {
return reply.code(400).send({ error: "invalid_port" });
}
const nextProjectDir =
typeof body.projectDirectory === "string" && body.projectDirectory.trim()
? body.projectDirectory.trim()
: current.projectDirectory;
const next: PersistedServerSysConfig = {
enabled: typeof body.enabled === "boolean" ? body.enabled : current.enabled,
port: nextPort,
startOnBoot:
typeof body.startOnBoot === "boolean" ? body.startOnBoot : current.startOnBoot,
projectDirectory: nextProjectDir,
};
const configured = applySystemStartupFiles(next);
return {
@@ -223,12 +258,75 @@ export async function buildApp() {
port: next.port,
activePort: config.port,
startOnBoot: next.startOnBoot,
projectDirectory: next.projectDirectory,
autostartConfigured: configured,
autostartPath: autostartDesktopPath,
systemdServicePath,
};
});
app.get("/api/system/list-dirs", async (req) => {
const query = (req.query ?? {}) as { path?: string };
const rawTarget = query.path ? expandTilde(query.path) : homedir();
const target = existsSync(rawTarget) ? rawTarget : homedir();
const parentPath = dirname(target);
const directories: { name: string; path: string }[] = [];
try {
const entries = readdirSync(target, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith(".")) continue;
try {
const full = join(target, entry.name);
if (entry.isDirectory() || (entry.isSymbolicLink() && statSync(full).isDirectory())) {
directories.push({ name: entry.name, path: full });
}
} catch {
/* skip unreadable entry */
}
}
directories.sort((a, b) => a.name.localeCompare(b.name));
} catch {
/* ignore */
}
return {
ok: true,
currentPath: target,
parentPath: parentPath !== target ? parentPath : null,
homePath: homedir(),
directories,
};
});
app.post("/api/system/save-export", async (req, reply) => {
const body = (req.body ?? {}) as {
directory?: string;
fileName?: string;
base64Data?: string;
};
const safeName = basename((body.fileName ?? "").trim());
if (!safeName) {
return reply.code(400).send({ error: "invalid_file_name" });
}
if (typeof body.base64Data !== "string") {
return reply.code(400).send({ error: "missing_data" });
}
const targetDir = expandTilde(
body.directory?.trim() || loadPersistedServerSysConfig().projectDirectory,
);
try {
mkdirSync(targetDir, { recursive: true });
const fullPath = join(targetDir, safeName);
const buf = Buffer.from(body.base64Data, "base64");
writeFileSync(fullPath, buf);
return { ok: true, savedPath: fullPath };
} catch (err) {
return reply.code(500).send({
error: "save_failed",
message: err instanceof Error ? err.message : String(err),
});
}
});
registerAuthHook(app);
registerAuthRoutes(app);
registerRoutes(app);