mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
feat(android): add quick add widget
This commit is contained in:
parent
25edb4ff1e
commit
4a89c05d32
14 changed files with 461 additions and 0 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>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
Loading…
Add table
Add a link
Reference in a new issue