import { invoke } from '@tauri-apps/api/core'; import { StyleOption, DocxStyleConfig, ElementStyle, FontConfig, ColorPalette } from '../types'; import { generatePreviewCss, generateWordConfig } from './templateRenderer'; interface RawTemplate { id: string; name: string; category?: string; description?: string; vibe?: string; googleFontsImport?: string; typography?: { fonts?: FontConfig; colors?: ColorPalette; }; elements?: Record; page?: Record; wordConfig?: Record; previewCss?: string; [key: string]: any; } interface TemplateFile { name: string; content: string; category: string; // Folder-based category from Rust } /** * Resolves a color reference or returns the color directly */ function resolveColor(color: string | undefined, palette: ColorPalette): string { if (!color) return '#000000'; if (palette[color as keyof ColorPalette]) { return '#' + palette[color as keyof ColorPalette]; } return color.startsWith('#') ? color : '#' + color; } /** * Resolves a font reference or returns the font directly */ function resolveFont(font: string | undefined, fonts: FontConfig): string { if (!font) return 'serif'; if (fonts[font as keyof FontConfig]) { return fonts[font as keyof FontConfig]!; } return font; } /** * Generates preview CSS from unified template structure */ /** /** * Validate that a parsed object conforms to the StyleOption interface */ function validateTemplate(raw: RawTemplate, filename: string): StyleOption | null { if (!raw.id || typeof raw.id !== 'string') { console.error(`Invalid template in ${filename}: missing or invalid 'id'`); return null; } if (!raw.name || typeof raw.name !== 'string') { console.error(`Invalid template in ${filename}: missing or invalid 'name'`); return null; } const hasUnifiedStructure = raw.elements && raw.typography; let wordConfig: StyleOption['wordConfig']; let previewCss: string; if (hasUnifiedStructure) { const fonts = raw.typography?.fonts || {}; const palette = raw.typography?.colors || {}; const elements = raw.elements || {}; // Only generate wordConfig if it's missing or incomplete in the raw template // Prefer the raw wordConfig if it exists (allows manual overrides) if (raw.wordConfig && Object.keys(raw.wordConfig).length > 0) { wordConfig = raw.wordConfig as StyleOption['wordConfig']; } else { // Use the imported generator which now has better logic wordConfig = generateWordConfig(elements, fonts, palette) as StyleOption['wordConfig']; } // Similarly for previewCss - prefer explicit override if (raw.previewCss) { previewCss = raw.previewCss; } else { previewCss = generatePreviewCss({ elements, typography: { fonts, colors: palette } } as StyleOption); } } else { const defaultHeading1: DocxStyleConfig = { font: 'Arial', size: 24, color: '000000', align: 'left', spacing: { before: 240, after: 120, line: 240 } }; const defaultHeading2: DocxStyleConfig = { font: 'Arial', size: 18, color: '333333', align: 'left', spacing: { before: 200, after: 100, line: 240 } }; const defaultBody: DocxStyleConfig = { font: 'Arial', size: 11, color: '333333', align: 'left', spacing: { before: 0, after: 120, line: 276 } }; wordConfig = raw.wordConfig && raw.wordConfig.heading1 && raw.wordConfig.heading2 && raw.wordConfig.body ? { heading1: { ...defaultHeading1, ...raw.wordConfig.heading1 }, heading2: { ...defaultHeading2, ...raw.wordConfig.heading2 }, body: { ...defaultBody, ...raw.wordConfig.body }, accentColor: raw.wordConfig.accentColor || '000000' } : { heading1: defaultHeading1, heading2: defaultHeading2, body: defaultBody, accentColor: '000000' }; previewCss = raw.previewCss || 'font-family: Arial, sans-serif;'; } const template: StyleOption = { id: raw.id, name: raw.name, category: raw.category || 'Other', description: raw.description || '', vibe: raw.vibe || '', googleFontsImport: raw.googleFontsImport || '', wordConfig, previewCss, // Preserve unified template structure if present ...(raw.typography && { typography: raw.typography }), ...(raw.elements && { elements: raw.elements }), ...(raw.page && { page: raw.page }) }; const extraFields = Object.entries(raw).reduce((acc, [key, val]) => { if (!['id', 'name', 'category', 'description', 'vibe', 'googleFontsImport', 'wordConfig', 'previewCss', 'typography', 'elements', 'page'].includes(key)) { acc[key] = val; } return acc; }, {} as Record); return { ...template, ...extraFields }; } /** * Debug function to check paths */ export async function debugPaths(): Promise { try { return await invoke('debug_paths'); } catch (e) { return `Error: ${e}`; } } /** * Load all templates from the portable folder (next to EXE) * Uses Rust backend for file operations since BaseDirectory.Executable doesn't work in JS */ export async function loadTemplates(): Promise<{ templates: StyleOption[]; error?: string }> { const templates: StyleOption[] = []; let error: string | undefined; try { // Use Rust command to read all templates console.log('Calling read_templates command...'); const files = await invoke('read_templates'); console.log(`Received ${files.length} templates from Rust`); for (const file of files) { try { const raw = JSON.parse(file.content) as RawTemplate; const validated = validateTemplate(raw, file.name); if (validated) { // Use folder-based category from Rust, fallback to 'Other' const category = file.category || 'Other'; templates.push({ ...validated, category }); } } catch (e) { console.error(`Failed to parse template ${file.name}:`, e); } } if (templates.length === 0) { error = 'No templates found.'; } } catch (e) { console.error('Error in loadTemplates:', e); error = e instanceof Error ? e.message : String(e); } // Sort by category then name templates.sort((a, b) => { const catCompare = a.category.localeCompare(b.category); if (catCompare !== 0) return catCompare; return a.name.localeCompare(b.name); }); // Deduplicate by ID (in case of duplicate files) const seen = new Set(); const deduplicated = templates.filter(t => { if (seen.has(t.id)) { console.warn(`Duplicate template ID found: ${t.id}, skipping duplicate`); return false; } seen.add(t.id); return true; }); console.log(`Returning ${deduplicated.length} templates (deduplicated from ${templates.length}), error: ${error}`); return { templates: deduplicated, error }; } /** * Open the templates folder in the system file explorer */ export async function openTemplatesFolder(): Promise<{ success: boolean; error?: string }> { try { await invoke('open_templates_folder'); return { success: true }; } catch (e) { const errorMsg = e instanceof Error ? e.message : String(e); return { success: false, error: errorMsg }; } } /** * Load a single template by ID */ export async function loadTemplateById(id: string): Promise { const { templates } = await loadTemplates(); return templates.find(t => t.id === id) || null; } /** * Get all available categories from templates */ export function getCategories(templates: StyleOption[]): string[] { const categories = new Set(); templates.forEach(t => categories.add(t.category)); return Array.from(categories).sort(); }