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 @@ + + +