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