mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
Merge branch 'master' into feat/operation-logs
* master: fix(test): use dynamic date in year boundary test to avoid today collision fix(electron): reduce idle detection log verbosity fix(plugins): ensure setCounter creates valid SimpleCounter records feat(android): add quick add widget perf(android): prewarm WebView during idle time to speed up startup fix(ical): prevent race condition in lazy loader Merge branch 'master' into master # Conflicts: # src/app/features/android/store/android.effects.ts # src/app/features/schedule/ical/get-relevant-events-from-ical.spec.ts
This commit is contained in:
commit
bb254dd07b
23 changed files with 620 additions and 43 deletions
|
|
@ -107,5 +107,25 @@
|
|||
android:name=".receiver.ReminderActionReceiver"
|
||||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- Quick Add Widget -->
|
||||
<receiver
|
||||
android:name=".widget.QuickAddWidgetProvider"
|
||||
android:exported="true"
|
||||
android:label="@string/widget_quick_add_label">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/widget_quick_add_info" />
|
||||
</receiver>
|
||||
|
||||
<!-- Quick Add Dialog Activity -->
|
||||
<activity
|
||||
android:name=".widget.QuickAddActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.AppCompat.Light.Dialog"
|
||||
android:excludeFromRecents="true" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) { } }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<EditText>(R.id.quick_add_input)
|
||||
val addButton = findViewById<Button>(R.id.quick_add_submit)
|
||||
|
||||
addButton.setOnClickListener {
|
||||
submitTask(editText)
|
||||
}
|
||||
|
||||
// Handle keyboard "done" action
|
||||
editText.setOnEditorActionListener { _, actionId, event ->
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE ||
|
||||
(event?.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN)
|
||||
) {
|
||||
submitTask(editText)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Show keyboard automatically
|
||||
editText.requestFocus()
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
|
||||
}
|
||||
|
||||
private fun submitTask(editText: EditText) {
|
||||
val title = editText.text.toString().trim()
|
||||
if (title.isNotEmpty()) {
|
||||
WidgetTaskQueue.addTask(this, title)
|
||||
Toast.makeText(this, R.string.widget_task_added, Toast.LENGTH_SHORT).show()
|
||||
finish()
|
||||
} else {
|
||||
Toast.makeText(this, R.string.widget_enter_title, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package com.superproductivity.superproductivity.widget
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.widget.RemoteViews
|
||||
import com.superproductivity.superproductivity.R
|
||||
|
||||
/**
|
||||
* Widget provider for the quick-add task widget.
|
||||
* Displays a button that opens the QuickAddActivity dialog.
|
||||
*/
|
||||
class QuickAddWidgetProvider : AppWidgetProvider() {
|
||||
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray
|
||||
) {
|
||||
for (widgetId in appWidgetIds) {
|
||||
updateWidget(context, appWidgetManager, widgetId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEnabled(context: Context) {
|
||||
// Called when the first widget is created
|
||||
}
|
||||
|
||||
override fun onDisabled(context: Context) {
|
||||
// Called when the last widget is removed
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Update all instances of this widget.
|
||||
*/
|
||||
fun updateAllWidgets(context: Context) {
|
||||
val appWidgetManager = AppWidgetManager.getInstance(context)
|
||||
val widgetIds = appWidgetManager.getAppWidgetIds(
|
||||
ComponentName(context, QuickAddWidgetProvider::class.java)
|
||||
)
|
||||
for (widgetId in widgetIds) {
|
||||
updateWidget(context, appWidgetManager, widgetId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateWidget(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
widgetId: Int
|
||||
) {
|
||||
val views = RemoteViews(context.packageName, R.layout.widget_quick_add)
|
||||
|
||||
// Create intent to open QuickAddActivity
|
||||
val intent = Intent(context, QuickAddActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
widgetId,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
// Set click handler on the entire widget
|
||||
views.setOnClickPendingIntent(R.id.widget_container, pendingIntent)
|
||||
|
||||
appWidgetManager.updateAppWidget(widgetId, views)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package com.superproductivity.superproductivity.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Manages a queue of tasks created from the home screen widget.
|
||||
* Tasks are stored in SharedPreferences and processed when the Angular app resumes.
|
||||
*/
|
||||
object WidgetTaskQueue {
|
||||
private const val PREFS_NAME = "SuperProductivityWidget"
|
||||
private const val KEY_TASK_QUEUE = "WIDGET_TASK_QUEUE"
|
||||
|
||||
private fun getPrefs(context: Context): SharedPreferences {
|
||||
return context.applicationContext.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a task to the queue.
|
||||
* @return The generated task ID
|
||||
*/
|
||||
fun addTask(context: Context, title: String): String {
|
||||
val taskId = UUID.randomUUID().toString()
|
||||
val task = JSONObject().apply {
|
||||
put("id", taskId)
|
||||
put("title", title.trim())
|
||||
put("createdAt", System.currentTimeMillis())
|
||||
}
|
||||
|
||||
val prefs = getPrefs(context)
|
||||
val queueJson = prefs.getString(KEY_TASK_QUEUE, null)
|
||||
val queue = if (queueJson != null) {
|
||||
try {
|
||||
JSONObject(queueJson)
|
||||
} catch (e: Exception) {
|
||||
JSONObject().put("tasks", JSONArray())
|
||||
}
|
||||
} else {
|
||||
JSONObject().put("tasks", JSONArray())
|
||||
}
|
||||
|
||||
val tasks = queue.optJSONArray("tasks") ?: JSONArray()
|
||||
tasks.put(task)
|
||||
queue.put("tasks", tasks)
|
||||
|
||||
prefs.edit().putString(KEY_TASK_QUEUE, queue.toString()).commit()
|
||||
return taskId
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all queued tasks and clear the queue atomically.
|
||||
* @return JSON string of queued tasks, or null if empty
|
||||
*/
|
||||
fun getAndClearQueue(context: Context): String? {
|
||||
val prefs = getPrefs(context)
|
||||
val queueJson = prefs.getString(KEY_TASK_QUEUE, null)
|
||||
|
||||
if (queueJson != null) {
|
||||
prefs.edit().remove(KEY_TASK_QUEUE).commit()
|
||||
|
||||
try {
|
||||
val queue = JSONObject(queueJson)
|
||||
val tasks = queue.optJSONArray("tasks")
|
||||
if (tasks != null && tasks.length() > 0) {
|
||||
return queueJson
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Invalid JSON, return null
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
36
android/app/src/main/res/drawable/ic_widget_plus_badge.xml
Normal file
36
android/app/src/main/res/drawable/ic_widget_plus_badge.xml
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Circle background -->
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="#0B77D2" />
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="#FFFFFF" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<!-- Horizontal bar of plus -->
|
||||
<item
|
||||
android:top="11dp"
|
||||
android:bottom="11dp"
|
||||
android:left="5dp"
|
||||
android:right="5dp">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#FFFFFF" />
|
||||
<corners android:radius="1dp" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<!-- Vertical bar of plus -->
|
||||
<item
|
||||
android:top="5dp"
|
||||
android:bottom="5dp"
|
||||
android:left="11dp"
|
||||
android:right="11dp">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#FFFFFF" />
|
||||
<corners android:radius="1dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
6
android/app/src/main/res/drawable/widget_background.xml
Normal file
6
android/app/src/main/res/drawable/widget_background.xml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#E0FFFFFF" />
|
||||
<corners android:radius="16dp" />
|
||||
</shape>
|
||||
53
android/app/src/main/res/drawable/widget_icon.xml
Normal file
53
android/app/src/main/res/drawable/widget_icon.xml
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- App icon as base -->
|
||||
<item
|
||||
android:drawable="@mipmap/ic_launcher"
|
||||
android:gravity="center" />
|
||||
|
||||
<!-- Plus badge in bottom-right corner -->
|
||||
<item
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:gravity="bottom|end"
|
||||
android:bottom="2dp"
|
||||
android:end="2dp">
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="#0B77D2" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#FFFFFF" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<!-- Plus sign -->
|
||||
<item
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:gravity="bottom|end"
|
||||
android:bottom="2dp"
|
||||
android:end="2dp">
|
||||
<layer-list>
|
||||
<!-- Horizontal bar -->
|
||||
<item
|
||||
android:width="10dp"
|
||||
android:height="2dp"
|
||||
android:gravity="center">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#FFFFFF" />
|
||||
<corners android:radius="1dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<!-- Vertical bar -->
|
||||
<item
|
||||
android:width="2dp"
|
||||
android:height="10dp"
|
||||
android:gravity="center">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#FFFFFF" />
|
||||
<corners android:radius="1dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
</item>
|
||||
</layer-list>
|
||||
32
android/app/src/main/res/layout/activity_quick_add.xml
Normal file
32
android/app/src/main/res/layout/activity_quick_add.xml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/widget_dialog_title"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/quick_add_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/widget_input_hint"
|
||||
android:inputType="text"
|
||||
android:imeOptions="actionDone"
|
||||
android:maxLines="1"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/quick_add_submit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/widget_add_button" />
|
||||
|
||||
</LinearLayout>
|
||||
25
android/app/src/main/res/layout/widget_quick_add.xml
Normal file
25
android/app/src/main/res/layout/widget_quick_add.xml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/widget_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/widget_background"
|
||||
android:padding="4dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:src="@mipmap/ic_launcher"
|
||||
android:scaleType="fitCenter"
|
||||
android:contentDescription="@string/widget_add_task" />
|
||||
|
||||
<!-- Plus badge overlay -->
|
||||
<ImageView
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="2dp"
|
||||
android:src="@drawable/ic_widget_plus_badge"
|
||||
android:contentDescription="@null" />
|
||||
|
||||
</FrameLayout>
|
||||
|
|
@ -7,4 +7,15 @@
|
|||
<string name="webview_block_provider">Provider package: %1$s</string>
|
||||
<string name="webview_block_update">Update WebView</string>
|
||||
<string name="webview_block_close">Close app</string>
|
||||
|
||||
<!-- Widget strings -->
|
||||
<string name="widget_quick_add_label">Quick Add Task</string>
|
||||
<string name="widget_description">Quickly add a task without opening the app</string>
|
||||
<string name="widget_tap_to_add">Tap to add a task</string>
|
||||
<string name="widget_add_task">Add task</string>
|
||||
<string name="widget_dialog_title">Add Task</string>
|
||||
<string name="widget_input_hint">What do you need to do?</string>
|
||||
<string name="widget_add_button">Add</string>
|
||||
<string name="widget_task_added">Task added</string>
|
||||
<string name="widget_enter_title">Please enter a task title</string>
|
||||
</resources>
|
||||
|
|
|
|||
12
android/app/src/main/res/xml/widget_quick_add_info.xml
Normal file
12
android/app/src/main/res/xml/widget_quick_add_info.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:minWidth="40dp"
|
||||
android:minHeight="40dp"
|
||||
android:targetCellWidth="1"
|
||||
android:targetCellHeight="1"
|
||||
android:resizeMode="none"
|
||||
android:widgetCategory="home_screen"
|
||||
android:initialLayout="@layout/widget_quick_add"
|
||||
android:previewImage="@mipmap/ic_launcher"
|
||||
android:description="@string/widget_description"
|
||||
android:updatePeriodMillis="0" />
|
||||
|
|
@ -63,7 +63,7 @@ export class IdleTimeHandler {
|
|||
gnomeShellSession,
|
||||
};
|
||||
|
||||
log.info('Environment detection:', environment);
|
||||
log.debug('Environment detection:', environment);
|
||||
return environment;
|
||||
}
|
||||
|
||||
|
|
@ -138,22 +138,22 @@ export class IdleTimeHandler {
|
|||
}
|
||||
|
||||
private async _determineWorkingMethod(): Promise<IdleDetectionMethod> {
|
||||
log.info('Determining idle detection method...');
|
||||
log.debug('Determining idle detection method...');
|
||||
|
||||
if (!this._environment.isWayland) {
|
||||
log.info('Using powerMonitor for non-Wayland session');
|
||||
log.debug('Using powerMonitor for non-Wayland session');
|
||||
return 'powerMonitor';
|
||||
}
|
||||
|
||||
for (const candidate of this._buildWaylandCandidates()) {
|
||||
log.info(`Testing ${candidate.name}...`);
|
||||
log.debug(`Testing ${candidate.name}...`);
|
||||
try {
|
||||
const works = await candidate.test();
|
||||
if (works) {
|
||||
log.info(`Selected ${candidate.name} for idle detection`);
|
||||
return candidate.name;
|
||||
}
|
||||
log.info(`${candidate.name} test failed`);
|
||||
log.debug(`${candidate.name} test failed`);
|
||||
} catch (error) {
|
||||
log.warn(`${candidate.name} test error`, error);
|
||||
}
|
||||
|
|
@ -177,7 +177,7 @@ export class IdleTimeHandler {
|
|||
return idleTime !== null;
|
||||
} catch (error) {
|
||||
if (this._environment.isSnap) {
|
||||
log.info('GNOME DBus test failed in snap environment', error);
|
||||
log.debug('GNOME DBus test failed in snap environment', error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -198,7 +198,7 @@ export class IdleTimeHandler {
|
|||
});
|
||||
|
||||
if (this._environment.isSnap) {
|
||||
log.info('Skipping loginctl in snap environment');
|
||||
log.debug('Skipping loginctl in snap environment');
|
||||
} else {
|
||||
candidates.push({
|
||||
name: 'loginctl',
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ export const startApp = (): void => {
|
|||
`threshold=${CONFIG.MIN_IDLE_TIME}ms`,
|
||||
`action=${actionSummary}`,
|
||||
];
|
||||
log(`🕘 Idle check (${logParts.join(', ')})`);
|
||||
electronLog.debug(`🕘 Idle check (${logParts.join(', ')})`);
|
||||
} catch (error) {
|
||||
consecutiveFailures += 1;
|
||||
log('💥 Error getting idle time, falling back to powerMonitor:', error);
|
||||
|
|
|
|||
|
|
@ -73,6 +73,9 @@ export interface AndroidInterface {
|
|||
): void;
|
||||
cancelNativeReminder?(notificationId: number): void;
|
||||
|
||||
// Widget task queue - get queued tasks from home screen widget
|
||||
getWidgetTaskQueue?(): string | null;
|
||||
|
||||
// added here only
|
||||
onResume$: Subject<void>;
|
||||
onPause$: Subject<void>;
|
||||
|
|
|
|||
|
|
@ -173,6 +173,43 @@ export class AndroidEffects {
|
|||
{ dispatch: false },
|
||||
);
|
||||
|
||||
// Process tasks queued from the home screen widget
|
||||
processWidgetTasks$ =
|
||||
IS_ANDROID_WEB_VIEW &&
|
||||
createEffect(
|
||||
() =>
|
||||
androidInterface.onResume$.pipe(
|
||||
tap(() => {
|
||||
const queueJson = androidInterface.getWidgetTaskQueue?.();
|
||||
if (!queueJson) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const queue = JSON.parse(queueJson);
|
||||
const tasks = queue.tasks || [];
|
||||
|
||||
for (const widgetTask of tasks) {
|
||||
this._taskService.add(widgetTask.title);
|
||||
}
|
||||
|
||||
if (tasks.length > 0) {
|
||||
this._snackService.open({
|
||||
type: 'SUCCESS',
|
||||
msg:
|
||||
tasks.length === 1
|
||||
? 'Task added from widget'
|
||||
: `${tasks.length} tasks added from widget`,
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
DroidLog.err('Failed to process widget tasks', e);
|
||||
}
|
||||
}),
|
||||
),
|
||||
{ dispatch: false },
|
||||
);
|
||||
|
||||
private async _ensureExactAlarmAccess(): Promise<void> {
|
||||
try {
|
||||
if (this._hasCheckedExactAlarm) {
|
||||
|
|
|
|||
|
|
@ -827,7 +827,7 @@ SUMMARY:Office 365 Meeting
|
|||
END:VEVENT
|
||||
END:VCALENDAR`;
|
||||
|
||||
// Should not throw an error - the test will fail if it throws
|
||||
// Should not throw an error
|
||||
const events = await getRelevantEventsForCalendarIntegrationFromIcal(
|
||||
icalData,
|
||||
calProviderId,
|
||||
|
|
|
|||
35
src/app/features/schedule/ical/ical-lazy-loader.spec.ts
Normal file
35
src/app/features/schedule/ical/ical-lazy-loader.spec.ts
Normal file
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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<any> | 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<any> => {
|
||||
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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -893,8 +893,11 @@ describe('AddTaskBarActionsComponent', () => {
|
|||
});
|
||||
|
||||
it('should handle dates near year boundaries correctly', () => {
|
||||
// Test New Year's Eve
|
||||
const newYearEve = '2025-12-31';
|
||||
// Test a date near year boundary that's not today or tomorrow
|
||||
// Use next year's New Year's Eve to avoid collision with current date
|
||||
const today = new Date();
|
||||
const nextYear = today.getFullYear() + 1;
|
||||
const newYearEve = `${nextYear}-12-31`;
|
||||
const stateWithNewYearEve = {
|
||||
...mockState,
|
||||
date: newYearEve,
|
||||
|
|
|
|||
|
|
@ -55,7 +55,11 @@ import { MAX_BATCH_OPERATIONS_SIZE } from '../op-log/core/operation-log.const';
|
|||
|
||||
// New imports for simple counters
|
||||
import { selectAllSimpleCounters } from '../features/simple-counter/store/simple-counter.reducer';
|
||||
import { SimpleCounter } from '../features/simple-counter/simple-counter.model';
|
||||
import {
|
||||
SimpleCounter,
|
||||
SimpleCounterType,
|
||||
} from '../features/simple-counter/simple-counter.model';
|
||||
import { EMPTY_SIMPLE_COUNTER } from '../features/simple-counter/simple-counter.const';
|
||||
import {
|
||||
upsertSimpleCounter,
|
||||
updateSimpleCounter,
|
||||
|
|
@ -1250,20 +1254,40 @@ export class PluginBridgeService implements OnDestroy {
|
|||
throw new Error('Invalid counter value: must be a non-negative number');
|
||||
}
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
// Upsert the counter (creates if not exists)
|
||||
this._store.dispatch(
|
||||
upsertSimpleCounter({
|
||||
simpleCounter: {
|
||||
id: id,
|
||||
//title: id,
|
||||
//isEnabled: true,
|
||||
//icon: null,
|
||||
//type: 'ClickCounter',
|
||||
countOnDay: { [today]: value },
|
||||
//isOn: false,
|
||||
} as SimpleCounter,
|
||||
}),
|
||||
);
|
||||
|
||||
// Check if counter already exists
|
||||
const existingCounter = await this.getSimpleCounter(id);
|
||||
|
||||
if (existingCounter) {
|
||||
// Update existing counter's countOnDay only
|
||||
this._store.dispatch(
|
||||
updateSimpleCounter({
|
||||
simpleCounter: {
|
||||
id,
|
||||
changes: {
|
||||
countOnDay: {
|
||||
...existingCounter.countOnDay,
|
||||
[today]: value,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
// Create new counter with all mandatory fields
|
||||
this._store.dispatch(
|
||||
upsertSimpleCounter({
|
||||
simpleCounter: {
|
||||
...EMPTY_SIMPLE_COUNTER,
|
||||
id,
|
||||
title: id,
|
||||
isEnabled: true,
|
||||
type: SimpleCounterType.ClickCounter,
|
||||
countOnDay: { [today]: value },
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async incrementCounter(id: string, incrementBy = 1): Promise<number> {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@
|
|||
"SHOW_ISSUE_PANEL": "显示问题看板",
|
||||
"SHOW_NOTES": "显示项目备注",
|
||||
"SHOW_TASK_VIEW_CUSTOMIZER_PANEL": "显示筛选/分组/排序看板"
|
||||
},
|
||||
},
|
||||
"CONFIRM": {
|
||||
"AUTO_FIX": "您的数据似乎已损坏(“{{validityError}}”)。您想尝试自动修复吗?这可能会导致部分数据丢失。",
|
||||
"RELOAD_AFTER_IDB_ERROR": "无法访问数据库 :( 可能的原因是在后台更新了应用或磁盘空间不足。如果您在 Linux 上将应用安装为 snap,您还需要启用刷新感知 'snap set core experimental.refresh-app-awareness=true',直到他们在他们那边修复此问题。按 OK 重新加载应用(在某些平台上可能需要手动重新启动应用)。",
|
||||
|
|
@ -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": "对于任务",
|
||||
|
|
@ -1136,11 +1141,15 @@
|
|||
"TITLE": "同步",
|
||||
"WEB_DAV": {
|
||||
"CORS_INFO": "<strong>使其在浏览器中工作:</strong> 要使其在浏览器中工作,您需要将超级生产力加入 WebDAV 服务器的跨域资源共享请求白名单。这可能会带来负面的安全影响!对于 nextcloud,请 <a href='https://github.com/nextcloud/server/issues/3131 '>参考此线程以获取更多信息</a>。一种在移动设备上实现此目的的方法是通过 nextcloud 应用 <a href='https://apps.nextcloud.com/apps/webapppassword '>webapppassword<a> 将 \"https://app.super-productivity.com \" 加入白名单。使用风险自负!</p>",
|
||||
"D_SYNC_FOLDER_PATH": "同步文件在WebDAV服务器根目录下的相对存储路径(例如,'/super-productivity' 或 '/')。请注意,这不是您服务器内部的文件目录路径。",
|
||||
"INFO": "WebDAV 实现差异很大。众所周知,Super Productivity 与 Nextcloud 配合良好,<strong>但它可能不适用于您的提供者</strong>。",
|
||||
"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": "任务描述模板",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue