From 4c3c863c7d9294cb269558ca7b6d151f52d2e4cb Mon Sep 17 00:00:00 2001 From: George Zhao <38124587+CreatorZZY@users.noreply.github.com> Date: Thu, 8 May 2025 21:59:55 +0800 Subject: [PATCH] feat: Custom mini app (#5731) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 新增文件写入功能,支持通过 ID 写入文件并加载自定义小应用配置。 * feat(i18n): 添加自定义小程序配置的多语言支持,包括英文、简体中文和繁体中文。 * fix(minapps): 使用 await 加载自定义小应用并合并到默认应用中,同时添加日志输出以便调试 * fix(minapps): 在开发环境中添加条件日志输出,以便调试加载的默认小应用。 * refactor(miniappSettings): 移动自定义小应用编辑区域的位置,优化界面布局。 * refactor(miniappSettings): 修改自定义小应用保存逻辑,优化应用列表重新加载方式。 * feat(i18n): 修复在merge过程中丢失的语言设置。 * fix(bug_risk): Consider adding stricter validation for the JSON config on load. Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * feat(miniapp): 添加自定义小应用功能,优化应用列表展示,支持通过模态框添加新应用。 * feat(App): enhance custom app modal to support logo upload via URL. * feat(miniapp): add application logo support and update mini app list reloading logic. * feat(i18n): update mini app custom settings translations for multiple languages. * feat(miniapp): add updateDefaultMinApps function and refactor mini app list reloading logic. * feat(miniapp): add removeCustom functionality to handle custom mini app deletion * feat(miniapp): add duplicate ID check when adding custom mini apps. * feat(i18n): 重构侧边栏相关翻译为结构化格式,增加删除自定义应用的翻译支持。 * feat(miniapp): 优化删除自定义应用的逻辑,使用条件渲染简化代码结构。 * feat(miniapp): 添加自定义小应用内容的空值处理,确保 JSON 格式有效。 * feat(i18n): 更新默认语言为英语,并移除多个语言文件中的默认代理字段。 * feat(i18n): 为多个语言文件添加自定义小应用配置编辑描述翻译。 * feat(i18n): add success and error messages for deleting custom mini apps in multiple language files. * feat(i18n): update success and error messages for custom mini app operations and add placeholder text in multiple language files. * feat(i18n): 为多个语言文件添加重复ID和冲突ID的错误信息翻译,并在自定义小应用设置中实现相关检查逻辑。 * feat(miniapp): 在添加自定义小应用时,增加对默认最小应用ID的重复检查逻辑。 * feat(i18n): update edit description for custom mini app configuration in Traditional Chinese locale * fix(miniapp): enhance error messages for duplicate and conflicting IDs in custom mini app configuration --------- Co-authored-by: George Zhao Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Co-authored-by: suyao --- packages/shared/IpcChannel.ts | 1 + scripts/check-i18n.js | 2 +- src/main/ipc.ts | 1 + src/main/services/FileStorage.ts | 34 ++- src/preload/index.ts | 1 + .../src/assets/images/apps/application.png | Bin 0 -> 10909 bytes .../src/components/Popups/MinAppsPopover.tsx | 1 + src/renderer/src/config/minapps.ts | 45 +++- src/renderer/src/i18n/locales/en-us.json | 58 +++- src/renderer/src/i18n/locales/ja-jp.json | 58 +++- src/renderer/src/i18n/locales/ru-ru.json | 58 +++- src/renderer/src/i18n/locales/zh-cn.json | 56 +++- src/renderer/src/i18n/locales/zh-tw.json | 56 +++- src/renderer/src/pages/apps/App.tsx | 250 ++++++++++++++++-- src/renderer/src/pages/apps/AppsPage.tsx | 3 +- .../MiniappSettings/MiniAppSettings.tsx | 133 +++++++++- src/renderer/src/types/index.ts | 2 + 17 files changed, 692 insertions(+), 67 deletions(-) create mode 100644 src/renderer/src/assets/images/apps/application.png diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 2daa380e2e..38aadf9233 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -104,6 +104,7 @@ export enum IpcChannel { File_SelectFolder = 'file:selectFolder', File_Create = 'file:create', File_Write = 'file:write', + File_WriteWithId = 'file:writeWithId', File_SaveImage = 'file:saveImage', File_Base64Image = 'file:base64Image', File_Download = 'file:download', diff --git a/scripts/check-i18n.js b/scripts/check-i18n.js index 411ce4d558..dd36c2670d 100644 --- a/scripts/check-i18n.js +++ b/scripts/check-i18n.js @@ -3,7 +3,7 @@ Object.defineProperty(exports, '__esModule', { value: true }) var fs = require('fs') var path = require('path') var translationsDir = path.join(__dirname, '../src/renderer/src/i18n/locales') -var baseLocale = 'zh-CN' +var baseLocale = 'en-us' var baseFileName = ''.concat(baseLocale, '.json') var baseFilePath = path.join(translationsDir, baseFileName) /** diff --git a/src/main/ipc.ts b/src/main/ipc.ts index b399abea30..e26b623198 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -206,6 +206,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ipcMain.handle(IpcChannel.File_SelectFolder, fileManager.selectFolder) ipcMain.handle(IpcChannel.File_Create, fileManager.createTempFile) ipcMain.handle(IpcChannel.File_Write, fileManager.writeFile) + ipcMain.handle(IpcChannel.File_WriteWithId, fileManager.writeFileWithId) ipcMain.handle(IpcChannel.File_SaveImage, fileManager.saveImage) ipcMain.handle(IpcChannel.File_Base64Image, fileManager.base64Image) ipcMain.handle(IpcChannel.File_Base64File, fileManager.base64File) diff --git a/src/main/services/FileStorage.ts b/src/main/services/FileStorage.ts index 2aeb441538..697eb6dd7c 100644 --- a/src/main/services/FileStorage.ts +++ b/src/main/services/FileStorage.ts @@ -28,11 +28,16 @@ class FileStorage { } private initStorageDir = (): void => { - if (!fs.existsSync(this.storageDir)) { - fs.mkdirSync(this.storageDir, { recursive: true }) - } - if (!fs.existsSync(this.tempDir)) { - fs.mkdirSync(this.tempDir, { recursive: true }) + try { + if (!fs.existsSync(this.storageDir)) { + fs.mkdirSync(this.storageDir, { recursive: true }) + } + if (!fs.existsSync(this.tempDir)) { + fs.mkdirSync(this.tempDir, { recursive: true }) + } + } catch (error) { + logger.error('[FileStorage] Failed to initialize storage directories:', error) + throw error } } @@ -475,6 +480,25 @@ class FileStorage { throw error } } + + public writeFileWithId = async (_: Electron.IpcMainInvokeEvent, id: string, content: string): Promise => { + try { + const filePath = path.join(this.storageDir, id) + logger.info('[FileStorage] Writing file:', filePath) + + // 确保目录存在 + if (!fs.existsSync(this.storageDir)) { + logger.info('[FileStorage] Creating storage directory:', this.storageDir) + fs.mkdirSync(this.storageDir, { recursive: true }) + } + + await fs.promises.writeFile(filePath, content, 'utf8') + logger.info('[FileStorage] File written successfully:', filePath) + } catch (error) { + logger.error('[FileStorage] Failed to write file:', error) + throw error + } + } } export default FileStorage diff --git a/src/preload/index.ts b/src/preload/index.ts index ce33f67166..425a052c42 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -57,6 +57,7 @@ const api = { get: (filePath: string) => ipcRenderer.invoke(IpcChannel.File_Get, filePath), create: (fileName: string) => ipcRenderer.invoke(IpcChannel.File_Create, fileName), write: (filePath: string, data: Uint8Array | string) => ipcRenderer.invoke(IpcChannel.File_Write, filePath, data), + writeWithId: (id: string, content: string) => ipcRenderer.invoke(IpcChannel.File_WriteWithId, id, content), open: (options?: OpenDialogOptions) => ipcRenderer.invoke(IpcChannel.File_Open, options), openPath: (path: string) => ipcRenderer.invoke(IpcChannel.File_OpenPath, path), save: (path: string, content: string | NodeJS.ArrayBufferView, options?: any) => diff --git a/src/renderer/src/assets/images/apps/application.png b/src/renderer/src/assets/images/apps/application.png new file mode 100644 index 0000000000000000000000000000000000000000..c4c65bb1588a38a0f1a9777151532e03ddbc00e6 GIT binary patch literal 10909 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 literal 0 HcmV?d00001 diff --git a/src/renderer/src/components/Popups/MinAppsPopover.tsx b/src/renderer/src/components/Popups/MinAppsPopover.tsx index 28a9621bc9..effdf1189c 100644 --- a/src/renderer/src/components/Popups/MinAppsPopover.tsx +++ b/src/renderer/src/components/Popups/MinAppsPopover.tsx @@ -51,6 +51,7 @@ const MinAppsPopover: FC = ({ children }) => { )} + ) diff --git a/src/renderer/src/config/minapps.ts b/src/renderer/src/config/minapps.ts index 195514a84e..25e2a3c003 100644 --- a/src/renderer/src/config/minapps.ts +++ b/src/renderer/src/config/minapps.ts @@ -1,3 +1,4 @@ +import ApplicationLogo from '@renderer/assets/images/apps/application.png?url' import ThreeMinTopAppLogo from '@renderer/assets/images/apps/3mintop.png?url' import AbacusLogo from '@renderer/assets/images/apps/abacus.webp?url' import AIStudioLogo from '@renderer/assets/images/apps/aistudio.svg?url' @@ -53,7 +54,36 @@ import GroqProviderLogo from '@renderer/assets/images/providers/groq.png?url' import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png?url' import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png?url' import { MinAppType } from '@renderer/types' -export const DEFAULT_MIN_APPS: MinAppType[] = [ + +// 加载自定义小应用 +const loadCustomMiniApp = async (): Promise => { + try { + let content: string + try { + content = await window.api.file.read('customMiniAPP') + } catch (error) { + // 如果文件不存在,创建一个空的 JSON 数组 + content = '[]' + await window.api.file.writeWithId('customMiniAPP', content) + } + + const customApps = JSON.parse(content) + const now = new Date().toISOString() + + return customApps.map((app: any) => ({ + ...app, + type: 'Custom', + logo: app.logo && app.logo !== '' ? app.logo : ApplicationLogo, + addTime: app.addTime || now + })) + } catch (error) { + console.error('Failed to load custom mini apps:', error) + return [] + } +} + +// 初始化默认小应用 +const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [ { id: 'openai', name: 'ChatGPT', @@ -420,3 +450,16 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [ } } ] + +// 加载自定义小应用并合并到默认应用中 +let DEFAULT_MIN_APPS = [...ORIGIN_DEFAULT_MIN_APPS, ...(await loadCustomMiniApp())] + +function updateDefaultMinApps(param) { + DEFAULT_MIN_APPS = param +} + +if (process.env.NODE_ENV === 'development') { + console.log('DEFAULT_MIN_APPS', DEFAULT_MIN_APPS) +} + +export { DEFAULT_MIN_APPS, loadCustomMiniApp, ORIGIN_DEFAULT_MIN_APPS, updateDefaultMinApps } diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 18c2690649..5d51d0d83c 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -629,11 +629,26 @@ "open_link_external_on": "Current: Open links in browser", "open_link_external_off": "Current: Open links in default window" }, - "sidebar.add.title": "Add to sidebar", - "sidebar.remove.title": "Remove from sidebar", - "sidebar.close.title": "Close", - "sidebar.closeall.title": "Close All", - "sidebar.hide.title": "Hide MinApp", + "sidebar": { + "add": { + "title": "Add to Sidebar" + }, + "remove": { + "title": "Remove from Sidebar" + }, + "remove_custom": { + "title": "Delete Custom App" + }, + "hide": { + "title": "Hide" + }, + "close": { + "title": "Close" + }, + "closeall": { + "title": "Close All" + } + }, "title": "MinApp" }, "miniwindow": { @@ -1111,6 +1126,37 @@ "display.topic.title": "Topic Settings", "miniapps": { "title": "Mini Apps Settings", + "custom": { + "title": "Custom Mini App", + "edit_title": "Edit Custom Mini App", + "save_success": "Custom mini app saved successfully.", + "save_error": "Failed to save custom mini app.", + "remove_success": "Custom mini app removed successfully.", + "remove_error": "Failed to remove custom mini app.", + "logo_upload_success": "Logo uploaded successfully.", + "logo_upload_error": "Failed to upload logo.", + "id": "ID", + "id_error": "ID is required.", + "id_placeholder": "Enter ID", + "name": "Name", + "name_error": "Name is required.", + "name_placeholder": "Enter name", + "url": "URL", + "url_error": "URL is required.", + "url_placeholder": "Enter URL", + "logo": "Logo", + "logo_url": "Logo URL", + "logo_file": "Upload Logo File", + "logo_url_label": "Logo URL", + "logo_url_placeholder": "Enter logo URL", + "logo_upload_label": "Upload Logo", + "logo_upload_button": "Upload", + "save": "Save", + "edit_description": "Edit custom mini app configuration here. Each app should include id, name, url, and logo fields.", + "placeholder": "Enter custom mini app configuration (JSON format)", + "duplicate_ids": "Duplicate IDs found: {{ids}}", + "conflicting_ids": "Conflicting IDs with default apps: {{ids}}" + }, "disabled": "Hidden Mini Apps", "empty": "Drag mini apps from the left to hide them", "visible": "Visible Mini Apps", @@ -1586,4 +1632,4 @@ "visualization": "Visualization" } } -} +} \ No newline at end of file diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 56d79a7124..93c1ac3c45 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -629,11 +629,26 @@ "open_link_external_on": "現在:ブラウザで開く", "open_link_external_off": "現在:デフォルトのウィンドウで開く" }, - "sidebar.add.title": "サイドバーに追加", - "sidebar.remove.title": "サイドバーから削除", - "sidebar.close.title": "閉じる", - "sidebar.closeall.title": "すべて閉じる", - "sidebar.hide.title": "ミニアプリを隠す", + "sidebar": { + "add": { + "title": "サイドバーに追加" + }, + "remove": { + "title": "サイドバーから削除" + }, + "remove_custom": { + "title": "カスタムアプリを削除" + }, + "hide": { + "title": "非表示" + }, + "close": { + "title": "閉じる" + }, + "closeall": { + "title": "すべて閉じる" + } + }, "title": "ミニアプリ" }, "miniwindow": { @@ -1124,7 +1139,38 @@ "display_title": "ミニアプリ表示設定", "sidebar_title": "サイドバーのアクティブなミニアプリ表示", "sidebar_description": "サイドバーにアクティブなミニアプリを表示するかどうかを設定します", - "cache_change_notice": "設定値に達するまでミニアプリの開閉が行われた後に変更が適用されます" + "cache_change_notice": "設定値に達するまでミニアプリの開閉が行われた後に変更が適用されます", + "custom": { + "title": "カスタムミニアプリ", + "edit_title": "カスタムミニアプリの編集", + "save_success": "カスタムミニアプリの保存に成功しました。", + "save_error": "カスタムミニアプリの保存に失敗しました。", + "remove_success": "カスタムミニアプリの削除に成功しました。", + "remove_error": "カスタムミニアプリの削除に失敗しました。", + "logo_upload_success": "ロゴのアップロードに成功しました。", + "logo_upload_error": "ロゴのアップロードに失敗しました。", + "id": "ID", + "id_error": "IDは必須項目です。", + "id_placeholder": "IDを入力してください", + "name": "名前", + "name_error": "名前は必須項目です。", + "name_placeholder": "名前を入力してください", + "url": "URL", + "url_error": "URLは必須項目です。", + "url_placeholder": "URLを入力してください", + "logo": "ロゴ", + "logo_url": "ロゴURL", + "logo_file": "ロゴファイルをアップロード", + "logo_url_label": "ロゴURL", + "logo_url_placeholder": "ロゴURLを入力してください", + "logo_upload_label": "ロゴをアップロード", + "logo_upload_button": "アップロード", + "save": "保存", + "edit_description": "ここでカスタムミニアプリの設定を編集します。各アプリにはid、name、url、logoフィールドが必要です。", + "placeholder": "カスタムミニアプリの設定を入力してください(JSON形式)", + "duplicate_ids": "重複するIDが見つかりました: {{ids}}", + "conflicting_ids": "デフォルトアプリとIDが競合しています: {{ids}}" + } }, "font_size.title": "メッセージのフォントサイズ", "general": "一般設定", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index ea8735f2b9..1fd1a31ed6 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -629,11 +629,26 @@ "open_link_external_on": "Текущий: Открыть ссылки в браузере", "open_link_external_off": "Текущий: Открыть ссылки в окне по умолчанию" }, - "sidebar.add.title": "Добавить в боковую панель", - "sidebar.remove.title": "Удалить из боковой панели", - "sidebar.close.title": "Закрыть", - "sidebar.closeall.title": "Закрыть все", - "sidebar.hide.title": "Скрыть приложение", + "sidebar": { + "add": { + "title": "Добавить в боковую панель" + }, + "remove": { + "title": "Удалить из боковой панели" + }, + "remove_custom": { + "title": "Удалить пользовательское приложение" + }, + "hide": { + "title": "Скрыть" + }, + "close": { + "title": "Закрыть" + }, + "closeall": { + "title": "Закрыть все" + } + }, "title": "Встроенные приложения" }, "miniwindow": { @@ -1124,7 +1139,38 @@ "display_title": "Настройки отображения мини-приложений", "sidebar_title": "Отображение активных мини-приложений в боковой панели", "sidebar_description": "Настройка отображения активных мини-приложений в боковой панели", - "cache_change_notice": "Изменения вступят в силу, когда количество открытых мини-приложений достигнет установленного значения" + "cache_change_notice": "Изменения вступят в силу, когда количество открытых мини-приложений достигнет установленного значения", + "custom": { + "save_success": "Пользовательское мини-приложение успешно сохранено.", + "save_error": "Не удалось сохранить пользовательское мини-приложение.", + "logo_upload_success": "Логотип успешно загружен.", + "logo_upload_error": "Не удалось загрузить логотип.", + "title": "Пользовательские мини-приложения", + "edit_title": "Редактировать пользовательское мини-приложение", + "id": "ID", + "remove_success": "Мини-приложение успешно удалено.", + "remove_error": "Не удалось удалить мини-приложение.", + "id_error": "ID обязателен.", + "id_placeholder": "Введите ID", + "name": "Имя", + "name_error": "Имя обязательно.", + "name_placeholder": "Введите имя", + "url": "URL", + "url_error": "URL обязателен.", + "url_placeholder": "Введите URL", + "logo": "Логотип", + "logo_url": "URL логотипа", + "logo_file": "Загрузить файл логотипа", + "logo_url_label": "URL логотипа", + "logo_url_placeholder": "Введите URL логотипа", + "logo_upload_label": "Загрузить логотип", + "logo_upload_button": "Загрузить", + "save": "Сохранить", + "edit_description": "Здесь вы можете редактировать конфигурации пользовательских мини-приложений. Каждое приложение должно содержать поля id, name, url и logo.", + "placeholder": "Введите конфигурацию мини-приложения (формат JSON)", + "duplicate_ids": "Найдены повторяющиеся ID: {{ids}}", + "conflicting_ids": "Конфликт ID с приложениями по умолчанию: {{ids}}" + } }, "font_size.title": "Размер шрифта сообщений", "general": "Общие настройки", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 6ea64e00f5..4e73f16411 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -629,11 +629,26 @@ "open_link_external_on": "当前:在浏览器中打开链接", "open_link_external_off": "当前:使用默认窗口打开链接" }, - "sidebar.add.title": "添加到侧边栏", - "sidebar.remove.title": "从侧边栏移除", - "sidebar.close.title": "关闭", - "sidebar.closeall.title": "全部关闭", - "sidebar.hide.title": "隐藏小程序", + "sidebar": { + "add": { + "title": "添加到侧边栏" + }, + "remove": { + "title": "从侧边栏移除" + }, + "remove_custom": { + "title": "删除自定义应用" + }, + "hide": { + "title": "隐藏" + }, + "close": { + "title": "关闭" + }, + "closeall": { + "title": "关闭所有" + } + }, "title": "小程序" }, "miniwindow": { @@ -1117,6 +1132,37 @@ "open_link_external": { "title": "在浏览器中打开新窗口链接" }, + "custom": { + "title": "自定义小程序", + "edit_title": "编辑自定义小程序", + "save_success": "自定义小程序保存成功。", + "save_error": "自定义小程序保存失败。", + "remove_success": "自定义小程序删除成功。", + "remove_error": "自定义小程序删除失败。", + "logo_upload_success": "Logo 上传成功。", + "logo_upload_error": "Logo 上传失败。", + "id": "ID", + "id_error": "ID 是必填项。", + "id_placeholder": "请输入 ID", + "name": "名称", + "name_error": "名称是必填项。", + "name_placeholder": "请输入名称", + "url": "URL", + "url_error": "URL 是必填项。", + "url_placeholder": "请输入 URL", + "logo": "Logo", + "logo_url": "Logo URL", + "logo_file": "上传 Logo 文件", + "logo_url_label": "Logo URL", + "logo_url_placeholder": "请输入 Logo URL", + "logo_upload_label": "上传 Logo", + "logo_upload_button": "上传", + "save": "保存", + "edit_description": "在这里编辑自定义小应用的配置。每个应用需要包含 id、name、url 和 logo 字段。", + "placeholder": "请输入自定义小程序配置(JSON格式)", + "duplicate_ids": "发现重复的ID: {{ids}}", + "conflicting_ids": "与默认应用ID冲突: {{ids}}" + }, "cache_settings": "缓存设置", "cache_title": "小程序缓存数量", "cache_description": "设置同时保持活跃状态的小程序最大数量", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 3f3cf34c0d..440953a5a7 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -629,11 +629,26 @@ "open_link_external_on": "当前:在瀏覽器中開啟連結", "open_link_external_off": "当前:使用預設視窗開啟連結" }, - "sidebar.add.title": "新增到側邊欄", - "sidebar.remove.title": "從側邊欄移除", - "sidebar.close.title": "關閉", - "sidebar.closeall.title": "全部關閉", - "sidebar.hide.title": "隱藏小工具", + "sidebar": { + "add": { + "title": "添加到側邊欄" + }, + "remove": { + "title": "從側邊欄移除" + }, + "remove_custom": { + "title": "刪除自定義應用" + }, + "hide": { + "title": "隱藏" + }, + "close": { + "title": "關閉" + }, + "closeall": { + "title": "關閉所有" + } + }, "title": "小工具" }, "miniwindow": { @@ -1117,6 +1132,37 @@ "open_link_external": { "title": "在瀏覽器中打開新視窗連結" }, + "custom": { + "duplicate_ids": "發現重複的ID: {{ids}}", + "conflicting_ids": "與預設應用ID衝突: {{ids}}", + "title": "自定義小程序", + "edit_title": "編輯自定義小程序", + "save_success": "自定義小程序保存成功。", + "save_error": "自定義小程序保存失敗。", + "remove_success": "自定義小程序刪除成功。", + "remove_error": "自定義小程序刪除失敗。", + "logo_upload_success": "Logo 上傳成功。", + "logo_upload_error": "Logo 上傳失敗。", + "id": "ID", + "id_error": "ID 是必填項。", + "id_placeholder": "請輸入 ID", + "name": "名稱", + "name_error": "名稱是必填項。", + "name_placeholder": "請輸入名稱", + "url": "URL", + "url_error": "URL 是必填項。", + "url_placeholder": "請輸入 URL", + "logo": "Logo", + "logo_url": "Logo URL", + "logo_file": "上傳 Logo 文件", + "logo_url_label": "Logo URL", + "logo_url_placeholder": "請輸入 Logo URL", + "logo_upload_label": "上傳 Logo", + "logo_upload_button": "上傳", + "save": "保存", + "placeholder": "請輸入自定義小程序配置(JSON格式)", + "edit_description": "編輯自定義小程序配置" + }, "cache_settings": "緩存設置", "cache_title": "小程式緩存數量", "cache_description": "設置同時保持活躍狀態的小程式最大數量", diff --git a/src/renderer/src/pages/apps/App.tsx b/src/renderer/src/pages/apps/App.tsx index d9d46db474..cbf6ec75c6 100644 --- a/src/renderer/src/pages/apps/App.tsx +++ b/src/renderer/src/pages/apps/App.tsx @@ -1,10 +1,13 @@ +import { PlusOutlined, UploadOutlined } from '@ant-design/icons' import MinAppIcon from '@renderer/components/Icons/MinAppIcon' +import { loadCustomMiniApp, ORIGIN_DEFAULT_MIN_APPS, updateDefaultMinApps } from '@renderer/config/minapps' import { useMinappPopup } from '@renderer/hooks/useMinappPopup' import { useMinapps } from '@renderer/hooks/useMinapps' import { MinAppType } from '@renderer/types' import type { MenuProps } from 'antd' -import { Dropdown } from 'antd' -import { FC } from 'react' +import { Button, Dropdown, Form, Input, message, Modal, Radio, Upload } from 'antd' +import type { UploadFile } from 'antd/es/upload/interface' +import { FC, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -12,52 +15,220 @@ interface Props { app: MinAppType onClick?: () => void size?: number + isLast?: boolean } -const App: FC = ({ app, onClick, size = 60 }) => { +const App: FC = ({ app, onClick, size = 60, isLast }) => { const { openMinappKeepAlive } = useMinappPopup() const { t } = useTranslation() const { minapps, pinned, disabled, updateMinapps, updateDisabledMinapps, updatePinnedMinapps } = useMinapps() const isPinned = pinned.some((p) => p.id === app.id) const isVisible = minapps.some((m) => m.id === app.id) + const [isModalVisible, setIsModalVisible] = useState(false) + const [form] = Form.useForm() + const [logoType, setLogoType] = useState<'url' | 'file'>('url') + const [fileList, setFileList] = useState([]) const handleClick = () => { + if (isLast) { + setIsModalVisible(true) + return + } openMinappKeepAlive(app) onClick?.() } - const menuItems: MenuProps['items'] = [ - { - key: 'togglePin', - label: isPinned ? t('minapp.sidebar.remove.title') : t('minapp.sidebar.add.title'), - onClick: () => { - const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...(pinned || []), app] - updatePinnedMinapps(newPinned) + const handleAddCustomApp = async (values: any) => { + try { + const content = await window.api.file.read('customMiniAPP') + const customApps = JSON.parse(content) + + // Check for duplicate ID + if (customApps.some((app: MinAppType) => app.id === values.id)) { + message.error(t('settings.miniapps.custom.duplicate_ids', { ids: values.id })) + return } - }, - { - key: 'hide', - label: t('minapp.sidebar.hide.title'), - onClick: () => { - const newMinapps = minapps.filter((item) => item.id !== app.id) - updateMinapps(newMinapps) - const newDisabled = [...(disabled || []), app] - updateDisabledMinapps(newDisabled) - const newPinned = pinned.filter((item) => item.id !== app.id) - updatePinnedMinapps(newPinned) + if (ORIGIN_DEFAULT_MIN_APPS.some((app: MinAppType) => app.id === values.id)) { + message.error(t('settings.miniapps.custom.conflicting_ids', { ids: values.id })) + return + } + + const newApp = { + id: values.id, + name: values.name, + url: values.url, + logo: form.getFieldValue('logo') || '', + type: 'Custom', + addTime: new Date().toISOString() + } + customApps.push(newApp) + await window.api.file.writeWithId('customMiniAPP', JSON.stringify(customApps, null, 2)) + message.success(t('settings.miniapps.custom.save_success')) + setIsModalVisible(false) + form.resetFields() + setFileList([]) + // 重新加载应用列表 + const reloadedApps = [...ORIGIN_DEFAULT_MIN_APPS, ...(await loadCustomMiniApp())] + updateDefaultMinApps(reloadedApps) + updateMinapps(reloadedApps) + } catch (error) { + message.error(t('settings.miniapps.custom.save_error')) + console.error('Failed to save custom mini app:', error) + } + } + + const handleLogoTypeChange = (e: any) => { + setLogoType(e.target.value) + form.setFieldValue('logo', '') + setFileList([]) + } + + const handleFileChange = async (info: any) => { + console.log(info) + const file = info.fileList[info.fileList.length - 1]?.originFileObj + console.log(file) + setFileList(info.fileList.slice(-1)) + + if (file) { + try { + const reader = new FileReader() + reader.onload = (event) => { + const base64Data = event.target?.result + if (typeof base64Data === 'string') { + message.success(t('settings.miniapps.custom.logo_upload_success')) + form.setFieldValue('logo', base64Data) + } + } + reader.readAsDataURL(file) + } catch (error) { + console.error('Failed to read file:', error) + message.error(t('settings.miniapps.custom.logo_upload_error')) } } - ] + } - if (!isVisible) return null + const menuItems: MenuProps['items'] = isLast + ? [] + : [ + { + key: 'togglePin', + label: isPinned ? t('minapp.sidebar.remove.title') : t('minapp.sidebar.add.title'), + onClick: () => { + const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...(pinned || []), app] + updatePinnedMinapps(newPinned) + } + }, + { + key: 'hide', + label: t('minapp.sidebar.hide.title'), + onClick: () => { + const newMinapps = minapps.filter((item) => item.id !== app.id) + updateMinapps(newMinapps) + const newDisabled = [...(disabled || []), app] + updateDisabledMinapps(newDisabled) + const newPinned = pinned.filter((item) => item.id !== app.id) + updatePinnedMinapps(newPinned) + } + }, + ...(app.type === 'Custom' + ? [ + { + key: 'removeCustom', + label: t('minapp.sidebar.remove_custom.title'), + danger: true, + onClick: async () => { + try { + const content = await window.api.file.read('customMiniAPP') + const customApps = JSON.parse(content) + const updatedApps = customApps.filter((customApp: MinAppType) => customApp.id !== app.id) + await window.api.file.writeWithId('customMiniAPP', JSON.stringify(updatedApps, null, 2)) + message.success(t('settings.miniapps.custom.remove_success')) + const reloadedApps = [...ORIGIN_DEFAULT_MIN_APPS, ...(await loadCustomMiniApp())] + updateDefaultMinApps(reloadedApps) + updateMinapps(reloadedApps) + } catch (error) { + message.error(t('settings.miniapps.custom.remove_error')) + console.error('Failed to remove custom mini app:', error) + } + } + } + ] + : []) + ] + + if (!isVisible && !isLast) return null return ( - - - - {app.name} - - + <> + + + {isLast ? ( + + + + ) : ( + + )} + {isLast ? t('settings.miniapps.custom.title') : app.name} + + + { + setIsModalVisible(false) + setFileList([]) + }} + footer={null}> +
+ + + + + + + + + + + + {t('settings.miniapps.custom.logo_url')} + {t('settings.miniapps.custom.logo_file')} + + + {logoType === 'url' ? ( + + + + ) : ( + + false}> + + + + )} + + + +
+
+ ) } @@ -79,4 +250,25 @@ const AppTitle = styled.div` white-space: nowrap; ` +const AddButton = styled.div` + width: 60px; + height: 60px; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + background: var(--color-background-soft); + border: 1px dashed var(--color-border); + color: var(--color-text-soft); + font-size: 24px; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: var(--color-background); + border-color: var(--color-primary); + color: var(--color-primary); + } +` + export default App diff --git a/src/renderer/src/pages/apps/AppsPage.tsx b/src/renderer/src/pages/apps/AppsPage.tsx index e61def4972..3ffecd9bcd 100644 --- a/src/renderer/src/pages/apps/AppsPage.tsx +++ b/src/renderer/src/pages/apps/AppsPage.tsx @@ -23,7 +23,7 @@ const AppsPage: FC = () => { // Calculate the required number of lines const itemsPerRow = Math.floor(930 / 115) // Maximum width divided by the width of each item (including spacing) - const rowCount = Math.ceil(filteredApps.length / itemsPerRow) + const rowCount = Math.ceil((filteredApps.length + 1) / itemsPerRow) // +1 for the add button // Each line height is 85px (60px icon + 5px margin + 12px text + spacing) const containerHeight = rowCount * 85 + (rowCount - 1) * 25 // 25px is the line spacing. @@ -60,6 +60,7 @@ const AppsPage: FC = () => { {filteredApps.map((app) => ( ))} + )} diff --git a/src/renderer/src/pages/settings/MiniappSettings/MiniAppSettings.tsx b/src/renderer/src/pages/settings/MiniappSettings/MiniAppSettings.tsx index 84b4195835..6dd3d52963 100644 --- a/src/renderer/src/pages/settings/MiniappSettings/MiniAppSettings.tsx +++ b/src/renderer/src/pages/settings/MiniappSettings/MiniAppSettings.tsx @@ -1,5 +1,10 @@ import { UndoOutlined } from '@ant-design/icons' // 导入重置图标 -import { DEFAULT_MIN_APPS } from '@renderer/config/minapps' +import { + DEFAULT_MIN_APPS, + loadCustomMiniApp, + ORIGIN_DEFAULT_MIN_APPS, + updateDefaultMinApps +} from '@renderer/config/minapps' import { useTheme } from '@renderer/context/ThemeProvider' import { useMinapps } from '@renderer/hooks/useMinapps' import { useSettings } from '@renderer/hooks/useSettings' @@ -9,7 +14,7 @@ import { setMinappsOpenLinkExternal, setShowOpenedMinappsInSidebar } from '@renderer/store/settings' -import { Button, message, Slider, Switch, Tooltip } from 'antd' +import { Button, Input, message, Slider, Switch, Tooltip } from 'antd' import { FC, useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -31,6 +36,92 @@ const MiniAppSettings: FC = () => { const [disabledMiniApps, setDisabledMiniApps] = useState(disabled || []) const [messageApi, contextHolder] = message.useMessage() const debounceTimerRef = useRef(null) + const [customMiniAppContent, setCustomMiniAppContent] = useState('[]') + + // 加载自定义小应用配置 + useEffect(() => { + const loadCustomMiniApp = async () => { + try { + const content = await window.api.file.read('customMiniAPP') + let validContent = '[]' + try { + const parsed = JSON.parse(content) + validContent = JSON.stringify(parsed) + } catch (e) { + console.error('Invalid JSON format in custom mini app config:', e) + } + setCustomMiniAppContent(validContent) + } catch (error) { + console.error('Failed to load custom mini app config:', error) + setCustomMiniAppContent('[]') + } + } + loadCustomMiniApp() + }, []) + + // 保存自定义小应用配置 + const handleSaveCustomMiniApp = useCallback(async () => { + try { + // 验证 JSON 格式 + if (customMiniAppContent === '') { + setCustomMiniAppContent('[]') + } + const parsedContent = JSON.parse(customMiniAppContent) + // 确保是数组 + if (!Array.isArray(parsedContent)) { + throw new Error('Content must be an array') + } + + // 检查自定义应用中的重复ID + const customIds = new Set() + const duplicateIds = new Set() + parsedContent.forEach((app: any) => { + if (app.id) { + if (customIds.has(app.id)) { + duplicateIds.add(app.id) + } + customIds.add(app.id) + } + }) + + // 检查与默认应用的ID重复 + const defaultIds = new Set(ORIGIN_DEFAULT_MIN_APPS.map((app) => app.id)) + const conflictingIds = new Set() + customIds.forEach((id) => { + if (defaultIds.has(id)) { + conflictingIds.add(id) + } + }) + + // 如果有重复ID,显示错误信息 + if (duplicateIds.size > 0 || conflictingIds.size > 0) { + let errorMessage = '' + if (duplicateIds.size > 0) { + errorMessage += t('settings.miniapps.custom.duplicate_ids', { ids: Array.from(duplicateIds).join(', ') }) + } + if (conflictingIds.size > 0) { + console.log('conflictingIds', Array.from(conflictingIds)) + if (errorMessage) errorMessage += '\n' + errorMessage += t('settings.miniapps.custom.conflicting_ids', { ids: Array.from(conflictingIds).join(', ') }) + } + messageApi.error(errorMessage) + return + } + + // 保存文件 + await window.api.file.writeWithId('customMiniAPP', customMiniAppContent) + messageApi.success(t('settings.miniapps.custom.save_success')) + // 重新加载应用列表 + console.log('Reloading mini app list...') + const reloadedApps = [...ORIGIN_DEFAULT_MIN_APPS, ...(await loadCustomMiniApp())] + updateDefaultMinApps(reloadedApps) + console.log('Reloaded mini app list:', reloadedApps) + updateMinapps(reloadedApps) + } catch (error) { + messageApi.error(t('settings.miniapps.custom.save_error')) + console.error('Failed to save custom mini app config:', error) + } + }, [customMiniAppContent, messageApi, t, updateMinapps]) const handleResetMinApps = useCallback(() => { setVisibleMiniApps(DEFAULT_MIN_APPS) @@ -77,6 +168,7 @@ const MiniAppSettings: FC = () => { {t('settings.miniapps.title')} + {t('settings.miniapps.display_title')} @@ -143,6 +235,30 @@ const MiniAppSettings: FC = () => { onChange={(checked) => dispatch(setShowOpenedMinappsInSidebar(checked))} /> + + + + {t('settings.miniapps.custom.edit_title')} + {t('settings.miniapps.custom.edit_description')} + + + + setCustomMiniAppContent(e.target.value)} + placeholder={t('settings.miniapps.custom.placeholder')} + style={{ + minHeight: 200, + fontFamily: 'monospace', + backgroundColor: 'var(--color-bg-2)', + color: 'var(--color-text)', + borderColor: 'var(--color-border)' + }} + /> + + ) @@ -229,4 +345,17 @@ const BorderedContainer = styled.div` background-color: var(--color-bg-1); ` +// 新增自定义编辑器容器样式 +const CustomEditorContainer = styled.div` + margin: 8px 0; + padding: 8px; + border: 1px solid var(--color-border); + border-radius: 8px; + background-color: var(--color-bg-1); + + .ant-input { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace; + } +` + export default MiniAppSettings diff --git a/src/renderer/src/types/index.ts b/src/renderer/src/types/index.ts index 0bece1576f..3f05e727b9 100644 --- a/src/renderer/src/types/index.ts +++ b/src/renderer/src/types/index.ts @@ -258,6 +258,8 @@ export type MinAppType = { bodered?: boolean background?: string style?: React.CSSProperties + addTime?: string + type?: 'Custom' | 'Default' // Added the 'type' property } export interface FileType {