From f7d087e30a63b7a14b3bceb031bd9acd13fd1aec Mon Sep 17 00:00:00 2001 From: John Date: Tue, 23 Dec 2025 06:54:58 +0800 Subject: [PATCH 1/7] Merge branch 'master' into master --- src/assets/i18n/zh.json | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/assets/i18n/zh.json b/src/assets/i18n/zh.json index ac46630ba..9c570477c 100644 --- a/src/assets/i18n/zh.json +++ b/src/assets/i18n/zh.json @@ -217,9 +217,14 @@ "ADD_TIME_MINUTE": "增加一分钟", "B": { "BREAK_RUNNING": "休息正在进行", - "POMODORO_SESSION_RUNNING": "番茄会话 #{{cycleNr}} 正在进行", + "END_BREAK": "休息结束", + "END_SESSION": "结束会话", + "PAUSE": "暂停", "POMODORO_BREAK_RUNNING": "休息 #{{cycleNr}} 正在进行", + "POMODORO_SESSION_RUNNING": "番茄会话 #{{cycleNr}} 正在进行", + "RESUME": "恢复", "SESSION_RUNNING": "专注会话正在进行", + "START": "开始", "TO_FOCUS_OVERLAY": "到专注覆盖" }, "BACK_TO_PLANNING": "返回规划", @@ -234,7 +239,7 @@ "COUNTDOWN_HINT": "专注直到时钟归零", "CURRENT_SESSION_TIME_TOOLTIP": "当前会话时间", "FINISH_TASK_AND_SELECT_NEXT": "完成任务并选择下一个", - "FLOWTIME": "Flowtime", + "FLOWTIME": "流程时间", "FLOWTIME_HINT": "无严格计时器的灵活节奏", "FOCUS_TIME_TOOLTIP": "专注时间", "FOR_TASK": "对于任务", @@ -1130,17 +1135,21 @@ "L_SYNC_INTERVAL": "同步间隔", "L_SYNC_PROVIDER": "同步提供者", "LOCAL_FILE": { - "L_SYNC_FILE_PATH_PERMISSION_VALIDATION": "需要文件访问权限", - "L_SYNC_FOLDER_PATH": "同步文件夹路径" + "L_SYNC_FILE_PATH_PERMISSION_VALIDATION": "需要文件访问权限", + "L_SYNC_FOLDER_PATH": "同步文件夹路径" }, "TITLE": "同步", "WEB_DAV": { "CORS_INFO": "使其在浏览器中工作: 要使其在浏览器中工作,您需要将超级生产力加入 WebDAV 服务器的跨域资源共享请求白名单。这可能会带来负面的安全影响!对于 nextcloud,请 参考此线程以获取更多信息。一种在移动设备上实现此目的的方法是通过 nextcloud 应用 webapppassword 将 \"https://app.super-productivity.com \" 加入白名单。使用风险自负!

", + "D_SYNC_FOLDER_PATH": "同步文件在WebDAV服务器根目录下的相对存储路径(例如,'/super-productivity' 或 '/')。请注意,这不是您服务器内部的文件目录路径。", "INFO": "WebDAV 实现差异很大。众所周知,Super Productivity 与 Nextcloud 配合良好,但它可能不适用于您的提供者。", "L_BASE_URL": "基本 URL", "L_PASSWORD": "密码", "L_SYNC_FOLDER_PATH": "同步文件夹路径", - "L_USER_NAME": "用户名" + "L_TEST_CONNECTION": "测试链接", + "L_USER_NAME": "用户名" , + "S_TEST_FAIL": "连接测试失败:{{error}} - 目标URL:{{url}}", + "S_TEST_SUCCESS": "连接测试成功!目标URL:{{url}}" } }, "S": { @@ -1150,6 +1159,9 @@ "BTN_FORCE_OVERWRITE": "强制覆盖", "ERROR_CORS": "WebDAV 同步错误: 网络请求失败。\n\n这可能是一个跨域资源共享问题。请确保:\n• 您的 WebDAV 服务器允许跨源请求\n• 服务器 URL 正确且可访问\n• 您有有效的互联网连接", "ERROR_DATA_IS_CURRENTLY_WRITTEN": "远程数据当前正在写入", + "ERROR_PERMISSION": "文件访问被拒绝。如果正在使用 Flatpak/Snap ,请通过 Flatseal 授予文件系统权限,或者使用 ~/.var/app/ 目录内的路径。", + "ERROR_PERMISSION_FLATPAK": "文件访问被拒绝。请通过Flatseal授予文件系统权限,或使用路径 ~/.var/app/ 内的目录。", + "ERROR_PERMISSION_SNAP": "文件访问被拒绝。请运行命令 'snap connect super-productivity:home',或使用路径 ~/snap/super-productivity/common/ 内的目录。", "ERROR_FALLBACK_TO_BACKUP": "导入数据时出错。回退到本地备份。", "ERROR_INVALID_DATA": "同步时出错。数据无效", "ERROR_NO_REV": "远程文件没有有效的修订", @@ -1751,6 +1763,10 @@ "HELP": "专注模式打开一个无干扰的屏幕,以帮助您专注于当前任务。", "L_ALWAYS_OPEN_FOCUS_MODE": "始终在跟踪时打开专注模式", "L_SKIP_PREPARATION_SCREEN": "跳过准备屏幕(拉伸等)", + "L_IS_PLAY_TICK": "专注时播放滴答声", + "L_PAUSE_TRACKING_DURING_BREAK": "休息期间暂停任务跟踪", + "L_SKIP_PREPARATION_SCREEN": "跳过准备界面(火箭动画)", + "L_START_IN_BACKGROUND": "仅以横幅开始专注时段(无覆盖层)", "TITLE": "专注模式" }, "IDLE": { @@ -1887,8 +1903,8 @@ "IS_TRAY_SHOW_CURRENT_COUNTDOWN": "在托盘/状态菜单中显示当前倒计时(仅限桌面 Mac)", "IS_TRAY_SHOW_CURRENT_TASK": "在托盘/状态菜单中显示当前任务(仅限桌面 Mac/Windows)", "IS_TURN_OFF_MARKDOWN": "关闭任务描述的 Markdown 解析", - "IS_USE_CUSTOM_WINDOW_TITLE_BAR": "Use custom title bar (Windows/Linux only)", - "IS_USE_CUSTOM_WINDOW_TITLE_BAR_HINT": "Requires restart to take effect", + "IS_USE_CUSTOM_WINDOW_TITLE_BAR": "使用自定义标题栏(仅限Windows/Linux)", + "IS_USE_CUSTOM_WINDOW_TITLE_BAR_HINT": "需要重启方可生效", "START_OF_NEXT_DAY": "下一天的开始时间", "START_OF_NEXT_DAY_HINT": "从何时(小时)开始计算下一天已经开始。默认是午夜,即 0。", "TASK_NOTES_TPL": "任务描述模板", From 6d82c1982d06d68b9262bda6f00f220ca1e0a519 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Wed, 31 Dec 2025 11:28:09 +0100 Subject: [PATCH 2/7] fix(ical): prevent race condition in lazy loader Use Promise-based singleton pattern to ensure concurrent calls share the same loading promise instead of triggering multiple imports. Also fix pre-existing test bug using await in sync function. --- .../get-relevant-events-from-ical.spec.ts | 18 +++++----- .../schedule/ical/ical-lazy-loader.spec.ts | 35 +++++++++++++++++++ .../schedule/ical/ical-lazy-loader.ts | 20 +++++++---- 3 files changed, 57 insertions(+), 16 deletions(-) create mode 100644 src/app/features/schedule/ical/ical-lazy-loader.spec.ts diff --git a/src/app/features/schedule/ical/get-relevant-events-from-ical.spec.ts b/src/app/features/schedule/ical/get-relevant-events-from-ical.spec.ts index 2b1dec45f..6735233db 100644 --- a/src/app/features/schedule/ical/get-relevant-events-from-ical.spec.ts +++ b/src/app/features/schedule/ical/get-relevant-events-from-ical.spec.ts @@ -828,16 +828,14 @@ END:VEVENT END:VCALENDAR`; // Should not throw an error - expect(() => { - const events = await getRelevantEventsForCalendarIntegrationFromIcal( - icalData, - calProviderId, - startTimestamp, - endTimestamp, - ); - expect(events.length).toBe(1); - expect(events[0].title).toBe('Office 365 Meeting'); - }).not.toThrow(); + const events = await getRelevantEventsForCalendarIntegrationFromIcal( + icalData, + calProviderId, + startTimestamp, + endTimestamp, + ); + expect(events.length).toBe(1); + expect(events[0].title).toBe('Office 365 Meeting'); }); it('should handle iCal with TZID reference to unknown timezone gracefully', async () => { diff --git a/src/app/features/schedule/ical/ical-lazy-loader.spec.ts b/src/app/features/schedule/ical/ical-lazy-loader.spec.ts new file mode 100644 index 000000000..aee5d0518 --- /dev/null +++ b/src/app/features/schedule/ical/ical-lazy-loader.spec.ts @@ -0,0 +1,35 @@ +import { loadIcalModule } from './ical-lazy-loader'; + +describe('ical-lazy-loader', () => { + describe('loadIcalModule', () => { + it('should load the ical.js module', async () => { + const ICAL = await loadIcalModule(); + + expect(ICAL).toBeDefined(); + expect(ICAL.parse).toBeDefined(); + expect(ICAL.Component).toBeDefined(); + }); + + it('should return the same module instance on subsequent calls', async () => { + const first = await loadIcalModule(); + const second = await loadIcalModule(); + + expect(first).toBe(second); + }); + + it('should handle concurrent calls without race conditions', async () => { + const results = await Promise.all([ + loadIcalModule(), + loadIcalModule(), + loadIcalModule(), + loadIcalModule(), + loadIcalModule(), + ]); + + const firstResult = results[0]; + results.forEach((result) => { + expect(result).toBe(firstResult); + }); + }); + }); +}); diff --git a/src/app/features/schedule/ical/ical-lazy-loader.ts b/src/app/features/schedule/ical/ical-lazy-loader.ts index 04245d831..1a01801b2 100644 --- a/src/app/features/schedule/ical/ical-lazy-loader.ts +++ b/src/app/features/schedule/ical/ical-lazy-loader.ts @@ -5,18 +5,26 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any let icalModule: any = null; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let loadingPromise: Promise | null = null; /** * Lazily loads the ical.js module on first use. * Subsequent calls return the cached module. + * Concurrent calls share the same loading promise to prevent race conditions. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export const loadIcalModule = async (): Promise => { - if (!icalModule) { - // @ts-ignore - ical.js exports default - const mod = await import('ical.js'); - // Handle both ESM default export and CommonJS module.exports - icalModule = mod.default || mod; + if (icalModule) { + return icalModule; } - return icalModule; + if (!loadingPromise) { + loadingPromise = import('ical.js').then((mod) => { + // @ts-ignore - ical.js exports default + // Handle both ESM default export and CommonJS module.exports + icalModule = mod.default || mod; + return icalModule; + }); + } + return loadingPromise; }; From 25edb4ff1e0fbded63a4ce2b9de5159259974bb6 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Wed, 31 Dec 2025 11:45:52 +0100 Subject: [PATCH 3/7] perf(android): prewarm WebView during idle time to speed up startup Load WebView native libraries during main thread idle time using IdleHandler and WebSettings.getDefaultUserAgent(). This reduces the 200-300ms freeze on first WebView creation. --- .../superproductivity/App.kt | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/android/app/src/main/java/com/superproductivity/superproductivity/App.kt b/android/app/src/main/java/com/superproductivity/superproductivity/App.kt index bb9d9443a..540af3e9f 100644 --- a/android/app/src/main/java/com/superproductivity/superproductivity/App.kt +++ b/android/app/src/main/java/com/superproductivity/superproductivity/App.kt @@ -1,16 +1,15 @@ package com.superproductivity.superproductivity import android.app.Application +import android.os.Looper +import android.os.MessageQueue +import android.util.Log +import android.webkit.WebSettings import com.superproductivity.superproductivity.app.AppLifecycleObserver import com.superproductivity.superproductivity.app.KeyValStore class App : Application() { - // NOTE using the web view like this causes all html5 inputs not to work -// val wv: WebView by lazy { -// WebHelper().instanceView(this) -// } - val keyValStore: KeyValStore by lazy { KeyValStore(this) } @@ -20,5 +19,36 @@ class App : Application() { // Initialize AppLifecycleObserver at app startup AppLifecycleObserver.getInstance() + + // Prewarm WebView by loading native libraries during idle time + prewarmWebView() + } + + /** + * Prewarms the WebView's Chromium engine by triggering native library loading + * during main thread idle time. This reduces the 200-300ms freeze that occurs + * on first WebView creation. + * + * Uses WebSettings.getDefaultUserAgent() as recommended by the Chromium team - + * it loads WebView libraries without side effects. + */ + private fun prewarmWebView() { + try { + Looper.getMainLooper().queue.addIdleHandler(object : MessageQueue.IdleHandler { + override fun queueIdle(): Boolean { + try { + val startTime = System.currentTimeMillis() + WebSettings.getDefaultUserAgent(this@App) + val duration = System.currentTimeMillis() - startTime + Log.d("SP-WebView", "WebView prewarmed in ${duration}ms") + } catch (e: Exception) { + Log.w("SP-WebView", "WebView prewarm failed: ${e.message}") + } + return false // Remove handler after execution + } + }) + } catch (e: Exception) { + Log.w("SP-WebView", "Failed to schedule WebView prewarm: ${e.message}") + } } } From 4a89c05d321ea7da429dd328bd1e51a921cf4c73 Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Wed, 31 Dec 2025 12:45:17 +0100 Subject: [PATCH 4/7] feat(android): add quick add widget --- android/app/src/main/AndroidManifest.xml | 20 +++++ .../webview/JavaScriptInterface.kt | 11 +++ .../widget/QuickAddActivity.kt | 64 ++++++++++++++++ .../widget/QuickAddWidgetProvider.kt | 75 ++++++++++++++++++ .../widget/WidgetTaskQueue.kt | 76 +++++++++++++++++++ .../res/drawable/ic_widget_plus_badge.xml | 36 +++++++++ .../main/res/drawable/widget_background.xml | 6 ++ .../app/src/main/res/drawable/widget_icon.xml | 53 +++++++++++++ .../main/res/layout/activity_quick_add.xml | 32 ++++++++ .../src/main/res/layout/widget_quick_add.xml | 25 ++++++ android/app/src/main/res/values/strings.xml | 11 +++ .../main/res/xml/widget_quick_add_info.xml | 12 +++ src/app/features/android/android-interface.ts | 3 + .../features/android/store/android.effects.ts | 37 +++++++++ 14 files changed, 461 insertions(+) create mode 100644 android/app/src/main/java/com/superproductivity/superproductivity/widget/QuickAddActivity.kt create mode 100644 android/app/src/main/java/com/superproductivity/superproductivity/widget/QuickAddWidgetProvider.kt create mode 100644 android/app/src/main/java/com/superproductivity/superproductivity/widget/WidgetTaskQueue.kt create mode 100644 android/app/src/main/res/drawable/ic_widget_plus_badge.xml create mode 100644 android/app/src/main/res/drawable/widget_background.xml create mode 100644 android/app/src/main/res/drawable/widget_icon.xml create mode 100644 android/app/src/main/res/layout/activity_quick_add.xml create mode 100644 android/app/src/main/res/layout/widget_quick_add.xml create mode 100644 android/app/src/main/res/xml/widget_quick_add_info.xml diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9f13a99d7..1289bdf16 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -107,5 +107,25 @@ android:name=".receiver.ReminderActionReceiver" android:enabled="true" android:exported="false" /> + + + + + + + + + + + diff --git a/android/app/src/main/java/com/superproductivity/superproductivity/webview/JavaScriptInterface.kt b/android/app/src/main/java/com/superproductivity/superproductivity/webview/JavaScriptInterface.kt index c8b1e4800..4f0ef9235 100644 --- a/android/app/src/main/java/com/superproductivity/superproductivity/webview/JavaScriptInterface.kt +++ b/android/app/src/main/java/com/superproductivity/superproductivity/webview/JavaScriptInterface.kt @@ -13,6 +13,7 @@ import com.superproductivity.superproductivity.app.LaunchDecider import com.superproductivity.superproductivity.service.FocusModeForegroundService import com.superproductivity.superproductivity.service.ReminderNotificationHelper import com.superproductivity.superproductivity.service.TrackingForegroundService +import com.superproductivity.superproductivity.widget.WidgetTaskQueue class JavaScriptInterface( @@ -192,6 +193,16 @@ class JavaScriptInterface( ReminderNotificationHelper.cancelReminder(activity, notificationId) } + /** + * Get queued tasks from the widget and clear the queue. + * Returns JSON string of tasks or null if empty. + */ + @Suppress("unused") + @JavascriptInterface + fun getWidgetTaskQueue(): String? { + return WidgetTaskQueue.getAndClearQueue(activity) + } + fun callJavaScriptFunction(script: String) { webView.post { webView.evaluateJavascript(script) { } } } diff --git a/android/app/src/main/java/com/superproductivity/superproductivity/widget/QuickAddActivity.kt b/android/app/src/main/java/com/superproductivity/superproductivity/widget/QuickAddActivity.kt new file mode 100644 index 000000000..07215a347 --- /dev/null +++ b/android/app/src/main/java/com/superproductivity/superproductivity/widget/QuickAddActivity.kt @@ -0,0 +1,64 @@ +package com.superproductivity.superproductivity.widget + +import android.app.Activity +import android.os.Bundle +import android.view.KeyEvent +import android.view.WindowManager +import android.view.inputmethod.EditorInfo +import android.widget.Button +import android.widget.EditText +import android.widget.Toast +import com.superproductivity.superproductivity.R + +/** + * A minimal floating dialog activity for quick task entry from the widget. + * Uses a dialog theme to appear as a floating window. + */ +class QuickAddActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_quick_add) + + // Make it dialog-like with proper sizing + window.setLayout( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.WRAP_CONTENT + ) + setFinishOnTouchOutside(true) + + val editText = findViewById(R.id.quick_add_input) + val addButton = findViewById