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 0000000000..c4c65bb158 Binary files /dev/null and b/src/renderer/src/assets/images/apps/application.png differ 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 {