Files
all-in-one-app/electron/main.cjs
2026-07-17 10:00:33 +01:00

900 lines
27 KiB
JavaScript

const { app, BrowserWindow, Menu, dialog, ipcMain, shell } = require("electron");
const crypto = require("crypto");
const fs = require("fs/promises");
const fsSync = require("fs");
const path = require("path");
const ICAL = require("ical.js");
const TASKS_FILE = "Tasks.md";
const NOTES_FOLDER = "Notes";
const APP_FOLDER = ".all-in-one";
const SETTINGS_FILE = "settings.json";
const CACHE_FILE = "calendar-cache.json";
const CALENDAR_CACHE_TTL_MS = 15 * 60 * 1000;
const DEFAULT_DAILY_TEMPLATE = "# {{dateLong}}\n\n## Notes\n\n";
const DEFAULT_THEME_COLORS = {
light: {
background: "#f6f7fb",
surface: "#ffffff",
text: "#20232a",
mutedText: "#647084",
border: "#d9dee8",
sidebar: "#24262d",
accent: "#0f766e"
},
dark: {
background: "#0f172a",
surface: "#111827",
text: "#e5edf5",
mutedText: "#94a3b8",
border: "#273449",
sidebar: "#111827",
accent: "#0f766e"
}
};
const DEFAULT_SETTINGS = {
calendars: [],
dailyNoteTemplate: DEFAULT_DAILY_TEMPLATE,
theme: "light",
themeColors: DEFAULT_THEME_COLORS
};
let mainWindow;
let state = {};
let vaultPath = "";
function userStatePath() {
return path.join(app.getPath("userData"), "state.json");
}
function defaultVaultPath() {
return path.join(app.getPath("documents"), "All-in-One Vault");
}
function appVaultPath(...segments) {
return path.join(vaultPath, APP_FOLDER, ...segments);
}
async function readJson(filePath, fallback) {
try {
const raw = await fs.readFile(filePath, "utf8");
return JSON.parse(raw);
} catch {
return fallback;
}
}
async function writeJson(filePath, data) {
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
}
async function loadState() {
state = await readJson(userStatePath(), {});
vaultPath = state.vaultPath || defaultVaultPath();
state.vaultPath = vaultPath;
await writeJson(userStatePath(), state);
}
async function saveState(patch) {
state = { ...state, ...patch };
vaultPath = state.vaultPath;
await writeJson(userStatePath(), state);
}
async function ensureFile(filePath, content) {
if (!fsSync.existsSync(filePath)) {
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, content, "utf8");
}
}
async function ensureVault() {
await fs.mkdir(vaultPath, { recursive: true });
await fs.mkdir(path.join(vaultPath, "Daily"), { recursive: true });
await fs.mkdir(path.join(vaultPath, NOTES_FOLDER), { recursive: true });
await fs.mkdir(appVaultPath(), { recursive: true });
await ensureFile(path.join(vaultPath, TASKS_FILE), "# Tasks\n\n");
await ensureFile(appVaultPath(SETTINGS_FILE), `${JSON.stringify(DEFAULT_SETTINGS, null, 2)}\n`);
}
function dateFromISO(dateISO) {
const [year, month, day] = dateISO.split("-").map(Number);
return new Date(year, month - 1, day);
}
function toISODate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
function addDays(date, days) {
const next = new Date(date);
next.setDate(next.getDate() + days);
return next;
}
function dayBounds(dateISO) {
const start = dateFromISO(dateISO);
const end = addDays(start, 1);
return { start, end };
}
function rangeBounds(startISO, endISO) {
const start = dateFromISO(startISO);
const end = addDays(dateFromISO(endISO), 1);
return { start, end };
}
function dateLabel(dateISO) {
return new Intl.DateTimeFormat(undefined, {
weekday: "long",
month: "long",
day: "numeric",
year: "numeric"
}).format(dateFromISO(dateISO));
}
function weekdayLabel(dateISO) {
return new Intl.DateTimeFormat(undefined, { weekday: "long" }).format(dateFromISO(dateISO));
}
function dailyNotePath(dateISO) {
return path.join(vaultPath, "Daily", `${dateISO}.md`);
}
function normalizeTags(input) {
const raw = Array.isArray(input) ? input : String(input || "").split(",");
const seen = new Set();
return raw
.map((tag) =>
String(tag || "")
.trim()
.replace(/^#+/, "")
.replace(/\s+/g, "-")
.slice(0, 48)
)
.filter(Boolean)
.filter((tag) => {
const key = tag.toLowerCase();
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
function normalizeProject(value) {
return String(value || "").replace(/\s+/g, " ").trim().slice(0, 100);
}
function stripNoteFrontmatter(content) {
const match = String(content || "").match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
return match ? content.slice(match[0].length) : content;
}
function parseNoteFrontmatter(content) {
const text = String(content || "");
const metadata = { tags: [], project: "" };
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
if (!match) {
return { metadata, body: text };
}
const lines = match[1].split(/\r?\n/);
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index];
const tagsInline = line.match(/^tags:\s*\[(.*)]\s*$/);
const tagsValue = line.match(/^tags:\s*(.+)\s*$/);
const projectValue = line.match(/^project:\s*(.*)\s*$/);
if (tagsInline) {
metadata.tags = normalizeTags(tagsInline[1]);
continue;
}
if (tagsValue && tagsValue[1].trim()) {
metadata.tags = normalizeTags(tagsValue[1]);
continue;
}
if (line.match(/^tags:\s*$/)) {
const tags = [];
while (lines[index + 1]?.match(/^\s*-\s+/)) {
index += 1;
tags.push(lines[index].replace(/^\s*-\s+/, ""));
}
metadata.tags = normalizeTags(tags);
continue;
}
if (projectValue) {
metadata.project = normalizeProject(projectValue[1].replace(/^["']|["']$/g, ""));
}
}
return {
metadata,
body: text.slice(match[0].length)
};
}
function serializeNoteFrontmatter(metadata) {
const tags = normalizeTags(metadata.tags);
const project = normalizeProject(metadata.project);
const lines = ["---"];
if (tags.length) {
lines.push("tags:");
tags.forEach((tag) => lines.push(` - ${tag}`));
}
if (project) {
lines.push(`project: ${project}`);
}
lines.push("---");
return `${lines.join("\n")}\n`;
}
function applyNoteMetadata(content, metadata) {
const parsed = parseNoteFrontmatter(content);
const next = {
tags: Object.prototype.hasOwnProperty.call(metadata, "tags") ? normalizeTags(metadata.tags) : parsed.metadata.tags,
project: Object.prototype.hasOwnProperty.call(metadata, "project")
? normalizeProject(metadata.project)
: parsed.metadata.project
};
if (!next.tags.length && !next.project) {
return parsed.body;
}
return `${serializeNoteFrontmatter(next)}${parsed.body.replace(/^\s+/, "")}`;
}
function renderDailyTemplate(template, dateISO) {
return String(template || DEFAULT_DAILY_TEMPLATE)
.replaceAll("{{date}}", dateISO)
.replaceAll("{{dateLong}}", dateLabel(dateISO))
.replaceAll("{{weekday}}", weekdayLabel(dateISO));
}
async function readDailyNote(dateISO) {
const filePath = dailyNotePath(dateISO);
const settings = await getSettings();
await ensureFile(filePath, renderDailyTemplate(settings.dailyNoteTemplate, dateISO));
const content = await fs.readFile(filePath, "utf8");
const parsed = parseNoteFrontmatter(content);
return {
dateISO,
filePath,
content,
tags: parsed.metadata.tags,
project: parsed.metadata.project
};
}
async function saveDailyNote(dateISO, content) {
const filePath = dailyNotePath(dateISO);
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, content, "utf8");
return readDailyNote(dateISO);
}
async function updateDailyNoteMetadata(dateISO, metadata) {
const filePath = dailyNotePath(dateISO);
const current = (await readDailyNote(dateISO)).content;
await fs.writeFile(filePath, applyNoteMetadata(current, metadata), "utf8");
return readDailyNote(dateISO);
}
function generalNotesPath(...segments) {
return path.join(vaultPath, NOTES_FOLDER, ...segments);
}
function noteTitleFromContent(content, fallback) {
const heading = stripNoteFrontmatter(content)
.split(/\r?\n/)
.find((line) => line.trim().startsWith("# "));
return (heading ? heading.replace(/^#\s+/, "").trim() : "") || fallback || "Untitled note";
}
function slugifyTitle(title) {
const slug = String(title || "")
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 48);
return slug || "untitled-note";
}
function safeGeneralNoteId(id) {
const value = String(id || "");
const baseName = path.basename(value);
if (value !== baseName || !baseName.toLowerCase().endsWith(".md")) {
throw new Error("Invalid note id.");
}
return baseName;
}
function publicGeneralNote(id, filePath, content, stats) {
const fallbackTitle = path.basename(id, ".md").replace(/-[a-f0-9]{8}$/, "").replace(/-/g, " ");
const parsed = parseNoteFrontmatter(content);
return {
id,
title: noteTitleFromContent(content, fallbackTitle),
filePath,
updatedAt: stats.mtime.toISOString(),
tags: parsed.metadata.tags,
project: parsed.metadata.project
};
}
async function listGeneralNotes() {
await fs.mkdir(generalNotesPath(), { recursive: true });
const entries = await fs.readdir(generalNotesPath(), { withFileTypes: true });
const notes = [];
for (const entry of entries) {
if (!entry.isFile() || !entry.name.toLowerCase().endsWith(".md")) continue;
const filePath = generalNotesPath(entry.name);
const [content, stats] = await Promise.all([fs.readFile(filePath, "utf8"), fs.stat(filePath)]);
notes.push(publicGeneralNote(entry.name, filePath, content, stats));
}
return notes.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
}
async function readGeneralNote(id) {
const noteId = safeGeneralNoteId(id);
const filePath = generalNotesPath(noteId);
const [content, stats] = await Promise.all([fs.readFile(filePath, "utf8"), fs.stat(filePath)]);
return {
...publicGeneralNote(noteId, filePath, content, stats),
content
};
}
async function createGeneralNote(title) {
const cleanTitle = String(title || "").trim().slice(0, 120) || "Untitled note";
const id = `${slugifyTitle(cleanTitle)}-${generateId().slice(0, 8)}.md`;
const filePath = generalNotesPath(id);
await fs.writeFile(filePath, `# ${cleanTitle}\n\n`, "utf8");
return readGeneralNote(id);
}
async function saveGeneralNote(id, content) {
const noteId = safeGeneralNoteId(id);
const filePath = generalNotesPath(noteId);
await fs.writeFile(filePath, content || "", "utf8");
return readGeneralNote(noteId);
}
async function updateGeneralNoteMetadata(id, metadata) {
const noteId = safeGeneralNoteId(id);
const filePath = generalNotesPath(noteId);
const current = await fs.readFile(filePath, "utf8");
await fs.writeFile(filePath, applyNoteMetadata(current, metadata), "utf8");
return readGeneralNote(noteId);
}
function tasksPath() {
return path.join(vaultPath, TASKS_FILE);
}
function generateId() {
return crypto.randomBytes(8).toString("hex");
}
function parseMeta(rawMeta = "") {
return rawMeta
.trim()
.split(/\s+/)
.filter(Boolean)
.reduce((meta, part) => {
const separator = part.indexOf("=");
if (separator > 0) {
meta[part.slice(0, separator)] = part.slice(separator + 1);
}
return meta;
}, {});
}
function encodeMetaValue(value) {
return encodeURIComponent(String(value || ""));
}
function decodeMetaValue(value) {
try {
return decodeURIComponent(String(value || ""));
} catch {
return String(value || "");
}
}
function cleanTaskTitle(title) {
return title
.replace(/<!--[\s\S]*?-->/g, "")
.replace(/\s+/g, " ")
.trim()
.slice(0, 300);
}
function serializeTask(task) {
const checked = task.completed ? "x" : " ";
const meta = [`id=${task.id}`, `created=${task.createdAt || new Date().toISOString()}`];
if (task.dueDate) meta.push(`due=${task.dueDate}`);
if (task.completedAt) meta.push(`completed=${task.completedAt}`);
if (task.tags?.length) meta.push(`tags=${encodeMetaValue(task.tags.join(","))}`);
if (task.project) meta.push(`project=${encodeMetaValue(task.project)}`);
return `- [${checked}] ${cleanTaskTitle(task.title) || "Untitled task"} <!-- aio:${meta.join(" ")} -->`;
}
function parseTaskLine(line, lineIndex) {
const match = line.match(/^(\s*)-\s+\[([ xX])\]\s+(.*?)(?:\s*<!--\s*aio:([^>]*)-->)?\s*$/);
if (!match) return null;
const meta = parseMeta(match[4]);
const hasId = Boolean(meta.id);
return {
id: meta.id || generateId(),
title: cleanTaskTitle(match[3]) || "Untitled task",
completed: match[2].toLowerCase() === "x",
dueDate: /^\d{4}-\d{2}-\d{2}$/.test(meta.due || "") ? meta.due : "",
createdAt: meta.created || new Date().toISOString(),
completedAt: meta.completed || "",
tags: normalizeTags(decodeMetaValue(meta.tags || "")),
project: normalizeProject(decodeMetaValue(meta.project || "")),
lineIndex,
needsRewrite: !hasId
};
}
async function readTaskDocument() {
await ensureFile(tasksPath(), "# Tasks\n\n");
const markdown = await fs.readFile(tasksPath(), "utf8");
const lines = markdown.split(/\r?\n/);
const tasks = [];
let needsRewrite = false;
lines.forEach((line, lineIndex) => {
const task = parseTaskLine(line, lineIndex);
if (task) {
tasks.push(task);
needsRewrite = needsRewrite || task.needsRewrite;
}
});
if (needsRewrite) {
tasks.forEach((task) => {
lines[task.lineIndex] = serializeTask(task);
});
await fs.writeFile(tasksPath(), `${lines.join("\n").replace(/\s*$/, "")}\n`, "utf8");
}
return { lines, tasks };
}
function publicTask(task) {
return {
id: task.id,
title: task.title,
completed: task.completed,
dueDate: task.dueDate,
createdAt: task.createdAt,
completedAt: task.completedAt,
tags: task.tags || [],
project: task.project || ""
};
}
function sortTasks(tasks) {
return [...tasks].sort((a, b) => {
if (a.completed !== b.completed) return a.completed ? 1 : -1;
if (a.dueDate && b.dueDate && a.dueDate !== b.dueDate) return a.dueDate.localeCompare(b.dueDate);
if (a.dueDate && !b.dueDate) return -1;
if (!a.dueDate && b.dueDate) return 1;
return a.createdAt.localeCompare(b.createdAt);
});
}
async function listTasks() {
const { tasks } = await readTaskDocument();
return sortTasks(tasks.map(publicTask));
}
async function addTask(title, dueDate, tags = [], project = "") {
const cleanTitle = cleanTaskTitle(title);
if (!cleanTitle) throw new Error("Task title is required.");
const task = {
id: generateId(),
title: cleanTitle,
completed: false,
dueDate: /^\d{4}-\d{2}-\d{2}$/.test(dueDate || "") ? dueDate : "",
createdAt: new Date().toISOString(),
completedAt: "",
tags: normalizeTags(tags),
project: normalizeProject(project)
};
const markdown = await fs.readFile(tasksPath(), "utf8");
const nextMarkdown = `${markdown.replace(/\s*$/, "")}\n${serializeTask(task)}\n`;
await fs.writeFile(tasksPath(), nextMarkdown, "utf8");
return listTasks();
}
async function updateTask(id, patch) {
const { lines, tasks } = await readTaskDocument();
const task = tasks.find((item) => item.id === id);
if (!task) return listTasks();
const hasCompletedPatch = Object.prototype.hasOwnProperty.call(patch, "completed");
const completed = hasCompletedPatch ? Boolean(patch.completed) : task.completed;
const nextTask = {
...task,
...patch,
completed,
tags: Object.prototype.hasOwnProperty.call(patch, "tags") ? normalizeTags(patch.tags) : task.tags,
project: Object.prototype.hasOwnProperty.call(patch, "project") ? normalizeProject(patch.project) : task.project,
completedAt: hasCompletedPatch ? (completed ? new Date().toISOString() : "") : task.completedAt
};
lines[task.lineIndex] = serializeTask(nextTask);
await fs.writeFile(tasksPath(), `${lines.join("\n").replace(/\s*$/, "")}\n`, "utf8");
return listTasks();
}
async function deleteTask(id) {
const { lines, tasks } = await readTaskDocument();
const task = tasks.find((item) => item.id === id);
if (!task) return listTasks();
lines.splice(task.lineIndex, 1);
await fs.writeFile(tasksPath(), `${lines.join("\n").replace(/\s*$/, "")}\n`, "utf8");
return listTasks();
}
function ensureCalendarUrl(value) {
const trimmed = String(value || "").trim();
if (!trimmed) return "";
try {
const url = new URL(trimmed);
if (!["http:", "https:"].includes(url.protocol)) throw new Error("Invalid protocol");
return url.toString();
} catch {
throw new Error("Calendar feed must be a valid http or https URL.");
}
}
function normalizeCalendarSource(calendar, index) {
const url = ensureCalendarUrl(calendar?.url || calendar?.calendarFeedUrl || "");
if (!url) return null;
return {
id: String(calendar?.id || generateId()).replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 32) || generateId(),
name: String(calendar?.name || `Calendar ${index + 1}`).replace(/\s+/g, " ").trim().slice(0, 80),
url
};
}
function normalizeSettings(raw) {
const migratedCalendars =
Array.isArray(raw?.calendars)
? raw.calendars
: raw?.calendarFeedUrl
? [{ id: "primary", name: "Google Calendar", url: raw.calendarFeedUrl }]
: [];
return {
calendars: migratedCalendars
.map((calendar, index) => normalizeCalendarSource(calendar, index))
.filter(Boolean),
dailyNoteTemplate: String(raw?.dailyNoteTemplate || DEFAULT_DAILY_TEMPLATE),
theme: raw?.theme === "dark" ? "dark" : "light",
themeColors: normalizeThemeColors(raw?.themeColors)
};
}
function normalizeHexColor(value, fallback) {
const color = String(value || "").trim();
return /^#[0-9a-fA-F]{6}$/.test(color) ? color : fallback;
}
function normalizeThemePalette(raw, defaults) {
return Object.fromEntries(
Object.entries(defaults).map(([key, fallback]) => [key, normalizeHexColor(raw?.[key], fallback)])
);
}
function normalizeThemeColors(raw) {
return {
light: normalizeThemePalette(raw?.light, DEFAULT_THEME_COLORS.light),
dark: normalizeThemePalette(raw?.dark, DEFAULT_THEME_COLORS.dark)
};
}
async function getSettings() {
const raw = await readJson(appVaultPath(SETTINGS_FILE), DEFAULT_SETTINGS);
const settings = normalizeSettings(raw);
return {
...settings,
calendarFeedUrl: settings.calendars[0]?.url || "",
vaultPath
};
}
async function updateSettings(patch) {
const current = await getSettings();
const next = normalizeSettings({
calendars: Object.prototype.hasOwnProperty.call(patch, "calendars") ? patch.calendars : current.calendars,
calendarFeedUrl: Object.prototype.hasOwnProperty.call(patch, "calendarFeedUrl")
? patch.calendarFeedUrl
: current.calendarFeedUrl,
dailyNoteTemplate: Object.prototype.hasOwnProperty.call(patch, "dailyNoteTemplate")
? patch.dailyNoteTemplate
: current.dailyNoteTemplate,
theme: Object.prototype.hasOwnProperty.call(patch, "theme") ? patch.theme : current.theme,
themeColors: Object.prototype.hasOwnProperty.call(patch, "themeColors") ? patch.themeColors : current.themeColors
});
await writeJson(appVaultPath(SETTINGS_FILE), next);
return getSettings();
}
function toDate(value) {
return value ? value.toJSDate() : null;
}
function overlaps(start, end, rangeStart, rangeEnd) {
return start < rangeEnd && end > rangeStart;
}
function normalizeCalendarEvent(event, startDate, endDate, occurrenceKey, calendar) {
const start = toDate(startDate);
const end = toDate(endDate) || addDays(start, startDate.isDate ? 1 : 0);
return {
id: `${calendar.id}:${event.uid || event.summary || "event"}:${occurrenceKey || start.toISOString()}`,
title: event.summary || "Untitled event",
startsAt: start.toISOString(),
endsAt: end.toISOString(),
allDay: Boolean(startDate.isDate),
location: event.location || "",
description: event.description || "",
calendarId: calendar.id,
calendarName: calendar.name
};
}
function parseCalendarEvents(ics, rangeStart, rangeEnd, calendar) {
const jcal = ICAL.parse(ics);
const component = new ICAL.Component(jcal);
const vevents = component.getAllSubcomponents("vevent");
const events = [];
for (const vevent of vevents) {
const event = new ICAL.Event(vevent);
if (event.isRecurring()) {
const iterator = event.iterator();
let next;
let count = 0;
while ((next = iterator.next()) && count < 5000) {
count += 1;
const details = event.getOccurrenceDetails(next);
const start = toDate(details.startDate);
const end = toDate(details.endDate) || addDays(start, details.startDate.isDate ? 1 : 0);
if (start >= rangeEnd) break;
if (overlaps(start, end, rangeStart, rangeEnd)) {
events.push(normalizeCalendarEvent(event, details.startDate, details.endDate, next.toString(), calendar));
}
}
continue;
}
const start = toDate(event.startDate);
const end = toDate(event.endDate) || addDays(start, event.startDate.isDate ? 1 : 0);
if (overlaps(start, end, rangeStart, rangeEnd)) {
events.push(normalizeCalendarEvent(event, event.startDate, event.endDate, undefined, calendar));
}
}
return events.sort((a, b) => a.startsAt.localeCompare(b.startsAt));
}
async function getCalendarIcs(calendar, force = false) {
const cachePath = appVaultPath(CACHE_FILE);
const cache = await readJson(cachePath, { calendars: {} });
const cachedCalendar = cache.calendars?.[calendar.id];
const cacheIsFresh =
cachedCalendar?.ics &&
cachedCalendar?.syncedAt &&
Date.now() - Date.parse(cachedCalendar.syncedAt) < CALENDAR_CACHE_TTL_MS;
if (!force && cacheIsFresh) {
return { ics: cachedCalendar.ics, syncedAt: cachedCalendar.syncedAt, status: "cached" };
}
const response = await fetch(calendar.url);
if (!response.ok) {
throw new Error(`${calendar.name} returned ${response.status}.`);
}
const ics = await response.text();
const syncedAt = new Date().toISOString();
await writeJson(cachePath, {
calendars: {
...(cache.calendars || {}),
[calendar.id]: { ics, syncedAt }
}
});
return { ics, syncedAt, status: "fresh" };
}
async function listCalendarEventsInRange(startISO, endISO, force = false) {
const { start, end } = rangeBounds(startISO, endISO);
const settings = await getSettings();
if (!settings.calendars.length) {
return { events: [], syncedAt: "", status: "not_configured", error: "" };
}
const events = [];
const syncedAtValues = [];
const statuses = [];
const errors = [];
for (const calendar of settings.calendars) {
try {
const feed = await getCalendarIcs(calendar, force);
syncedAtValues.push(feed.syncedAt);
statuses.push(feed.status);
events.push(...parseCalendarEvents(feed.ics, start, end, calendar));
} catch (error) {
statuses.push("error");
errors.push(error.message || `Could not load ${calendar.name}.`);
}
}
return {
events: events.sort((a, b) => a.startsAt.localeCompare(b.startsAt)),
syncedAt: syncedAtValues.sort().at(-1) || "",
status: errors.length ? "error" : statuses.includes("fresh") ? "fresh" : "cached",
error: errors.join(" ")
};
}
async function listCalendarEvents(dateISO, force = false) {
return listCalendarEventsInRange(dateISO, dateISO, force);
}
async function loadDashboard(dateISO) {
await ensureVault();
const [settings, note, generalNotes, tasks, calendar] = await Promise.all([
getSettings(),
readDailyNote(dateISO),
listGeneralNotes(),
listTasks(),
listCalendarEvents(dateISO).catch((error) => ({
events: [],
syncedAt: "",
status: "error",
error: error.message || "Could not load calendar."
}))
]);
return { settings, note, generalNotes, tasks, calendar };
}
function createWindow() {
Menu.setApplicationMenu(null);
mainWindow = new BrowserWindow({
width: 1280,
height: 860,
minWidth: 960,
minHeight: 680,
title: "All-in-One Dashboard",
autoHideMenuBar: true,
backgroundColor: "#f6f7fb",
webPreferences: {
preload: path.join(__dirname, "preload.cjs"),
contextIsolation: true,
nodeIntegration: false
}
});
mainWindow.setMenu(null);
mainWindow.setMenuBarVisibility(false);
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: "deny" };
});
if (process.env.VITE_DEV_SERVER_URL) {
mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL);
} else {
mainWindow.loadFile(path.join(__dirname, "..", "dist", "index.html"));
}
}
function registerIpc() {
ipcMain.handle("dashboard:load", (_event, dateISO) => loadDashboard(dateISO || toISODate(new Date())));
ipcMain.handle("notes:save", (_event, payload) => saveDailyNote(payload.dateISO, payload.content || ""));
ipcMain.handle("notes:updateMetadata", (_event, payload) => updateDailyNoteMetadata(payload.dateISO, payload.metadata || {}));
ipcMain.handle("generalNotes:create", (_event, title) => createGeneralNote(title));
ipcMain.handle("generalNotes:read", (_event, id) => readGeneralNote(id));
ipcMain.handle("generalNotes:save", (_event, payload) => saveGeneralNote(payload.id, payload.content || ""));
ipcMain.handle("generalNotes:updateMetadata", (_event, payload) =>
updateGeneralNoteMetadata(payload.id, payload.metadata || {})
);
ipcMain.handle("tasks:add", (_event, payload) => addTask(payload.title, payload.dueDate, payload.tags, payload.project));
ipcMain.handle("tasks:toggle", (_event, payload) => updateTask(payload.id, { completed: payload.completed }));
ipcMain.handle("tasks:updateMetadata", (_event, payload) =>
updateTask(payload.id, { tags: payload.tags || [], project: payload.project || "" })
);
ipcMain.handle("tasks:delete", (_event, id) => deleteTask(id));
ipcMain.handle("settings:update", (_event, patch) => updateSettings(patch || {}));
ipcMain.handle("calendar:list", (_event, payload) =>
listCalendarEventsInRange(payload?.startDate || toISODate(new Date()), payload?.endDate || payload?.startDate || toISODate(new Date()))
);
ipcMain.handle("calendar:refresh", (_event, payload) => {
if (typeof payload === "object" && payload) {
return listCalendarEventsInRange(payload.startDate || toISODate(new Date()), payload.endDate || payload.startDate || toISODate(new Date()), true);
}
return listCalendarEvents(payload || toISODate(new Date()), true);
});
ipcMain.handle("vault:open", () => shell.openPath(vaultPath));
ipcMain.handle("vault:choose", async () => {
const result = await dialog.showOpenDialog(mainWindow, {
title: "Choose Markdown vault",
defaultPath: vaultPath,
properties: ["openDirectory", "createDirectory"]
});
if (result.canceled || !result.filePaths[0]) return getSettings();
await saveState({ vaultPath: result.filePaths[0] });
await ensureVault();
return getSettings();
});
}
app.whenReady().then(async () => {
await loadState();
await ensureVault();
registerIpc();
createWindow();
app.on("activate", () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") app.quit();
});