Initial clean project import
This commit is contained in:
@@ -0,0 +1,985 @@
|
||||
/**
|
||||
* 字幕模板管理 - IPC Handlers
|
||||
* 提供字幕模板的CRUD操作
|
||||
* 用于替代 localStorage,将字幕模板持久化到数据库
|
||||
*/
|
||||
|
||||
import { ipcMain, dialog } from "electron";
|
||||
import { Log } from "../log/main";
|
||||
import DB from "../db/main";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import { AppEnv } from "../env";
|
||||
import { getCommonResourcePath } from "../../lib/resource-path";
|
||||
|
||||
// UUID生成函数
|
||||
const generateUUID = (): string => {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
};
|
||||
|
||||
const cloneJson = <T>(value: T): T => {
|
||||
if (value === null || value === undefined) {
|
||||
return value;
|
||||
}
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
};
|
||||
|
||||
const isSystemTemplateRecord = (template: any): boolean => (
|
||||
template?.isSystem === true || template?.is_system === 1
|
||||
);
|
||||
|
||||
const parseTemplateConfig = (config: any): any => {
|
||||
if (!config) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (typeof config === "string") {
|
||||
return JSON.parse(config);
|
||||
}
|
||||
|
||||
return cloneJson(config);
|
||||
};
|
||||
|
||||
const mergeTemplateConfig = (baseConfig: any, overrideConfig: any): any => {
|
||||
const base = baseConfig || {};
|
||||
const override = overrideConfig || {};
|
||||
|
||||
return {
|
||||
...base,
|
||||
...override,
|
||||
subtitleStyle: override.subtitleStyle ?? base.subtitleStyle,
|
||||
titleSubtitleConfig: override.titleSubtitleConfig ?? base.titleSubtitleConfig,
|
||||
keywordGroupsStyles: override.keywordGroupsStyles ?? base.keywordGroupsStyles,
|
||||
previewConfig: {
|
||||
...(base.previewConfig || {}),
|
||||
...(override.previewConfig || {}),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const dedupeTemplatesById = (templates: any[]): any[] => {
|
||||
const deduped = new Map<string, any>();
|
||||
|
||||
for (const template of templates) {
|
||||
if (!template?.id) {
|
||||
continue;
|
||||
}
|
||||
deduped.set(template.id, template);
|
||||
}
|
||||
|
||||
return Array.from(deduped.values());
|
||||
};
|
||||
|
||||
const loadExistingExportConfig = (configPath: string): any => {
|
||||
const possiblePaths = Array.from(new Set([
|
||||
configPath,
|
||||
path.join(AppEnv.appRoot || process.cwd(), "electron", "config", "system-templates.json"),
|
||||
path.join(process.cwd(), "electron", "config", "system-templates.json"),
|
||||
]));
|
||||
|
||||
for (const candidatePath of possiblePaths) {
|
||||
if (!candidatePath || !fs.existsSync(candidatePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(candidatePath, "utf-8");
|
||||
const parsed = JSON.parse(content);
|
||||
return {
|
||||
version: parsed.version || "2.0.0",
|
||||
description: parsed.description || "系统默认模板配置 - 应用启动时自动初始化",
|
||||
subtitleTemplates: Array.isArray(parsed.subtitleTemplates) ? parsed.subtitleTemplates : [],
|
||||
subtitleStyles: Array.isArray(parsed.subtitleStyles) ? parsed.subtitleStyles : [],
|
||||
coverTemplates: Array.isArray(parsed.coverTemplates) ? parsed.coverTemplates : [],
|
||||
};
|
||||
} catch (error) {
|
||||
Log.warn("ipAgent.exportSubtitleTemplates.loadExistingConfigFailed", {
|
||||
configPath: candidatePath,
|
||||
error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version: "2.0.0",
|
||||
description: "系统默认模板配置 - 应用启动时自动初始化",
|
||||
subtitleTemplates: [],
|
||||
subtitleStyles: [],
|
||||
coverTemplates: [],
|
||||
};
|
||||
};
|
||||
|
||||
const buildBundledSystemTemplateMap = (config: any): Map<string, any> => {
|
||||
const map = new Map<string, any>();
|
||||
|
||||
for (const template of config?.subtitleTemplates || []) {
|
||||
if (!template?.id || !isSystemTemplateRecord(template)) {
|
||||
continue;
|
||||
}
|
||||
map.set(template.id, template);
|
||||
}
|
||||
|
||||
return map;
|
||||
};
|
||||
|
||||
const normalizeSubtitleTemplateRecord = (template: any, bundledSystemTemplateMap: Map<string, any>): any => {
|
||||
const dbConfig = parseTemplateConfig(template.config);
|
||||
const bundledTemplate = template.is_system === 1 ? bundledSystemTemplateMap.get(template.id) : null;
|
||||
const bundledConfig = bundledTemplate?.config ? parseTemplateConfig(bundledTemplate.config) : {};
|
||||
const mergedConfig = template.is_system === 1
|
||||
? mergeTemplateConfig(bundledConfig, dbConfig)
|
||||
: dbConfig;
|
||||
|
||||
return {
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
createdAt: template.created_at,
|
||||
isSystem: template.is_system === 1,
|
||||
config: mergedConfig,
|
||||
};
|
||||
};
|
||||
|
||||
// 注册字幕模板相关的IPC handlers
|
||||
export const registerSubtitleTemplateHandlers = () => {
|
||||
// 保存字幕模板
|
||||
ipcMain.handle("ipAgent:saveSubtitleTemplate", async (event, params: {
|
||||
name: string;
|
||||
description?: string;
|
||||
config: any;
|
||||
id?: string;
|
||||
isDev?: boolean;
|
||||
}) => {
|
||||
try {
|
||||
const { name, description, config, id, isDev } = params;
|
||||
|
||||
Log.info("ipAgent.saveSubtitleTemplate - START", {
|
||||
name,
|
||||
id,
|
||||
haConfig: !!config,
|
||||
isDev
|
||||
});
|
||||
console.log('[SubtitleTemplate saveSubtitleTemplate] 接收到参数:', {
|
||||
name,
|
||||
id,
|
||||
description,
|
||||
isDev
|
||||
});
|
||||
|
||||
// 验证参数
|
||||
if (!name || !config) {
|
||||
console.error('[SubtitleTemplate saveSubtitleTemplate] ❌ 参数验证失败:', {
|
||||
nameEmpty: !name,
|
||||
configEmpty: !config
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
message: "模板名称和配置不能为空"
|
||||
};
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const configJson = JSON.stringify(config);
|
||||
|
||||
// 如果提供了id,先检查该id是否存在
|
||||
if (id) {
|
||||
const existing = await DB.first(
|
||||
`SELECT id, name, is_system FROM subtitle_templates WHERE id = ?`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
// ✅ 修复:仅在非开发模式下保护系统模板,开发模式下允许修改
|
||||
if (existing.is_system === 1 && !isDev) {
|
||||
Log.warn("ipAgent.saveSubtitleTemplate.systemTemplate", { id, name, isDev });
|
||||
return {
|
||||
success: false,
|
||||
message: "系统默认模板不能修改"
|
||||
};
|
||||
}
|
||||
|
||||
// 如果修改了名称,检查新名称是否与其他模板冲突
|
||||
if (existing.name !== name) {
|
||||
const nameConflict = await DB.first(
|
||||
`SELECT id FROM subtitle_templates WHERE name = ? AND id != ?`,
|
||||
[name, id]
|
||||
);
|
||||
if (nameConflict) {
|
||||
return {
|
||||
success: false,
|
||||
message: "模板名称已存在,请使用其他名称"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 更新现有模板
|
||||
await DB.execute(
|
||||
`UPDATE subtitle_templates
|
||||
SET name = ?, description = ?, config = ?, updated_at = ?
|
||||
WHERE id = ?`,
|
||||
[name, description || null, configJson, now, id]
|
||||
);
|
||||
|
||||
Log.info("ipAgent.saveSubtitleTemplate.updated", { name, id });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
id,
|
||||
message: "模板更新成功"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 🔧 防止误用已存在的ID进行创建操作
|
||||
if (id && typeof id === 'string' && id.trim()) {
|
||||
Log.warn("ipAgent.saveSubtitleTemplate.idProvisionedForCreation", { id, name });
|
||||
return {
|
||||
success: false,
|
||||
message: "创建新模板不应该指定ID,请不指定ID让系统自动生成,或使用更新接口修改现有模板"
|
||||
};
|
||||
}
|
||||
|
||||
// 检查是否已存在同名模板
|
||||
const existingByName = await DB.first(
|
||||
`SELECT id FROM subtitle_templates WHERE name = ?`,
|
||||
[name]
|
||||
);
|
||||
|
||||
if (existingByName) {
|
||||
return {
|
||||
success: false,
|
||||
message: "模板名称已存在,请使用其他名称"
|
||||
};
|
||||
}
|
||||
|
||||
// 创建新模板 - 生成新UUID
|
||||
const newId = generateUUID();
|
||||
await DB.execute(
|
||||
`INSERT INTO subtitle_templates (id, name, description, config, is_system, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[newId, name, description || null, configJson, 0, now, now]
|
||||
);
|
||||
|
||||
Log.info("ipAgent.saveSubtitleTemplate.created", { name, id: newId });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
id: newId,
|
||||
message: "模板保存成功"
|
||||
};
|
||||
} catch (error) {
|
||||
Log.error("ipAgent.saveSubtitleTemplate.error", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "保存模板失败: " + (error?.message || error)
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 获取所有字幕模板
|
||||
ipcMain.handle("ipAgent:getSubtitleTemplates", async (event, params?: { metadataOnly?: boolean }) => {
|
||||
try {
|
||||
const metadataOnly = params?.metadataOnly === true;
|
||||
Log.info("ipAgent.getSubtitleTemplates - START", { metadataOnly });
|
||||
console.log(`[SubtitleTemplate] 开始获取字幕模板列表 (metadataOnly=${metadataOnly})`);
|
||||
|
||||
const selectFields = metadataOnly
|
||||
? `id, name, description, created_at, updated_at, is_system`
|
||||
: `id, name, description, config, created_at, updated_at, is_system`;
|
||||
|
||||
const templates = await DB.select(
|
||||
`SELECT ${selectFields}
|
||||
FROM subtitle_templates
|
||||
ORDER BY is_system DESC, created_at DESC`
|
||||
);
|
||||
|
||||
console.log(`[SubtitleTemplate] 查询到 ${templates.length} 个模板`);
|
||||
Log.info("ipAgent.getSubtitleTemplates.queryResult", { count: templates.length, metadataOnly });
|
||||
|
||||
// 仅元数据模式,直接返回
|
||||
if (metadataOnly) {
|
||||
const metadataTemplates = templates.map(t => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
createdAt: t.created_at,
|
||||
updatedAt: t.updated_at,
|
||||
is_system: t.is_system || 0
|
||||
}));
|
||||
|
||||
console.log(`[SubtitleTemplate] 返回 ${metadataTemplates.length} 个模板(仅元数据)`);
|
||||
return {
|
||||
success: true,
|
||||
templates: metadataTemplates
|
||||
};
|
||||
}
|
||||
|
||||
// 解析config JSON并返回完整模板
|
||||
const parsedTemplates = templates.map(t => {
|
||||
try {
|
||||
const config = JSON.parse(t.config);
|
||||
const template = {
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
createdAt: t.created_at,
|
||||
updatedAt: t.updated_at,
|
||||
is_system: t.is_system || 0,
|
||||
config: config
|
||||
};
|
||||
console.log(`[SubtitleTemplate] 解析模板: ${t.name}, is_system: ${t.is_system}`);
|
||||
return template;
|
||||
} catch (error) {
|
||||
Log.error("ipAgent.getSubtitleTemplates.parseError", {
|
||||
templateId: t.id,
|
||||
error: error
|
||||
});
|
||||
console.error(`[SubtitleTemplate] 解析模板失败: ${t.id}`, error);
|
||||
// 返回基本信息,避免整个列表加载失败
|
||||
return {
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
createdAt: t.created_at,
|
||||
updatedAt: t.updated_at,
|
||||
is_system: t.is_system || 0,
|
||||
config: {}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
Log.info("ipAgent.getSubtitleTemplates.success", {
|
||||
count: parsedTemplates.length,
|
||||
systemCount: parsedTemplates.filter(t => t.is_system === 1).length
|
||||
});
|
||||
|
||||
console.log(`[SubtitleTemplate] 成功返回 ${parsedTemplates.length} 个模板`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
templates: parsedTemplates
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[SubtitleTemplate] 获取模板列表失败:", error);
|
||||
Log.error("ipAgent.getSubtitleTemplates.error", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "获取模板列表失败: " + (error?.message || error),
|
||||
templates: []
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 获取单个字幕模板
|
||||
ipcMain.handle("ipAgent:getSubtitleTemplate", async (event, params: { id: string }) => {
|
||||
try {
|
||||
const { id } = params;
|
||||
|
||||
Log.info("ipAgent.getSubtitleTemplate", { id });
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "模板ID不能为空"
|
||||
};
|
||||
}
|
||||
|
||||
const template = await DB.first(
|
||||
`SELECT id, name, description, config, created_at, updated_at, is_system
|
||||
FROM subtitle_templates
|
||||
WHERE id = ?`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (!template) {
|
||||
return {
|
||||
success: false,
|
||||
message: "模板不存在"
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const config = JSON.parse(template.config);
|
||||
const parsedTemplate = {
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
createdAt: template.created_at,
|
||||
updatedAt: template.updated_at,
|
||||
is_system: template.is_system || 0,
|
||||
config: config
|
||||
};
|
||||
|
||||
Log.info("ipAgent.getSubtitleTemplate.success", { id });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
template: parsedTemplate
|
||||
};
|
||||
} catch (error) {
|
||||
Log.error("ipAgent.getSubtitleTemplate.parseError", {
|
||||
id,
|
||||
error: error
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
message: "模板数据解析失败"
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
Log.error("ipAgent.getSubtitleTemplate.error", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "获取模板失败: " + (error?.message || error)
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 删除字幕模板
|
||||
ipcMain.handle("ipAgent:deleteSubtitleTemplate", async (event, params: {
|
||||
id: string;
|
||||
isDev?: boolean;
|
||||
}) => {
|
||||
try {
|
||||
const { id, isDev } = params;
|
||||
|
||||
Log.info("ipAgent.deleteSubtitleTemplate", { id });
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "模板ID不能为空"
|
||||
};
|
||||
}
|
||||
|
||||
// 检查是否为系统模板
|
||||
const template = await DB.first(
|
||||
`SELECT is_system FROM subtitle_templates WHERE id = ?`,
|
||||
[id]
|
||||
);
|
||||
|
||||
// ✅ 修复:仅在非开发模式下保护系统模板,开发模式下允许删除
|
||||
if (template && template.is_system === 1 && !isDev) {
|
||||
Log.warn("ipAgent.deleteSubtitleTemplate.systemTemplate", { id, isDev });
|
||||
return {
|
||||
success: false,
|
||||
message: "系统默认模板不能删除"
|
||||
};
|
||||
}
|
||||
|
||||
await DB.execute(
|
||||
`DELETE FROM subtitle_templates WHERE id = ?`,
|
||||
[id]
|
||||
);
|
||||
|
||||
Log.info("ipAgent.deleteSubtitleTemplate.success", { id });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "模板删除成功"
|
||||
};
|
||||
} catch (error) {
|
||||
Log.error("ipAgent.deleteSubtitleTemplate.error", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "删除模板失败: " + (error?.message || error)
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 更新字幕模板
|
||||
ipcMain.handle("ipAgent:updateSubtitleTemplate", async (event, params: {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
config?: any;
|
||||
is_system?: number;
|
||||
isDev?: boolean;
|
||||
}) => {
|
||||
try {
|
||||
const { id, name, description, config, is_system, isDev } = params;
|
||||
|
||||
Log.info("ipAgent.updateSubtitleTemplate", { id, hasName: !!name, hasConfig: !!config });
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "模板ID不能为空"
|
||||
};
|
||||
}
|
||||
|
||||
// ✅ 修复:仅在非开发模式下保护系统模板,开发模式下允许修改
|
||||
let template;
|
||||
try {
|
||||
template = await DB.first(
|
||||
`SELECT is_system FROM subtitle_templates WHERE id = ?`,
|
||||
[id]
|
||||
);
|
||||
} catch (columnError) {
|
||||
console.warn("[SubtitleTemplate] is_system 列不存在,跳过系统模板检查");
|
||||
template = null;
|
||||
}
|
||||
|
||||
if (template && template.is_system === 1 && !isDev) {
|
||||
Log.warn("ipAgent.updateSubtitleTemplate.systemTemplate", { id, isDev });
|
||||
return {
|
||||
success: false,
|
||||
message: "系统默认模板不能修改"
|
||||
};
|
||||
}
|
||||
|
||||
const updates: string[] = [];
|
||||
const values: any[] = [];
|
||||
|
||||
if (name !== undefined) {
|
||||
updates.push('name = ?');
|
||||
values.push(name);
|
||||
}
|
||||
|
||||
if (description !== undefined) {
|
||||
updates.push('description = ?');
|
||||
values.push(description);
|
||||
}
|
||||
|
||||
if (config !== undefined) {
|
||||
updates.push('config = ?');
|
||||
values.push(JSON.stringify(config));
|
||||
}
|
||||
|
||||
if (is_system !== undefined) {
|
||||
updates.push('is_system = ?');
|
||||
values.push(is_system);
|
||||
}
|
||||
|
||||
if (updates.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: "没有要更新的内容"
|
||||
};
|
||||
}
|
||||
|
||||
updates.push('updated_at = ?');
|
||||
values.push(Date.now());
|
||||
values.push(id);
|
||||
|
||||
await DB.execute(
|
||||
`UPDATE subtitle_templates
|
||||
SET ${updates.join(', ')}
|
||||
WHERE id = ?`,
|
||||
values
|
||||
);
|
||||
|
||||
Log.info("ipAgent.updateSubtitleTemplate.success", { id });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "模板更新成功"
|
||||
};
|
||||
} catch (error) {
|
||||
Log.error("ipAgent.updateSubtitleTemplate.error", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "更新模板失败: " + (error?.message || error)
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 导出所有字幕模板为JSON配置文件
|
||||
ipcMain.handle("ipAgent:exportSubtitleTemplates", async (event, params?: {
|
||||
includeSystemOnly?: boolean;
|
||||
}) => {
|
||||
try {
|
||||
const includeSystemOnly = params?.includeSystemOnly === true;
|
||||
|
||||
Log.info("ipAgent.exportSubtitleTemplates - START", { includeSystemOnly });
|
||||
console.log('[SubtitleTemplate] 开始导出字幕模板 (includeSystemOnly=' + includeSystemOnly + ')');
|
||||
|
||||
// 查询数据库中的所有模板
|
||||
const sqlQuery = includeSystemOnly
|
||||
? `SELECT id, name, description, config, created_at, is_system FROM subtitle_templates WHERE is_system = 1 ORDER BY created_at ASC`
|
||||
: `SELECT id, name, description, config, created_at, is_system FROM subtitle_templates ORDER BY is_system DESC, created_at ASC`;
|
||||
|
||||
const dbTemplates = await DB.select(sqlQuery);
|
||||
console.log(`[SubtitleTemplate] 查询到 ${dbTemplates.length} 个模板`);
|
||||
|
||||
// 转换为JSON导出格式
|
||||
const subtitleTemplates = dbTemplates.map(t => {
|
||||
try {
|
||||
return {
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
createdAt: t.created_at,
|
||||
isSystem: t.is_system === 1,
|
||||
config: typeof t.config === 'string' ? JSON.parse(t.config) : t.config
|
||||
};
|
||||
} catch (error) {
|
||||
Log.warn("ipAgent.exportSubtitleTemplates.parseError", {
|
||||
templateId: t.id,
|
||||
error: error
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}).filter(t => t !== null);
|
||||
|
||||
// 获取系统模板配置文件路径(使用统一的打包配置文件)
|
||||
const configPath = getCommonResourcePath('config/system-templates.json');
|
||||
const existingConfig = loadExistingExportConfig(configPath);
|
||||
const bundledSystemTemplateMap = buildBundledSystemTemplateMap(existingConfig);
|
||||
const normalizedTemplates = dedupeTemplatesById(
|
||||
dbTemplates.map((template) => normalizeSubtitleTemplateRecord(template, bundledSystemTemplateMap))
|
||||
);
|
||||
console.log(`[SubtitleTemplate] 配置文件路径: ${configPath}`);
|
||||
|
||||
let config: any = {
|
||||
version: "2.0.0",
|
||||
description: "系统默认模板配置 - 应用启动时自动初始化",
|
||||
subtitleTemplates: [],
|
||||
subtitleStyles: [],
|
||||
coverTemplates: []
|
||||
};
|
||||
|
||||
// 读取现有配置文件(保留覆盖模板)
|
||||
if (fs.existsSync(configPath)) {
|
||||
try {
|
||||
const content = fs.readFileSync(configPath, 'utf-8');
|
||||
config = JSON.parse(content);
|
||||
} catch (error) {
|
||||
console.warn(`[SubtitleTemplate] 读取现有配置失败,使用默认结构`);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取系统模板ID列表(用于过滤)
|
||||
config = {
|
||||
...existingConfig,
|
||||
...config,
|
||||
version: existingConfig.version || config.version || "2.0.0",
|
||||
description: existingConfig.description || config.description || "系统默认模板配置 - 应用启动时自动初始化",
|
||||
};
|
||||
|
||||
const systemTemplateIds = normalizedTemplates.filter(t => t.isSystem).map(t => t.id);
|
||||
|
||||
// 直接查询所有系统字幕样式(is_system = 1)
|
||||
// 这样可以确保导出所有系统样式,不依赖模板引用
|
||||
let subtitleStyles: any[] = [];
|
||||
try {
|
||||
subtitleStyles = await DB.select(
|
||||
`SELECT id, name, description, fontName, fontSize, fontColor, outlineColor, outlineWidth,
|
||||
shadowOffset, shadowColor, shadowBlur, backgroundColor, backgroundOpacity,
|
||||
position, alignment, is_system, preview
|
||||
FROM subtitle_styles
|
||||
WHERE is_system = 1
|
||||
ORDER BY id ASC`
|
||||
);
|
||||
console.log(`[SubtitleTemplate] ✅ 查询到 ${subtitleStyles.length} 个系统字幕样式`);
|
||||
} catch (error) {
|
||||
console.warn(`[SubtitleTemplate] ⚠️ 查询字幕样式失败,继续不导出样式:`, error);
|
||||
}
|
||||
|
||||
// 更新字幕模板:移除旧的系统模板,添加新的
|
||||
if (includeSystemOnly) {
|
||||
const existingNonSystemTemplates = (existingConfig.subtitleTemplates || []).filter((template: any) => !isSystemTemplateRecord(template));
|
||||
config.subtitleTemplates = dedupeTemplatesById([
|
||||
...normalizedTemplates,
|
||||
...existingNonSystemTemplates,
|
||||
]);
|
||||
} else {
|
||||
config.subtitleTemplates = normalizedTemplates;
|
||||
}
|
||||
|
||||
// 更新字幕样式:移除旧的系统样式,添加新的
|
||||
const systemStyleIds = new Set(subtitleStyles.map(s => s.id));
|
||||
config.subtitleStyles = [
|
||||
...subtitleStyles.map(s => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
description: s.description,
|
||||
preview: s.preview || "示例文字",
|
||||
is_system: 1,
|
||||
fontName: s.fontName,
|
||||
fontSize: s.fontSize,
|
||||
fontColor: s.fontColor,
|
||||
outlineColor: s.outlineColor,
|
||||
outlineWidth: s.outlineWidth,
|
||||
shadowOffset: s.shadowOffset,
|
||||
shadowColor: s.shadowColor,
|
||||
shadowBlur: s.shadowBlur,
|
||||
backgroundColor: s.backgroundColor,
|
||||
backgroundOpacity: s.backgroundOpacity,
|
||||
position: s.position,
|
||||
alignment: s.alignment
|
||||
})),
|
||||
...(config.subtitleStyles || []).filter((s: any) => !systemStyleIds.has(s.id))
|
||||
];
|
||||
|
||||
// 写回配置文件
|
||||
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
||||
|
||||
console.log(`[SubtitleTemplate] ✅ 已导出 ${config.subtitleTemplates.length} 个模板和 ${subtitleStyles.length} 个样式到配置文件`);
|
||||
console.log(`[SubtitleTemplate] 配置文件已更新: ${configPath}`);
|
||||
|
||||
Log.info("ipAgent.exportSubtitleTemplates.success", {
|
||||
count: config.subtitleTemplates.length,
|
||||
styleCount: subtitleStyles.length,
|
||||
configPath: configPath,
|
||||
templates: config.subtitleTemplates.map((t: any) => ({ id: t.id, name: t.name }))
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
count: config.subtitleTemplates.length,
|
||||
styleCount: subtitleStyles.length,
|
||||
message: `已将 ${config.subtitleTemplates.length} 个模板和 ${subtitleStyles.length} 个样式导出到配置文件`,
|
||||
configPath: configPath,
|
||||
templates: config.subtitleTemplates.map((t: any) => ({ id: t.id, name: t.name }))
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[SubtitleTemplate] 导出模板失败:", error);
|
||||
Log.error("ipAgent.exportSubtitleTemplates.error", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "导出失败: " + (error?.message || error),
|
||||
data: null
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 导出单个字幕模板为JSON
|
||||
ipcMain.handle("ipAgent:exportSubtitleTemplateById", async (event, params: {
|
||||
id: string;
|
||||
}) => {
|
||||
try {
|
||||
const { id } = params;
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "模板ID不能为空"
|
||||
};
|
||||
}
|
||||
|
||||
Log.info("ipAgent.exportSubtitleTemplateById", { id });
|
||||
console.log('[SubtitleTemplate] 开始导出单个字幕模板:', id);
|
||||
|
||||
const template = await DB.first(
|
||||
`SELECT id, name, description, config, created_at, is_system FROM subtitle_templates WHERE id = ?`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (!template) {
|
||||
return {
|
||||
success: false,
|
||||
message: "模板不存在"
|
||||
};
|
||||
}
|
||||
|
||||
const bundledSystemTemplateMap = buildBundledSystemTemplateMap(
|
||||
loadExistingExportConfig(getCommonResourcePath('config/system-templates.json'))
|
||||
);
|
||||
const normalizedTemplate = normalizeSubtitleTemplateRecord(template, bundledSystemTemplateMap);
|
||||
const exportData = {
|
||||
version: "2.0.0",
|
||||
description: "单个字幕模板导出",
|
||||
timestamp: new Date().toISOString(),
|
||||
subtitleTemplate: {
|
||||
id: normalizedTemplate.id,
|
||||
name: normalizedTemplate.name,
|
||||
description: normalizedTemplate.description,
|
||||
createdAt: normalizedTemplate.createdAt,
|
||||
isSystem: normalizedTemplate.isSystem,
|
||||
config: normalizedTemplate.config
|
||||
}
|
||||
};
|
||||
|
||||
console.log(`[SubtitleTemplate] 导出单个模板成功: ${template.name}`);
|
||||
Log.info("ipAgent.exportSubtitleTemplateById.success", { id, name: template.name });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: exportData,
|
||||
message: "导出成功"
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[SubtitleTemplate] 导出单个模板失败:", error);
|
||||
Log.error("ipAgent.exportSubtitleTemplateById.error", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "导出失败: " + (error?.message || error),
|
||||
data: null
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// 复制字幕模板
|
||||
ipcMain.handle("ipAgent:duplicateSubtitleTemplate", async (event, params: {
|
||||
id: string;
|
||||
newName?: string;
|
||||
}) => {
|
||||
try {
|
||||
const { id, newName } = params;
|
||||
|
||||
Log.info("ipAgent.duplicateSubtitleTemplate", { id, newName });
|
||||
console.log('[SubtitleTemplate duplicateSubtitleTemplate] 开始复制模板:', id);
|
||||
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
message: "模板ID不能为空"
|
||||
};
|
||||
}
|
||||
|
||||
// 获取原模板
|
||||
const sourceTemplate = await DB.first(
|
||||
`SELECT id, name, description, config, is_system FROM subtitle_templates WHERE id = ?`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (!sourceTemplate) {
|
||||
return {
|
||||
success: false,
|
||||
message: "源模板不存在"
|
||||
};
|
||||
}
|
||||
|
||||
// 生成新模板名称
|
||||
let duplicateName = newName || `${sourceTemplate.name} - 副本`;
|
||||
|
||||
// 检查名称是否已存在,如果存在则添加数字后缀
|
||||
let counter = 1;
|
||||
let finalName = duplicateName;
|
||||
while (true) {
|
||||
const existing = await DB.first(
|
||||
`SELECT id FROM subtitle_templates WHERE name = ?`,
|
||||
[finalName]
|
||||
);
|
||||
if (!existing) break;
|
||||
|
||||
finalName = `${duplicateName} (${counter})`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
// 创建新模板
|
||||
const newId = generateUUID();
|
||||
const now = Date.now();
|
||||
const bundledSystemTemplateMap = buildBundledSystemTemplateMap(
|
||||
loadExistingExportConfig(getCommonResourcePath('config/system-templates.json'))
|
||||
);
|
||||
const normalizedSourceTemplate = normalizeSubtitleTemplateRecord(sourceTemplate, bundledSystemTemplateMap);
|
||||
|
||||
await DB.execute(
|
||||
`INSERT INTO subtitle_templates (id, name, description, config, is_system, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[newId, finalName, normalizedSourceTemplate.description, JSON.stringify(normalizedSourceTemplate.config), 0, now, now]
|
||||
);
|
||||
|
||||
Log.info("ipAgent.duplicateSubtitleTemplate.success", {
|
||||
sourceId: id,
|
||||
newId,
|
||||
sourceName: sourceTemplate.name,
|
||||
newName: finalName
|
||||
});
|
||||
console.log(`[SubtitleTemplate] ✅ 复制成功: ${sourceTemplate.name} → ${finalName}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
id: newId,
|
||||
name: finalName,
|
||||
message: "模板复制成功"
|
||||
};
|
||||
} catch (error) {
|
||||
Log.error("ipAgent.duplicateSubtitleTemplate.error", error);
|
||||
console.error("[SubtitleTemplate] 复制模板失败:", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "复制模板失败: " + (error?.message || error)
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("ipAgent:importSubtitleTemplate", async (event) => {
|
||||
try {
|
||||
const result = await dialog.showOpenDialog({
|
||||
title: '导入字幕模板',
|
||||
filters: [
|
||||
{ name: 'JSON文件', extensions: ['json'] }
|
||||
],
|
||||
properties: ['openFile']
|
||||
});
|
||||
|
||||
if (result.canceled || !result.filePaths || result.filePaths.length === 0) {
|
||||
return { success: false, message: '用户取消导入' };
|
||||
}
|
||||
|
||||
const filePath = result.filePaths[0];
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const data = JSON.parse(content);
|
||||
|
||||
let templateData: any = null;
|
||||
if (data.subtitleTemplate) {
|
||||
templateData = data.subtitleTemplate;
|
||||
} else if (data.name && data.config) {
|
||||
templateData = data;
|
||||
} else {
|
||||
return { success: false, message: '无法识别的模板文件格式' };
|
||||
}
|
||||
|
||||
if (!templateData.config) {
|
||||
return { success: false, message: '模板文件缺少配置数据' };
|
||||
}
|
||||
|
||||
const templateName = templateData.name || '导入的模板';
|
||||
const configJson = JSON.stringify(templateData.config);
|
||||
const now = Date.now();
|
||||
|
||||
const count = await DB.first(
|
||||
`SELECT COUNT(*) as count FROM subtitle_templates WHERE is_system = 0 OR is_system IS NULL`
|
||||
);
|
||||
if (count && count.count >= 24) {
|
||||
return { success: false, message: '最多只能保存24个自定义模板,请先删除一些模板' };
|
||||
}
|
||||
|
||||
const existingByName = await DB.first(
|
||||
`SELECT id FROM subtitle_templates WHERE name = ?`,
|
||||
[templateName]
|
||||
);
|
||||
let finalName = templateName;
|
||||
if (existingByName) {
|
||||
finalName = templateName + ' (' + new Date().toLocaleString() + ')';
|
||||
}
|
||||
|
||||
const newId = generateUUID();
|
||||
await DB.execute(
|
||||
`INSERT INTO subtitle_templates (id, name, description, config, created_at, updated_at, is_system)
|
||||
VALUES (?, ?, ?, ?, ?, ?, 0)`,
|
||||
[newId, finalName, templateData.description || '', configJson, now, now]
|
||||
);
|
||||
|
||||
console.log(`[SubtitleTemplate] 导入模板成功: ${finalName}`);
|
||||
Log.info("ipAgent.importSubtitleTemplate.success", { id: newId, name: finalName });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
id: newId,
|
||||
name: finalName,
|
||||
message: `模板 "${finalName}" 导入成功`
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("[SubtitleTemplate] 导入模板失败:", error);
|
||||
Log.error("ipAgent.importSubtitleTemplate.error", error);
|
||||
return {
|
||||
success: false,
|
||||
message: "导入失败: " + (error?.message || error)
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
Log.info("ipAgent.subtitleTemplate.handlers.registered");
|
||||
};
|
||||
|
||||
export default {
|
||||
registerSubtitleTemplateHandlers
|
||||
};
|
||||
Reference in New Issue
Block a user