diff --git a/README.md b/README.md
index 62f206c..1bfef7a 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,7 @@
| 🔊 **Sonido configurable** | Activa o desactiva el sonido de las notificaciones |
| 🌐 **Multiidioma** | 8 idiomas: Español · English · 中文 · Français · Deutsch · Português · 日本語 · 한국어 |
| 🎨 **Material Design 3** | Interfaz moderna con gradientes, colores vibrantes y soporte edge-to-edge |
+| 🟣 **Widget** | Widget de escritorio que muestra la tarea activa y una meta aleatoria |
---
@@ -184,6 +185,7 @@ El idioma se selecciona desde **Configuración → Idioma** y se aplica instant
- [x] Ciclo de días configurable
- [x] Multiidioma (8 idiomas)
- [x] Sonido configurable independiente del canal Android
+- [x] Widget de pantalla de inicio
- [ ] Estadísticas de cumplimiento
- [ ] Widget de pantalla de inicio
- [ ] Backup en la nube
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index bcae6c5..ba0b5ae 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -27,6 +27,21 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/manalejandro/motivame/ui/viewmodel/TaskViewModel.kt b/app/src/main/java/com/manalejandro/motivame/ui/viewmodel/TaskViewModel.kt
index 0e65c1b..0ed5889 100644
--- a/app/src/main/java/com/manalejandro/motivame/ui/viewmodel/TaskViewModel.kt
+++ b/app/src/main/java/com/manalejandro/motivame/ui/viewmodel/TaskViewModel.kt
@@ -5,6 +5,7 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.manalejandro.motivame.data.Task
import com.manalejandro.motivame.data.TaskRepository
+import com.manalejandro.motivame.widget.MotivameWidget
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -65,6 +66,7 @@ class TaskViewModel(application: Application) : AndroidViewModel(application) {
viewModelScope.launch {
repository.addTask(task)
onRescheduleReminders?.invoke(_notificationEnabled.value)
+ MotivameWidget.requestUpdate(getApplication())
}
}
@@ -72,6 +74,7 @@ class TaskViewModel(application: Application) : AndroidViewModel(application) {
viewModelScope.launch {
repository.updateTask(task)
onRescheduleReminders?.invoke(_notificationEnabled.value)
+ MotivameWidget.requestUpdate(getApplication())
}
}
@@ -79,6 +82,7 @@ class TaskViewModel(application: Application) : AndroidViewModel(application) {
viewModelScope.launch {
repository.deleteTask(taskId)
onRescheduleReminders?.invoke(_notificationEnabled.value)
+ MotivameWidget.requestUpdate(getApplication())
}
}
diff --git a/app/src/main/java/com/manalejandro/motivame/widget/MotivameWidget.kt b/app/src/main/java/com/manalejandro/motivame/widget/MotivameWidget.kt
new file mode 100644
index 0000000..525e0e0
--- /dev/null
+++ b/app/src/main/java/com/manalejandro/motivame/widget/MotivameWidget.kt
@@ -0,0 +1,198 @@
+package com.manalejandro.motivame.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.os.Bundle
+import android.view.View
+import android.widget.RemoteViews
+import com.manalejandro.motivame.MainActivity
+import com.manalejandro.motivame.R
+import com.manalejandro.motivame.data.Task
+import com.manalejandro.motivame.data.TaskRepository
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+
+class MotivameWidget : AppWidgetProvider() {
+
+ override fun onUpdate(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetIds: IntArray
+ ) {
+ appWidgetIds.forEach { updateWidget(context, appWidgetManager, it) }
+ }
+
+ override fun onAppWidgetOptionsChanged(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetId: Int,
+ newOptions: Bundle
+ ) {
+ updateWidget(context, appWidgetManager, appWidgetId)
+ }
+
+ override fun onReceive(context: Context, intent: Intent) {
+ super.onReceive(context, intent)
+ if (intent.action == ACTION_REFRESH) {
+ val mgr = AppWidgetManager.getInstance(context)
+ mgr.getAppWidgetIds(ComponentName(context, MotivameWidget::class.java))
+ .forEach { updateWidget(context, mgr, it) }
+ }
+ }
+
+ companion object {
+ const val ACTION_REFRESH = "com.manalejandro.motivame.WIDGET_REFRESH"
+
+ // ── Número de tareas según altura real (MIN_HEIGHT) ─────────────
+ // ~1 fila ≈ 74dp, ~2 filas ≈ 148dp, ~3 filas ≈ 222dp, ~4 filas ≈ 296dp
+ private fun taskCount(heightDp: Int) = when {
+ heightDp >= 220 -> 3
+ heightDp >= 145 -> 2
+ else -> 1
+ }
+
+ // ── Número de metas por tarea según espacio disponible ───────────
+ // Se divide la altura disponible entre el número de tareas para estimar
+ // el espacio por tarea y decidir cuántas metas caben.
+ private fun goalsPerTask(heightDp: Int, tasks: Int): Int {
+ val spacePerTask = heightDp / tasks
+ return when {
+ spacePerTask >= 160 -> 3 // mucho espacio → 3 metas
+ spacePerTask >= 100 -> 2 // espacio medio → 2 metas
+ else -> 1 // poco espacio → 1 meta
+ }
+ }
+
+ private fun layoutFor(tasks: Int) = when (tasks) {
+ 3 -> R.layout.widget_motivame_large
+ 2 -> R.layout.widget_motivame_medium
+ else -> R.layout.widget_motivame_small
+ }
+
+ // IDs agrupados por tarea y slot de meta
+ private val TITLE_IDS = intArrayOf(
+ R.id.widget_task_title, R.id.widget_task2_title, R.id.widget_task3_title
+ )
+ private val GOAL_IDS = arrayOf(
+ intArrayOf(R.id.widget_t1_goal1, R.id.widget_t1_goal2, R.id.widget_t1_goal3),
+ intArrayOf(R.id.widget_t2_goal1, R.id.widget_t2_goal2, R.id.widget_t2_goal3),
+ intArrayOf(R.id.widget_t3_goal1, R.id.widget_t3_goal2, R.id.widget_t3_goal3)
+ )
+ private val CHIP_IDS = intArrayOf(
+ R.id.widget_chip1, R.id.widget_chip2, R.id.widget_chip3
+ )
+
+ fun updateWidget(context: Context, mgr: AppWidgetManager, widgetId: Int) {
+ CoroutineScope(Dispatchers.IO).launch {
+ val options = mgr.getAppWidgetOptions(widgetId)
+ // MIN_HEIGHT = altura real actual del widget en el launcher
+ val heightDp = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, 110)
+ val numTasks = taskCount(heightDp)
+ val numGoals = goalsPerTask(heightDp, numTasks)
+ val showChip = numTasks == 3 // chip solo en layout grande
+
+ val activeTasks = TaskRepository(context).tasks.first().filter { it.isActive }
+ val views = RemoteViews(context.packageName, layoutFor(numTasks))
+
+ // Click → abrir app
+ views.setOnClickPendingIntent(
+ R.id.widget_root,
+ PendingIntent.getActivity(
+ context, widgetId,
+ Intent(context, MainActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
+ },
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+ )
+ views.setTextViewText(R.id.widget_hint,
+ context.getString(R.string.widget_tap_to_open))
+
+ if (activeTasks.isEmpty()) {
+ views.setTextViewText(R.id.widget_status, "○")
+ views.setTextViewText(R.id.widget_task_title, "Motívame")
+ views.setTextViewText(R.id.widget_t1_goal1,
+ context.getString(R.string.widget_no_tasks))
+ // ocultar metas 2 y 3 del slot 1
+ views.setViewVisibility(R.id.widget_t1_goal2, View.GONE)
+ views.setViewVisibility(R.id.widget_t1_goal3, View.GONE)
+ // ocultar slots extra
+ if (numTasks >= 2) hideTaskSlot(views, 1, showChip)
+ if (numTasks >= 3) hideTaskSlot(views, 2, showChip)
+ } else {
+ views.setTextViewText(R.id.widget_status, "●")
+ for (slot in 0 until numTasks) {
+ val task = activeTasks.getOrNull(slot)
+ fillTaskSlot(context, views, slot, task, numGoals, showChip)
+ }
+ }
+
+ mgr.updateAppWidget(widgetId, views)
+ }
+ }
+
+ private fun fillTaskSlot(
+ context: Context,
+ views: RemoteViews,
+ slot: Int, // 0-based
+ task: Task?,
+ numGoals: Int,
+ showChip: Boolean
+ ) {
+ val titleId = TITLE_IDS[slot]
+ val goalIds = GOAL_IDS[slot]
+
+ if (task == null) {
+ hideTaskSlot(views, slot, showChip)
+ return
+ }
+
+ views.setViewVisibility(titleId, View.VISIBLE)
+ views.setTextViewText(titleId, task.title)
+
+ // Rellenar metas con opacidad decreciente; ocultar las que excedan numGoals
+ val goals = task.goals
+ for (i in 0..2) {
+ val goalId = goalIds[i]
+ if (i < numGoals && i < goals.size) {
+ views.setViewVisibility(goalId, View.VISIBLE)
+ views.setTextViewText(goalId, "🎯 ${goals[i]}")
+ } else {
+ views.setViewVisibility(goalId, View.GONE)
+ }
+ }
+
+ // Chip de avisos (solo layout grande)
+ if (showChip) {
+ val chipId = CHIP_IDS[slot]
+ val chipText = if (task.repeatEveryDays == 1)
+ context.getString(R.string.task_summary_reminders_daily, task.dailyReminders)
+ else
+ context.getString(R.string.task_summary_reminders, task.dailyReminders, task.repeatEveryDays)
+ views.setViewVisibility(chipId, View.VISIBLE)
+ views.setTextViewText(chipId, "🔔 $chipText")
+ }
+ }
+
+ private fun hideTaskSlot(views: RemoteViews, slot: Int, showChip: Boolean) {
+ views.setViewVisibility(TITLE_IDS[slot], View.GONE)
+ GOAL_IDS[slot].forEach { views.setViewVisibility(it, View.GONE) }
+ if (showChip) views.setViewVisibility(CHIP_IDS[slot], View.GONE)
+ }
+
+ fun requestUpdate(context: Context) {
+ context.sendBroadcast(
+ Intent(context, MotivameWidget::class.java).apply {
+ action = ACTION_REFRESH
+ }
+ )
+ }
+ }
+}
+
diff --git a/app/src/main/java/com/manalejandro/motivame/worker/DailyReminderWorker.kt b/app/src/main/java/com/manalejandro/motivame/worker/DailyReminderWorker.kt
index 00f897a..864492a 100644
--- a/app/src/main/java/com/manalejandro/motivame/worker/DailyReminderWorker.kt
+++ b/app/src/main/java/com/manalejandro/motivame/worker/DailyReminderWorker.kt
@@ -5,6 +5,7 @@ import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.manalejandro.motivame.data.TaskRepository
import com.manalejandro.motivame.notifications.NotificationHelper
+import com.manalejandro.motivame.widget.MotivameWidget
import kotlinx.coroutines.flow.first
class DailyReminderWorker(
@@ -38,6 +39,9 @@ class DailyReminderWorker(
notificationHelper.sendTaskReminder(it, soundEnabled)
}
+ // Refrescar el widget con la meta actualizada
+ MotivameWidget.requestUpdate(applicationContext)
+
return Result.success()
}
}
diff --git a/app/src/main/res/drawable/widget_background.xml b/app/src/main/res/drawable/widget_background.xml
new file mode 100644
index 0000000..9803492
--- /dev/null
+++ b/app/src/main/res/drawable/widget_background.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/widget_task_bg.xml b/app/src/main/res/drawable/widget_task_bg.xml
new file mode 100644
index 0000000..ccac484
--- /dev/null
+++ b/app/src/main/res/drawable/widget_task_bg.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/widget_motivame.xml b/app/src/main/res/layout/widget_motivame.xml
new file mode 100644
index 0000000..b0313cc
--- /dev/null
+++ b/app/src/main/res/layout/widget_motivame.xml
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/widget_motivame_large.xml b/app/src/main/res/layout/widget_motivame_large.xml
new file mode 100644
index 0000000..c2eef1a
--- /dev/null
+++ b/app/src/main/res/layout/widget_motivame_large.xml
@@ -0,0 +1,248 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/widget_motivame_medium.xml b/app/src/main/res/layout/widget_motivame_medium.xml
new file mode 100644
index 0000000..9fe5bdd
--- /dev/null
+++ b/app/src/main/res/layout/widget_motivame_medium.xml
@@ -0,0 +1,162 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/widget_motivame_small.xml b/app/src/main/res/layout/widget_motivame_small.xml
new file mode 100644
index 0000000..e32bf2e
--- /dev/null
+++ b/app/src/main/res/layout/widget_motivame_small.xml
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 22b696f..8be3cc7 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -76,5 +76,11 @@
Denke daran, diese Aufgabe abzuschließen!
📝 Aufgabe: %1$s\n\n🎯 Denk daran: %2$s
+
+
+ Zeigt deine aktive Aufgabe und ein motivierendes Ziel
+ Keine aktiven Aufgaben.\nÖffne Motivier mich, um eine hinzuzufügen.
+ Tippen zum Öffnen →
+ aktiv
diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml
index e7c30c9..6da3a5b 100644
--- a/app/src/main/res/values-en/strings.xml
+++ b/app/src/main/res/values-en/strings.xml
@@ -81,6 +81,12 @@
Remember to complete this task!
📝 Task: %1$s\n\n🎯 Remember: %2$s
+
+
+ Shows your active task and a motivational goal
+ No active tasks.\nOpen Motivate Me to add one.
+ Tap to open →
+ active
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 1cc287b..3d314b0 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -76,5 +76,11 @@
N\'oubliez pas de terminer cette tâche !
📝 Tâche : %1$s\n\n🎯 Rappel : %2$s
+
+
+ Affiche votre tâche active et un objectif motivationnel
+ Aucune tâche active.\nOuvrez Motivez-moi pour en ajouter une.
+ Toucher pour ouvrir →
+ active
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 026d285..7d65e33 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -76,5 +76,11 @@
このタスクを完了することを忘れずに!
📝 タスク:%1$s\n\n🎯 リマインダー:%2$s
+
+
+ アクティブなタスクとモチベーション目標を表示
+ アクティブなタスクがありません。\nやる気アップを開いて追加してください。
+ タップして開く →
+ アクティブ
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index c5692c1..55d4621 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -76,5 +76,11 @@
이 작업을 완료하는 것을 잊지 마세요!
📝 작업: %1$s\n\n🎯 알림: %2$s
+
+
+ 활성 작업과 동기 부여 목표를 표시합니다
+ 활성 작업이 없습니다.\n동기부여 앱을 열어 추가하세요.
+ 탭하여 열기 →
+ 활성
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 43e168f..d0ebab3 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -76,5 +76,11 @@
Lembre-se de completar esta tarefa!
📝 Tarefa: %1$s\n\n🎯 Lembre-se: %2$s
+
+
+ Mostra sua tarefa ativa e uma meta motivacional
+ Sem tarefas ativas.\nAbra Motiva-me para adicionar uma.
+ Toque para abrir →
+ ativa
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index 42ec491..84d853a 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -79,8 +79,11 @@
应用将重启以应用语言更改
- 记得完成这个任务!
- 📝 任务:%1$s\n\n🎯 提醒:%2$s
+
+ 显示您的活跃任务和激励目标
+ 没有活跃任务。\n打开激励我来添加一个。
+ 点击打开 →
+ 活跃
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 7f0083c..397b439 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -80,4 +80,10 @@
¡Recuerda completar esta tarea!
📝 Tarea: %1$s\n\n🎯 Recuerda: %2$s
+
+
+ Muestra tu tarea activa y una meta motivacional
+ Sin tareas activas.\nAbre Motívame para añadir una.
+ Toca para abrir →
+ activa
\ No newline at end of file
diff --git a/app/src/main/res/xml/motivame_widget_info.xml b/app/src/main/res/xml/motivame_widget_info.xml
new file mode 100644
index 0000000..15f17f1
--- /dev/null
+++ b/app/src/main/res/xml/motivame_widget_info.xml
@@ -0,0 +1,13 @@
+
+
+