import { getDbInstance, runDb, getDb, allDb } from '../database/connection'; import { AppearanceSettings, UpdateAppearanceDto } from '../types/appearance.types'; import { defaultUiTheme } from '../config/default-themes'; import { findThemeById as findTerminalThemeById } from './terminal-theme.repository'; import * as sqlite3 from 'sqlite3'; const TABLE_NAME = 'appearance_settings'; interface DbAppearanceSettingsRow { key: string; value: string; created_at: number; updated_at: number; } const mapRowsToAppearanceSettings = (rows: DbAppearanceSettingsRow[]): AppearanceSettings => { const settings: Partial = {}; let latestUpdatedAt = 0; let terminalBackgroundEnabledFound = false; // 标记是否在数据库中找到该设置 let terminalBackgroundOverlayOpacityFound = false; // 标记是否找到蒙版透明度设置 for (const row of rows) { // 更新 latestUpdatedAt if (row.updated_at > latestUpdatedAt) { latestUpdatedAt = row.updated_at; } switch (row.key) { case 'customUiTheme': settings.customUiTheme = row.value; break; case 'activeTerminalThemeId': const parsedId = parseInt(row.value, 10); settings.activeTerminalThemeId = isNaN(parsedId) ? null : parsedId; break; case 'terminalFontFamily': settings.terminalFontFamily = row.value; break; case 'terminalFontSize': settings.terminalFontSize = parseInt(row.value, 10); break; case 'editorFontSize': settings.editorFontSize = parseInt(row.value, 10); break; case 'terminalBackgroundImage': settings.terminalBackgroundImage = row.value || undefined; break; case 'pageBackgroundImage': settings.pageBackgroundImage = row.value || undefined; break; case 'terminalBackgroundEnabled': settings.terminalBackgroundEnabled = row.value === 'true'; // 将 'true'/'false' 字符串转为 boolean terminalBackgroundEnabledFound = true; break; case 'terminalBackgroundOverlayOpacity': settings.terminalBackgroundOverlayOpacity = parseFloat(row.value); terminalBackgroundOverlayOpacityFound = true; break; } } const defaults = getDefaultAppearanceSettings(); return { _id: 'global_appearance', // 全局外观设置的固定 ID customUiTheme: settings.customUiTheme ?? defaults.customUiTheme, activeTerminalThemeId: settings.activeTerminalThemeId ?? defaults.activeTerminalThemeId, terminalFontFamily: settings.terminalFontFamily ?? defaults.terminalFontFamily, terminalFontSize: settings.terminalFontSize ?? defaults.terminalFontSize, editorFontSize: settings.editorFontSize ?? defaults.editorFontSize, terminalBackgroundImage: settings.terminalBackgroundImage ?? defaults.terminalBackgroundImage, pageBackgroundImage: settings.pageBackgroundImage ?? defaults.pageBackgroundImage, // 修改:只有当数据库中未找到记录时才使用默认值 terminalBackgroundEnabled: terminalBackgroundEnabledFound ? settings.terminalBackgroundEnabled // 使用数据库找到的值 (true 或 false) : defaults.terminalBackgroundEnabled, // 否则使用默认值 (true) terminalBackgroundOverlayOpacity: terminalBackgroundOverlayOpacityFound ? settings.terminalBackgroundOverlayOpacity // 使用数据库找到的值 : defaults.terminalBackgroundOverlayOpacity, // 否则使用默认值 updatedAt: latestUpdatedAt || defaults.updatedAt, // 使用最新的更新时间,否则使用默认时间戳 }; }; // 获取默认外观设置 (已简化, _id 在此不再相关) const getDefaultAppearanceSettings = (): Omit => { return { customUiTheme: JSON.stringify(defaultUiTheme), activeTerminalThemeId: null, // 初始默认应为 null terminalFontFamily: 'Consolas, "Courier New", monospace, "Microsoft YaHei", "微软雅黑"', terminalFontSize: 14, editorFontSize: 14, terminalBackgroundImage: undefined, pageBackgroundImage: undefined, terminalBackgroundEnabled: true, // 默认启用 terminalBackgroundOverlayOpacity: 0.5, // 默认蒙版透明度 updatedAt: Date.now(), // 提供默认时间戳 }; }; /** * 确保默认设置存在于键值表中。 * 此函数在数据库初始化期间调用。 * @param db - 活动的数据库实例 */ export const ensureDefaultSettingsExist = async (db: sqlite3.Database): Promise => { const defaults = getDefaultAppearanceSettings(); const nowSeconds = Math.floor(Date.now() / 1000); const sqlInsertOrIgnore = `INSERT OR IGNORE INTO ${TABLE_NAME} (key, value, created_at, updated_at) VALUES (?, ?, ?, ?)`; // 定义默认键值对以确保存在 const defaultEntries: Array<{ key: keyof Omit, value: any }> = [ { key: 'customUiTheme', value: defaults.customUiTheme }, { key: 'activeTerminalThemeId', value: null }, // 以 null 开始 { key: 'terminalFontFamily', value: defaults.terminalFontFamily }, { key: 'terminalFontSize', value: defaults.terminalFontSize }, { key: 'editorFontSize', value: defaults.editorFontSize }, { key: 'terminalBackgroundImage', value: defaults.terminalBackgroundImage ?? '' }, // 数据库中使用空字符串 { key: 'pageBackgroundImage', value: defaults.pageBackgroundImage ?? '' }, // 数据库中使用空字符串 { key: 'terminalBackgroundEnabled', value: defaults.terminalBackgroundEnabled }, { key: 'terminalBackgroundOverlayOpacity', value: defaults.terminalBackgroundOverlayOpacity }, ]; try { for (const entry of defaultEntries) { // 将值转换为字符串以存储到数据库,处理 null/undefined let dbValue: string; if (entry.value === null || entry.value === undefined) { dbValue = entry.key === 'activeTerminalThemeId' ? 'null' : ''; // 主题 ID 特殊存储为 'null',其他情况为空字符串 } else if (typeof entry.value === 'object') { dbValue = JSON.stringify(entry.value); } else { dbValue = String(entry.value); } // 对 activeTerminalThemeId 的特殊处理:将 null 存储为 'null' 字符串,或将数字存储为字符串 if (entry.key === 'activeTerminalThemeId') { dbValue = entry.value === null ? 'null' : String(entry.value); } await runDb(db, sqlInsertOrIgnore, [entry.key, dbValue, nowSeconds, nowSeconds]); } // console.log('[AppearanceRepo] 默认外观设置键值对检查完成。'); // 移除:信息不太关键 // 确保键存在后,如果当前为 null,则尝试设置默认主题 ID await findAndSetDefaultThemeIdIfNull(db); } catch (err: any) { console.error(`[AppearanceRepo] 检查或插入默认外观设置键值对时出错:`, err.message); throw new Error(`检查或插入默认外观设置失败: ${err.message}`); } }; /** * 查找默认终端主题 ID,并在 'activeTerminalThemeId' 设置当前为 null 时更新它。 * @param db - 活动的数据库实例 */ const findAndSetDefaultThemeIdIfNull = async (db: sqlite3.Database): Promise => { try { // 检查 activeTerminalThemeId 的当前值 const currentSetting = await getDb<{ value: string }>(db, `SELECT value FROM ${TABLE_NAME} WHERE key = ?`, ['activeTerminalThemeId']); // 仅当设置存在且其值为 'null' 字符串时继续 if (currentSetting && currentSetting.value === 'null') { // 从 terminal_themes 表中查找默认主题(假设名称 'default' 标记为默认) const defaultThemeSql = `SELECT id FROM terminal_themes WHERE name = 'default' AND theme_type = 'preset' LIMIT 1`; const defaultThemeRow = await getDb<{ id: number }>(db, defaultThemeSql); if (defaultThemeRow) { const defaultThemeIdNum = defaultThemeRow.id; // console.log(`[AppearanceRepo] activeTerminalThemeId 为 null,尝试设置为默认主题 ID: ${defaultThemeIdNum}`); // 移除:信息不太关键 // 使用 INSERT OR REPLACE 更新设置 const sqlReplace = `INSERT OR REPLACE INTO ${TABLE_NAME} (key, value, updated_at) VALUES (?, ?, ?)`; await runDb(db, sqlReplace, ['activeTerminalThemeId', String(defaultThemeIdNum), Math.floor(Date.now() / 1000)]); } else { // console.warn("[AppearanceRepo] 未找到名为 'default' 的预设终端主题,无法设置默认 activeTerminalThemeId。"); } } // 如果 activeTerminalThemeId 已设置或键不存在,则不执行任何操作 } catch (error: any) { console.error("[AppearanceRepo] 设置默认终端主题 ID 时出错:", error.message); // 这里不抛出错误,只记录日志 } }; /** * 获取外观设置。 * 从数据库中检索所有外观相关的键值对,并将它们映射到一个 AppearanceSettings 对象。 * @returns {Promise} 返回包含当前外观设置的对象。 * @throws {Error} 如果从数据库获取设置失败。 */ export const getAppearanceSettings = async (): Promise => { try { const db = await getDbInstance(); // 从键值表中获取所有行 const rows = await allDb(db, `SELECT key, value, updated_at FROM ${TABLE_NAME}`); console.log('[AppearanceRepo LOG] 从数据库读取的原始行:', JSON.stringify(rows)); // 添加原始行日志 const mappedSettings = mapRowsToAppearanceSettings(rows); // 将键值对映射到设置对象 console.log(`[AppearanceRepo LOG] 映射后的 terminalBackgroundEnabled 值: ${mappedSettings.terminalBackgroundEnabled}`); // 添加映射后值的日志 return mappedSettings; } catch (err: any) { console.error('[AppearanceRepo] 获取外观设置失败:', err.message); throw new Error('获取外观设置失败'); } }; /** * 更新外观设置 (公共 API)。 * 接收一个包含要更新设置的 DTO,执行必要的验证,然后调用内部更新函数。 * @param {UpdateAppearanceDto} settingsDto - 包含要更新设置的对象。 * @returns {Promise} 如果至少有一个设置被成功更新或插入,则返回 true,否则返回 false。 * @throws {Error} 如果验证失败或内部更新过程中发生错误。 */ export const updateAppearanceSettings = async (settingsDto: UpdateAppearanceDto): Promise => { const db = await getDbInstance(); // 在调用内部更新之前,如果需要,执行验证或复杂逻辑 // 验证示例(已存在于服务中,但也可以在这里): if (settingsDto.activeTerminalThemeId !== undefined && settingsDto.activeTerminalThemeId !== null) { try { const themeExists = await findTerminalThemeById(settingsDto.activeTerminalThemeId); if (!themeExists) { throw new Error(`指定的终端主题 ID 不存在: ${settingsDto.activeTerminalThemeId}`); } } catch (validationError: any) { console.error(`[AppearanceRepo] 验证主题 ID ${settingsDto.activeTerminalThemeId} 时出错:`, validationError.message); throw new Error(`验证主题 ID 失败: ${validationError.message}`); } } // ... 其他验证 ... return updateAppearanceSettingsInternal(db, settingsDto); }; /** * 内部更新外观设置函数 (供内部调用,例如在初始化或公共 API 中)。 * 此函数直接与数据库交互,使用 INSERT OR REPLACE 来更新或插入键值对。 * @param {sqlite3.Database} db - 活动的数据库实例。 * @param {UpdateAppearanceDto} settingsDto - 包含要更新设置的对象。 * @returns {Promise} 如果至少有一个设置被成功更新或插入,则返回 true,否则返回 false。 * @throws {Error} 如果在数据库操作期间发生错误。 */ // 在键值表中更新设置的内部函数 const updateAppearanceSettingsInternal = async (db: sqlite3.Database, settingsDto: UpdateAppearanceDto): Promise => { const nowSeconds = Math.floor(Date.now() / 1000); const sqlReplace = `INSERT OR REPLACE INTO ${TABLE_NAME} (key, value, updated_at) VALUES (?, ?, ?)`; let changesMade = false; try { for (const key of Object.keys(settingsDto) as Array) { const value = settingsDto[key]; let dbValue: string; // 将值转换为字符串以存储到数据库,处理 null/undefined if (value === null || value === undefined) { dbValue = key === 'activeTerminalThemeId' ? 'null' : ''; // 主题 ID 特殊存储为 'null' } else if (typeof value === 'object') { dbValue = JSON.stringify(value); } else if (typeof value === 'boolean') { // 处理布尔值 dbValue = value ? 'true' : 'false'; } else { dbValue = String(value); } // 对 activeTerminalThemeId 的特殊处理:存储 'null' 字符串或数字字符串 if (key === 'activeTerminalThemeId') { dbValue = value === null ? 'null' : String(value); } // 保存前验证 active_terminal_theme_id 类型 if (key === 'activeTerminalThemeId' && value !== null && typeof value !== 'number') { console.error(`[AppearanceRepo] 更新 activeTerminalThemeId 时收到无效类型值: ${value} (类型: ${typeof value}),应为数字或 null。跳过此字段。`); continue; // 跳过此键 } // 对每个键值对执行 INSERT OR REPLACE console.log(`[AppearanceRepo LOG] 准备更新/插入键: '${key}', 值: '${dbValue}'`); // 添加保存日志 const result = await runDb(db, sqlReplace, [key, dbValue, nowSeconds]); if (result.changes > 0) { console.log(`[AppearanceRepo LOG] 键 '${key}' 更新成功。`); // 添加成功日志 changesMade = true; } } return changesMade; // 如果有任何行被插入或替换,则返回 true } catch (err: any) { console.error('[AppearanceRepo] 更新外观设置失败:', err.message); throw new Error('更新外观设置失败'); } };