293 lines
8.4 KiB
TypeScript
293 lines
8.4 KiB
TypeScript
import {
|
|
exists,
|
|
mkdir,
|
|
readTextFile,
|
|
writeTextFile,
|
|
readDir,
|
|
remove,
|
|
copyFile,
|
|
} from "@tauri-apps/plugin-fs";
|
|
import { appDataDir, join } from "@tauri-apps/api/path";
|
|
import { boardSchema, appSettingsSchema } from "./schemas";
|
|
import type { Board, BoardMeta } from "@/types/board";
|
|
import type { AppSettings } from "@/types/settings";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Path helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
async function getBaseDir(): Promise<string> {
|
|
const base = await appDataDir();
|
|
return join(base, "openpylon");
|
|
}
|
|
|
|
async function getBoardsDir(): Promise<string> {
|
|
const base = await getBaseDir();
|
|
return join(base, "boards");
|
|
}
|
|
|
|
async function getAttachmentsDir(): Promise<string> {
|
|
const base = await getBaseDir();
|
|
return join(base, "attachments");
|
|
}
|
|
|
|
async function getSettingsPath(): Promise<string> {
|
|
const base = await getBaseDir();
|
|
return join(base, "settings.json");
|
|
}
|
|
|
|
function boardFilePath(boardsDir: string, boardId: string): Promise<string> {
|
|
return join(boardsDir, `${boardId}.json`);
|
|
}
|
|
|
|
function boardBackupPath(boardsDir: string, boardId: string): Promise<string> {
|
|
return join(boardsDir, `${boardId}.backup.json`);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Directory setup
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export async function ensureDataDirs(): Promise<void> {
|
|
const base = await getBaseDir();
|
|
if (!(await exists(base))) {
|
|
await mkdir(base, { recursive: true });
|
|
}
|
|
|
|
const boardsDir = await getBoardsDir();
|
|
if (!(await exists(boardsDir))) {
|
|
await mkdir(boardsDir, { recursive: true });
|
|
}
|
|
|
|
const attachmentsDir = await getAttachmentsDir();
|
|
if (!(await exists(attachmentsDir))) {
|
|
await mkdir(attachmentsDir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Settings
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export async function loadSettings(): Promise<AppSettings> {
|
|
const settingsPath = await getSettingsPath();
|
|
|
|
if (!(await exists(settingsPath))) {
|
|
const defaults = appSettingsSchema.parse({});
|
|
return defaults;
|
|
}
|
|
|
|
try {
|
|
const raw = await readTextFile(settingsPath);
|
|
const data = JSON.parse(raw);
|
|
return appSettingsSchema.parse(data);
|
|
} catch {
|
|
// If the file is corrupted, return defaults
|
|
return appSettingsSchema.parse({});
|
|
}
|
|
}
|
|
|
|
export async function saveSettings(settings: AppSettings): Promise<void> {
|
|
const settingsPath = await getSettingsPath();
|
|
await writeTextFile(settingsPath, JSON.stringify(settings, null, 2));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Board listing
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export async function listBoards(): Promise<BoardMeta[]> {
|
|
const boardsDir = await getBoardsDir();
|
|
|
|
if (!(await exists(boardsDir))) {
|
|
return [];
|
|
}
|
|
|
|
const entries = await readDir(boardsDir);
|
|
const metas: BoardMeta[] = [];
|
|
|
|
for (const entry of entries) {
|
|
// DirEntry has `name` property in Tauri v2
|
|
if (!entry.name || !entry.name.endsWith(".json") || entry.name.endsWith(".backup.json")) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
const filePath = await join(boardsDir, entry.name);
|
|
const raw = await readTextFile(filePath);
|
|
const data = JSON.parse(raw);
|
|
const board = boardSchema.parse(data);
|
|
|
|
metas.push({
|
|
id: board.id,
|
|
title: board.title,
|
|
color: board.color,
|
|
cardCount: Object.keys(board.cards).length,
|
|
columnCount: board.columns.length,
|
|
updatedAt: board.updatedAt,
|
|
});
|
|
} catch {
|
|
// Skip corrupted board files
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Sort by updatedAt descending (most recent first)
|
|
metas.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
return metas;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Board CRUD
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export async function loadBoard(boardId: string): Promise<Board> {
|
|
const boardsDir = await getBoardsDir();
|
|
const filePath = await boardFilePath(boardsDir, boardId);
|
|
|
|
const raw = await readTextFile(filePath);
|
|
const data = JSON.parse(raw);
|
|
return boardSchema.parse(data) as Board;
|
|
}
|
|
|
|
export async function saveBoard(board: Board): Promise<void> {
|
|
const boardsDir = await getBoardsDir();
|
|
const filePath = await boardFilePath(boardsDir, board.id);
|
|
const backupPath = await boardBackupPath(boardsDir, board.id);
|
|
|
|
// Rotate previous version to backup
|
|
if (await exists(filePath)) {
|
|
try {
|
|
const previous = await readTextFile(filePath);
|
|
await writeTextFile(backupPath, previous);
|
|
} catch {
|
|
// If we can't create a backup, continue saving anyway
|
|
}
|
|
}
|
|
|
|
await writeTextFile(filePath, JSON.stringify(board, null, 2));
|
|
}
|
|
|
|
export async function deleteBoard(boardId: string): Promise<void> {
|
|
const boardsDir = await getBoardsDir();
|
|
const filePath = await boardFilePath(boardsDir, boardId);
|
|
const backupPath = await boardBackupPath(boardsDir, boardId);
|
|
|
|
// Remove board file
|
|
if (await exists(filePath)) {
|
|
await remove(filePath);
|
|
}
|
|
|
|
// Remove backup file
|
|
if (await exists(backupPath)) {
|
|
await remove(backupPath);
|
|
}
|
|
|
|
// Remove board attachments directory
|
|
const attachmentsDir = await getAttachmentsDir();
|
|
const boardAttachmentsDir = await join(attachmentsDir, boardId);
|
|
if (await exists(boardAttachmentsDir)) {
|
|
await remove(boardAttachmentsDir, { recursive: true });
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Backup restoration
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export async function restoreFromBackup(boardId: string): Promise<Board> {
|
|
const boardsDir = await getBoardsDir();
|
|
const backupPath = await boardBackupPath(boardsDir, boardId);
|
|
|
|
const raw = await readTextFile(backupPath);
|
|
const data = JSON.parse(raw);
|
|
const board = boardSchema.parse(data) as Board;
|
|
|
|
// Save the restored board as the current version
|
|
await saveBoard(board);
|
|
|
|
return board;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Search
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export interface SearchResult {
|
|
boardId: string;
|
|
boardTitle: string;
|
|
cardId: string;
|
|
cardTitle: string;
|
|
matchField: "title" | "description";
|
|
}
|
|
|
|
export async function searchAllBoards(query: string): Promise<SearchResult[]> {
|
|
const boardsDir = await getBoardsDir();
|
|
|
|
if (!(await exists(boardsDir))) {
|
|
return [];
|
|
}
|
|
|
|
const entries = await readDir(boardsDir);
|
|
const results: SearchResult[] = [];
|
|
const lowerQuery = query.toLowerCase();
|
|
|
|
for (const entry of entries) {
|
|
if (!entry.name || !entry.name.endsWith(".json") || entry.name.endsWith(".backup.json")) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
const filePath = await join(boardsDir, entry.name);
|
|
const raw = await readTextFile(filePath);
|
|
const data = JSON.parse(raw);
|
|
const board = boardSchema.parse(data);
|
|
|
|
for (const card of Object.values(board.cards)) {
|
|
if (card.title.toLowerCase().includes(lowerQuery)) {
|
|
results.push({
|
|
boardId: board.id,
|
|
boardTitle: board.title,
|
|
cardId: card.id,
|
|
cardTitle: card.title,
|
|
matchField: "title",
|
|
});
|
|
} else if (card.description.toLowerCase().includes(lowerQuery)) {
|
|
results.push({
|
|
boardId: board.id,
|
|
boardTitle: board.title,
|
|
cardId: card.id,
|
|
cardTitle: card.title,
|
|
matchField: "description",
|
|
});
|
|
}
|
|
}
|
|
} catch {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Attachment helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export async function copyAttachment(
|
|
boardId: string,
|
|
sourcePath: string,
|
|
fileName: string,
|
|
): Promise<string> {
|
|
const attachmentsDir = await getAttachmentsDir();
|
|
const boardAttDir = await join(attachmentsDir, boardId);
|
|
|
|
if (!(await exists(boardAttDir))) {
|
|
await mkdir(boardAttDir, { recursive: true });
|
|
}
|
|
|
|
const destPath = await join(boardAttDir, fileName);
|
|
await copyFile(sourcePath, destPath);
|
|
return destPath;
|
|
}
|