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:
+100
-2
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user