diff --git a/package.json b/package.json index a26cb1647c..153be63374 100644 --- a/package.json +++ b/package.json @@ -220,6 +220,7 @@ "axios": "^1.7.3", "browser-image-compression": "^2.0.2", "chardet": "^2.1.0", + "check-disk-space": "3.4.0", "cheerio": "^1.1.2", "chokidar": "^4.0.3", "cli-progress": "^3.12.0", diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 87243df0c7..fa02991682 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -35,6 +35,7 @@ export enum IpcChannel { App_InstallBunBinary = 'app:install-bun-binary', App_LogToMain = 'app:log-to-main', App_SaveData = 'app:save-data', + App_GetDiskInfo = 'app:get-disk-info', App_SetFullScreen = 'app:set-full-screen', App_IsFullScreen = 'app:is-full-screen', diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 875e70b656..6b36a96a35 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -12,6 +12,7 @@ import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core' import { MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH, UpgradeChannel } from '@shared/config/constant' import { IpcChannel } from '@shared/IpcChannel' import { FileMetadata, Provider, Shortcut, ThemeMode } from '@types' +import checkDiskSpace from 'check-disk-space' import { BrowserWindow, dialog, ipcMain, ProxyConfig, session, shell, systemPreferences, webContents } from 'electron' import { Notification } from 'src/renderer/src/types/notification' @@ -783,6 +784,20 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { addStreamMessage(spanId, modelName, context, msg) ) + ipcMain.handle(IpcChannel.App_GetDiskInfo, async (_, directoryPath: string) => { + try { + const diskSpace = await checkDiskSpace(directoryPath) // { free, size } in bytes + logger.debug('disk space', diskSpace) + const { free, size } = diskSpace + return { + free, + size + } + } catch (error) { + logger.error('check disk space error', error as Error) + return null + } + }) // API Server apiServerService.registerIpcHandlers() diff --git a/src/preload/index.ts b/src/preload/index.ts index 0c9e89ad76..d8c55dd256 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -44,6 +44,8 @@ export function tracedInvoke(channel: string, spanContext: SpanContext | undefin // Custom APIs for renderer const api = { getAppInfo: () => ipcRenderer.invoke(IpcChannel.App_Info), + getDiskInfo: (directoryPath: string): Promise<{ free: number; size: number } | null> => + ipcRenderer.invoke(IpcChannel.App_GetDiskInfo, directoryPath), reload: () => ipcRenderer.invoke(IpcChannel.App_Reload), setProxy: (proxy: string | undefined, bypassRules?: string) => ipcRenderer.invoke(IpcChannel.App_Proxy, proxy, bypassRules), diff --git a/src/renderer/src/hooks/useAppInit.ts b/src/renderer/src/hooks/useAppInit.ts index 1bbe278431..571e5877a8 100644 --- a/src/renderer/src/hooks/useAppInit.ts +++ b/src/renderer/src/hooks/useAppInit.ts @@ -12,6 +12,7 @@ import { handleSaveData } from '@renderer/store' import { selectMemoryConfig } from '@renderer/store/memory' import { setAvatar, setFilesPath, setResourcesPath, setUpdateState } from '@renderer/store/runtime' import { delay, runAsyncFunction } from '@renderer/utils' +import { checkDataLimit } from '@renderer/utils' import { defaultLanguage } from '@shared/config/constant' import { IpcChannel } from '@shared/IpcChannel' import { useLiveQuery } from 'dexie-react-hooks' @@ -159,4 +160,8 @@ export function useAppInit() { logger.error('Failed to update memory config:', error) }) }, [memoryConfig]) + + useEffect(() => { + checkDataLimit() + }, []) } diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 8ccab4db9d..b4dc172bd2 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -2650,6 +2650,10 @@ "url": "Joplin Web Clipper Service URL", "url_placeholder": "http://127.0.0.1:41184/" }, + "limit": { + "appDataDiskQuota": "Disk Space Warning", + "appDataDiskQuotaDescription": "Data directory space is almost full, please clear disk space, otherwise data will be lost" + }, "local": { "autoSync": { "label": "Auto Backup", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index f43a689c3d..5b8cae2695 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -132,6 +132,7 @@ }, "title": "API 服务器" }, + "assistants": { "abbr": "助手", "clear": { @@ -2650,6 +2651,10 @@ "url": "Joplin 剪裁服务监听 URL", "url_placeholder": "http://127.0.0.1:41184/" }, + "limit": { + "appDataDiskQuota": "磁盘空间警告", + "appDataDiskQuotaDescription": "数据目录空间即将用尽, 请清理磁盘空间, 否则会丢失数据" + }, "local": { "autoSync": { "label": "自动备份", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 28fb04eea4..40639664d2 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -2650,6 +2650,10 @@ "url": "Joplin 剪輯服務 URL", "url_placeholder": "http://127.0.0.1:41184/" }, + "limit": { + "appDataDiskQuota": "磁碟空間警告", + "appDataDiskQuotaDescription": "資料目錄空間即將用盡, 請清理磁碟空間, 否則會丟失數據" + }, "local": { "autoSync": { "label": "自動備份", diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json index 909a610784..c757be180a 100644 --- a/src/renderer/src/i18n/translate/el-gr.json +++ b/src/renderer/src/i18n/translate/el-gr.json @@ -538,6 +538,10 @@ "tip": "Στη γραμμή εργαλείων των εκτελέσιμων blocks κώδικα θα εμφανίζεται το κουμπί εκτέλεσης· προσέξτε να μην εκτελέσετε επικίνδυνο κώδικα!", "title": "Εκτέλεση Κώδικα" }, + "code_fancy_block": { + "label": "[to be translated]:花式代码块", + "tip": "[to be translated]:使用更美观的代码块样式,例如 HTML 卡片" + }, "code_image_tools": { "label": "Ενεργοποίηση εργαλείου προεπισκόπησης", "tip": "Ενεργοποίηση εργαλείου προεπισκόπησης για εικόνες που αποδίδονται από blocks κώδικα όπως το mermaid" @@ -1741,8 +1745,15 @@ "compress_content": "μείωση πλάτους στήλης", "compress_content_description": "Ενεργοποιώντας το, θα περιορίζεται ο αριθμός των χαρακτήρων ανά γραμμή, μειώνοντας την οθόνη που εμφανίζεται", "default_font": "προεπιλεγμένη γραμματοσειρά", + "font_size": "[to be translated]:字体大小", + "font_size_description": "[to be translated]:调整字体大小以获得更好的阅读体验 (10-30px)", + "font_size_large": "[to be translated]:大", + "font_size_medium": "[to be translated]:中", + "font_size_small": "[to be translated]:小", "font_title": "ρυθμίσεις γραμματοσειράς", "serif_font": "σειρά γραμματοσειρών", + "show_table_of_contents": "[to be translated]:显示目录大纲", + "show_table_of_contents_description": "[to be translated]:显示目录大纲侧边栏,方便文档内导航", "title": "ρυθμίσεις εμφάνισης" }, "editor": { @@ -2639,6 +2650,10 @@ "url": "URL υπηρεσίας περικοπής Joplin", "url_placeholder": "http://127.0.0.1:41184/" }, + "limit": { + "appDataDiskQuota": "Προειδοποίηση χώρου δίσκου", + "appDataDiskQuotaDescription": "Ο κατάλογος δεδομένων της εφαρμογής είναι σχεδόν γεμάτος, παρακαλώ απομακρύνετε τον χώρο δίσκου, αλλιώς θα χαθούν τα δεδομένα" + }, "local": { "autoSync": { "label": "Αυτόματο αντίγραφο ασφαλείας", diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json index 353fcf35b4..49119b7d22 100644 --- a/src/renderer/src/i18n/translate/es-es.json +++ b/src/renderer/src/i18n/translate/es-es.json @@ -538,6 +538,10 @@ "tip": "En la barra de herramientas de bloques de código ejecutables se mostrará un botón de ejecución. ¡Tenga cuidado en no ejecutar código peligroso!", "title": "Ejecución de Código" }, + "code_fancy_block": { + "label": "[to be translated]:花式代码块", + "tip": "[to be translated]:使用更美观的代码块样式,例如 HTML 卡片" + }, "code_image_tools": { "label": "Habilitar herramienta de vista previa", "tip": "Habilitar herramientas de vista previa para imágenes renderizadas de bloques de código como mermaid" @@ -1741,8 +1745,15 @@ "compress_content": "reducir el ancho de la columna", "compress_content_description": "Al activarlo, se limitará el número de caracteres por línea, reduciendo el contenido mostrado en pantalla.", "default_font": "fuente predeterminada", + "font_size": "[to be translated]:字体大小", + "font_size_description": "[to be translated]:调整字体大小以获得更好的阅读体验 (10-30px)", + "font_size_large": "[to be translated]:大", + "font_size_medium": "[to be translated]:中", + "font_size_small": "[to be translated]:小", "font_title": "Configuración de fuente", "serif_font": "fuente serif", + "show_table_of_contents": "[to be translated]:显示目录大纲", + "show_table_of_contents_description": "[to be translated]:显示目录大纲侧边栏,方便文档内导航", "title": "configuración de visualización" }, "editor": { @@ -2639,6 +2650,10 @@ "url": "URL a la que escucha el servicio de recorte de Joplin", "url_placeholder": "http://127.0.0.1:41184/" }, + "limit": { + "appDataDiskQuota": "Advertencia de espacio en disco", + "appDataDiskQuotaDescription": "El espacio de almacenamiento de datos está casi lleno, por favor, limpie el espacio en disco, de lo contrario, se perderán los datos" + }, "local": { "autoSync": { "label": "Copia de seguridad automática", diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json index 6b17f9ebac..90d26eb045 100644 --- a/src/renderer/src/i18n/translate/fr-fr.json +++ b/src/renderer/src/i18n/translate/fr-fr.json @@ -538,6 +538,10 @@ "tip": "Une bouton d'exécution s'affichera dans la barre d'outils des blocs de code exécutables. Attention à ne pas exécuter de code dangereux !", "title": "Exécution de code" }, + "code_fancy_block": { + "label": "[to be translated]:花式代码块", + "tip": "[to be translated]:使用更美观的代码块样式,例如 HTML 卡片" + }, "code_image_tools": { "label": "Activer l'outil d'aperçu", "tip": "Activer les outils de prévisualisation pour les images rendues des blocs de code tels que mermaid" @@ -1741,8 +1745,15 @@ "compress_content": "réduire la largeur des colonnes", "compress_content_description": "L'activation limitera le nombre de caractères par ligne, réduisant ainsi le contenu affiché à l'écran.", "default_font": "police par défaut", + "font_size": "[to be translated]:字体大小", + "font_size_description": "[to be translated]:调整字体大小以获得更好的阅读体验 (10-30px)", + "font_size_large": "[to be translated]:大", + "font_size_medium": "[to be translated]:中", + "font_size_small": "[to be translated]:小", "font_title": "paramétrage des polices", "serif_font": "police à empattements", + "show_table_of_contents": "[to be translated]:显示目录大纲", + "show_table_of_contents_description": "[to be translated]:显示目录大纲侧边栏,方便文档内导航", "title": "Paramètres d'affichage" }, "editor": { @@ -2639,6 +2650,10 @@ "url": "URL surveillée par le service de découpage de Joplin", "url_placeholder": "http://127.0.0.1:41184/" }, + "limit": { + "appDataDiskQuota": "Avertissement d'espace sur le disque", + "appDataDiskQuotaDescription": "L'espace de stockage des données est presque plein, veuillez nettoyer l'espace sur le disque, sinon les données seront perdues" + }, "local": { "autoSync": { "label": "Sauvegarde automatique", diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json index a47670bafc..a65d6b4f8e 100644 --- a/src/renderer/src/i18n/translate/ja-jp.json +++ b/src/renderer/src/i18n/translate/ja-jp.json @@ -538,6 +538,10 @@ "tip": "実行可能なコードブロックのツールバーには実行ボタンが表示されます。危険なコードを実行しないでください!", "title": "コード実行" }, + "code_fancy_block": { + "label": "[to be translated]:花式代码块", + "tip": "[to be translated]:使用更美观的代码块样式,例如 HTML 卡片" + }, "code_image_tools": { "label": "プレビューツールを有効にする", "tip": "mermaid などのコードブロックから生成された画像に対してプレビューツールを有効にする" @@ -1741,8 +1745,15 @@ "compress_content": "バーの幅を減らします", "compress_content_description": "有効にすると、1行あたりの単語数が制限され、画面に表示されるコンテンツが減少します。", "default_font": "デフォルトフォント", + "font_size": "[to be translated]:字体大小", + "font_size_description": "[to be translated]:调整字体大小以获得更好的阅读体验 (10-30px)", + "font_size_large": "[to be translated]:大", + "font_size_medium": "[to be translated]:中", + "font_size_small": "[to be translated]:小", "font_title": "フォント設定", "serif_font": "セリフフォント", + "show_table_of_contents": "[to be translated]:显示目录大纲", + "show_table_of_contents_description": "[to be translated]:显示目录大纲侧边栏,方便文档内导航", "title": "見せる" }, "editor": { @@ -2639,6 +2650,10 @@ "url": "Joplin 剪輯服務 URL", "url_placeholder": "http://127.0.0.1:41184/" }, + "limit": { + "appDataDiskQuota": "ディスク容量警告", + "appDataDiskQuotaDescription": "データディレクトリの容量がほぼ満杯になっており、新しいデータの保存ができなくなる可能性があります。まずデータをバックアップしてから、ディスク容量を整理してください。" + }, "local": { "autoSync": { "label": "自動バックアップ", diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json index 24716c2726..fc0fea726e 100644 --- a/src/renderer/src/i18n/translate/pt-pt.json +++ b/src/renderer/src/i18n/translate/pt-pt.json @@ -538,6 +538,10 @@ "tip": "A barra de ferramentas de blocos de código executáveis exibirá um botão de execução; atenção para não executar códigos perigosos!", "title": "Execução de Código" }, + "code_fancy_block": { + "label": "[to be translated]:花式代码块", + "tip": "[to be translated]:使用更美观的代码块样式,例如 HTML 卡片" + }, "code_image_tools": { "label": "Habilitar ferramenta de visualização", "tip": "Ativar ferramentas de visualização para imagens renderizadas de blocos de código como mermaid" @@ -1741,8 +1745,15 @@ "compress_content": "reduzir a largura da coluna", "compress_content_description": "Ativando isso limitará o número de caracteres por linha, reduzindo o conteúdo exibido na tela.", "default_font": "fonte padrão", + "font_size": "[to be translated]:字体大小", + "font_size_description": "[to be translated]:调整字体大小以获得更好的阅读体验 (10-30px)", + "font_size_large": "[to be translated]:大", + "font_size_medium": "[to be translated]:中", + "font_size_small": "[to be translated]:小", "font_title": "configuração de fonte", "serif_font": "fonte com serifa", + "show_table_of_contents": "[to be translated]:显示目录大纲", + "show_table_of_contents_description": "[to be translated]:显示目录大纲侧边栏,方便文档内导航", "title": "configurações de exibição" }, "editor": { @@ -2639,6 +2650,10 @@ "url": "URL para o qual o serviço de recorte do Joplin está escutando", "url_placeholder": "http://127.0.0.1:41184/" }, + "limit": { + "appDataDiskQuota": "Aviso de espaço em disco", + "appDataDiskQuotaDescription": "O espaço de armazenamento de dados está quase cheio, por favor, limpe o espaço em disco, caso contrário, os dados serão perdidos" + }, "local": { "autoSync": { "label": "Backup automático", diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json index 9394503d6e..a9879dc962 100644 --- a/src/renderer/src/i18n/translate/ru-ru.json +++ b/src/renderer/src/i18n/translate/ru-ru.json @@ -538,6 +538,10 @@ "tip": "Выполнение кода в блоке кода возможно, но не рекомендуется выполнять опасный код!", "title": "Выполнение кода" }, + "code_fancy_block": { + "label": "[to be translated]:花式代码块", + "tip": "[to be translated]:使用更美观的代码块样式,例如 HTML 卡片" + }, "code_image_tools": { "label": "Включить инструменты предпросмотра", "tip": "Включить инструменты предпросмотра для изображений, сгенерированных из блоков кода (например mermaid)" @@ -1741,8 +1745,15 @@ "compress_content": "Уменьшить ширину стержня", "compress_content_description": "При включении он ограничит количество слов на строку, уменьшая содержимое, отображаемое на экране.", "default_font": "По умолчанию шрифт", + "font_size": "[to be translated]:字体大小", + "font_size_description": "[to be translated]:调整字体大小以获得更好的阅读体验 (10-30px)", + "font_size_large": "[to be translated]:大", + "font_size_medium": "[to be translated]:中", + "font_size_small": "[to be translated]:小", "font_title": "Настройки шрифта", "serif_font": "Serif Font", + "show_table_of_contents": "[to be translated]:显示目录大纲", + "show_table_of_contents_description": "[to be translated]:显示目录大纲侧边栏,方便文档内导航", "title": "показывать" }, "editor": { @@ -2639,6 +2650,10 @@ "url": "URL Joplin", "url_placeholder": "http://127.0.0.1:41184/" }, + "limit": { + "appDataDiskQuota": "Предупреждение о пространстве на диске", + "appDataDiskQuotaDescription": "Каталог данных почти заполнен, что может привести к невозможности сохранения новых данных. Сначала создайте резервную копию данных, затем освободите дисковое пространство." + }, "local": { "autoSync": { "label": "Автоматическое резервное копирование", diff --git a/src/renderer/src/utils/dataLimit.ts b/src/renderer/src/utils/dataLimit.ts new file mode 100644 index 0000000000..9cbc378a53 --- /dev/null +++ b/src/renderer/src/utils/dataLimit.ts @@ -0,0 +1,104 @@ +import { loggerService } from '@logger' +import { AppInfo } from '@renderer/types' +import { GB, MB } from '@shared/config/constant' +import { t } from 'i18next' + +const logger = loggerService.withContext('useDataLimit') + +const CHECK_INTERVAL_NORMAL = 1000 * 60 * 10 // 10 minutes +const CHECK_INTERVAL_WARNING = 1000 * 60 * 1 // 1 minute when warning is active + +let currentInterval: NodeJS.Timeout | null = null +let currentToastId: string | null = null + +async function checkAppStorageQuota() { + try { + const { usage, quota } = await navigator.storage.estimate() + if (usage && quota) { + const usageInMB = (usage / MB).toFixed(2) + const quotaInMB = (quota / MB).toFixed(2) + const usagePercentage = (usage / quota) * 100 + + logger.info(`App storage quota: Used ${usageInMB} MB / Total ${quotaInMB} MB (${usagePercentage.toFixed(2)}%)`) + + // if usage percentage is greater than 95%, + // warn user to clean up app internal data + if (usagePercentage >= 95) { + return true + } + } + } catch (error) { + logger.error('Failed to get storage quota:', error as Error) + } + return false +} + +async function checkAppDataDiskQuota(appDataPath: string) { + try { + const diskInfo = await window.api.getDiskInfo(appDataPath) + if (!diskInfo) { + return false + } + const { free } = diskInfo + logger.info(`App data disk quota: Free ${free} GB`) + // if free is less than 1GB, return true + return free < 1 * GB + } catch (error) { + logger.error('Failed to get app data disk quota:', error as Error) + } + return false +} + +export async function checkDataLimit() { + const check = async () => { + let isStorageQuotaLow = false + let isAppDataDiskQuotaLow = false + + isStorageQuotaLow = await checkAppStorageQuota() + + const appInfo: AppInfo = await window.api.getAppInfo() + if (appInfo?.appDataPath) { + isAppDataDiskQuotaLow = await checkAppDataDiskQuota(appInfo.appDataPath) + } + + const shouldShowWarning = isStorageQuotaLow || isAppDataDiskQuotaLow + + // Show or hide toast based on warning state + if (shouldShowWarning && !currentToastId) { + // Show persistent toast without close button + const toastId = window.toast.warning({ + title: t('settings.data.limit.appDataDiskQuota'), + description: t('settings.data.limit.appDataDiskQuotaDescription'), + timeout: 0, // Never auto-dismiss + hideCloseButton: true // Hide close button so user cannot dismiss + }) + currentToastId = toastId + + // Switch to warning mode with shorter interval + logger.info('Disk space low, switching to 1-minute check interval') + if (currentInterval) { + clearInterval(currentInterval) + } + currentInterval = setInterval(check, CHECK_INTERVAL_WARNING) + } else if (!shouldShowWarning && currentToastId) { + // Dismiss toast when space is recovered + window.toast.closeToast(currentToastId) + currentToastId = null + + // Switch back to normal mode + logger.info('Disk space recovered, switching back to 10-minute check interval') + if (currentInterval) { + clearInterval(currentInterval) + } + currentInterval = setInterval(check, CHECK_INTERVAL_NORMAL) + } + } + + // Initial check + check() + + // Set up initial interval (normal mode) + if (!currentInterval) { + currentInterval = setInterval(check, CHECK_INTERVAL_NORMAL) + } +} diff --git a/src/renderer/src/utils/index.ts b/src/renderer/src/utils/index.ts index a09d9aa7a8..828d288ba8 100644 --- a/src/renderer/src/utils/index.ts +++ b/src/renderer/src/utils/index.ts @@ -222,6 +222,7 @@ export function uniqueObjectArray(array: T[]): T[] { export * from './api' export * from './collection' +export * from './dataLimit' export * from './file' export * from './image' export * from './json' diff --git a/yarn.lock b/yarn.lock index 79a8143753..eb0cde8a17 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13150,6 +13150,7 @@ __metadata: axios: "npm:^1.7.3" browser-image-compression: "npm:^2.0.2" chardet: "npm:^2.1.0" + check-disk-space: "npm:3.4.0" cheerio: "npm:^1.1.2" chokidar: "npm:^4.0.3" cli-progress: "npm:^3.12.0" @@ -14612,6 +14613,13 @@ __metadata: languageName: node linkType: hard +"check-disk-space@npm:3.4.0": + version: 3.4.0 + resolution: "check-disk-space@npm:3.4.0" + checksum: 10c0/cc39c91e1337e974fb5069c2fbd9eb92aceca6e35f3da6863a4eada58f15c1bf6970055bffed1e41c15cde1fd0ad2580bb99bef8275791ed56d69947f8657aa5 + languageName: node + linkType: hard + "check-error@npm:^2.1.1": version: 2.1.1 resolution: "check-error@npm:2.1.1"