251 lines
7.7 KiB
TypeScript
251 lines
7.7 KiB
TypeScript
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<string, ElementStyle>;
|
|
page?: Record<string, any>;
|
|
wordConfig?: Record<string, any>;
|
|
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<string, any>);
|
|
|
|
return { ...template, ...extraFields };
|
|
}
|
|
|
|
/**
|
|
* Debug function to check paths
|
|
*/
|
|
export async function debugPaths(): Promise<string> {
|
|
try {
|
|
return await invoke<string>('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<TemplateFile[]>('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<string>();
|
|
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<StyleOption | null> {
|
|
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<string>();
|
|
templates.forEach(t => categories.add(t.category));
|
|
return Array.from(categories).sort();
|
|
}
|