diff --git a/README.md b/README.md index 83e7a3d..62f206c 100644 --- a/README.md +++ b/README.md @@ -1,207 +1,208 @@ -# Motívame - Tu Compañero de Motivación Diaria +# Motívame · Tu Compañero de Motivación Diaria -## 📱 Descripción +

+ Motívame icon +

-**Motívame** es una aplicación Android moderna diseñada para ayudarte a mantener la motivación en tus tareas pendientes. La app te permite configurar recordatorios diarios personalizados con tus metas específicas, ayudándote a visualizar el "por qué" detrás de cada tarea. +

+ Release + Android 7+ + Kotlin + Compose + MIT +

-## ✨ Características Principales +> **Motívame** es una app Android de código abierto que te ayuda a mantener la motivación en tus hábitos y tareas pendientes. Define tus metas, elige con qué frecuencia quieres que te recuerde y deja que la app haga el resto. -- **📝 Gestión de Tareas**: Crea, edita y elimina tareas pendientes fácilmente -- **🎯 Definición de Metas**: Asocia múltiples objetivos a cada tarea para recordar por qué es importante -- **🔔 Notificaciones Diarias**: Recibe recordatorios automáticos todos los días a las 9:00 AM -- **🔊 Alertas Personalizables**: Configura sonido y vibración según tus preferencias -- **⏯️ Control de Tareas**: Activa o pausa tareas según tu conveniencia -- **🎨 Diseño Moderno**: Interfaz Material Design 3 con colores vibrantes y motivadores -- **📊 Tareas Predeterminadas**: Comienza con ejemplos inspiradores o crea las tuyas propias +--- -## 🚀 Funcionalidades Técnicas +## 📥 Descarga -### Arquitectura -- **MVVM (Model-View-ViewModel)**: Separación clara de responsabilidades -- **Jetpack Compose**: UI moderna y declarativa -- **WorkManager**: Tareas programadas en segundo plano confiables -- **DataStore**: Persistencia de datos ligera y eficiente -- **Kotlin Coroutines**: Programación asíncrona fluida +👉 [github.com/manalejandro/motivame](https://github.com/manalejandro/motivame) -### Componentes Principales +--- -#### 1. Pantalla Principal -- Lista de tareas activas y pausadas -- Tarjetas visuales con gradientes -- Indicadores de estado (activo/pausado) -- Navegación rápida a configuración y agregar tareas +## ✨ Características -#### 2. Agregar Tareas -- Campo de título de tarea -- Agregar múltiples metas personalizadas -- Validación de campos -- Interfaz intuitiva con iconos descriptivos +| Función | Descripción | +|---|---| +| 📝 **Gestión de tareas** | Crea, edita (pulsación larga) y elimina tareas | +| 🎯 **Metas por tarea** | Asocia múltiples objetivos a cada tarea | +| ⏯️ **Pausa / Reanudar** | Desactiva temporalmente una tarea sin borrarla | +| 🔔 **Avisos personalizables** | Elige cuántos avisos al día (1–10) y cada cuántos días se repite el ciclo | +| 🎲 **Horarios aleatorios** | Cada aviso se programa a una hora distinta dentro de la franja 9:00–21:00 | +| 🔊 **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 | -#### 3. Configuración -- Activar/desactivar notificaciones -- Control de sonido -- Probar notificaciones en tiempo real -- Solicitud de permisos en Android 13+ +--- -### Sistema de Notificaciones +## 📱 Capturas de pantalla -La aplicación utiliza un sistema de notificaciones inteligente: +| Principal | Añadir tarea | Configuración | +|:---:|:---:|:---:| +| *(lista de tareas con resumen de avisos)* | *(formulario con metas y frecuencia)* | *(idioma, notificaciones, sonido)* | -- **Canal de Alta Prioridad**: Garantiza que las notificaciones sean visibles -- **Vibración Personalizada**: Patrón distintivo para llamar la atención -- **Mensajes Motivacionales**: Cada notificación muestra una meta aleatoria de la tarea -- **Sonido Configurable**: Opción de activar/desactivar sonido de notificación +--- -### WorkManager - Recordatorios Diarios +## 🚀 Cómo funciona -- Ejecuta tareas diarias a las 9:00 AM -- Persiste incluso después de reiniciar el dispositivo -- Optimizado para el consumo de batería -- No requiere conexión a Internet +1. **Crea una tarea** — ponle título y añade tus metas (el «por qué»). +2. **Configura la frecuencia** — número de avisos diarios y cada cuántos días se repite el ciclo. +3. **Recibe recordatorios** — la app programa los avisos a horas aleatorias distintas dentro de 9:00–21:00, distribuidos en días diferentes del ciclo para que no todos lleguen el mismo día. +4. **Pausa o edita** — mantén pulsada una tarea para editarla o usa el botón ⏸ para pausarla sin perder su configuración. -## 📦 Dependencias +--- -```kotlin -// Core Android -androidx.core:core-ktx:1.10.1 -androidx.lifecycle:lifecycle-runtime-ktx:2.6.1 -androidx.activity:activity-compose:1.8.0 +## 🏗️ Arquitectura y tecnología -// Compose -androidx.compose:compose-bom:2024.09.00 -androidx.compose.material3:material3 -androidx.compose.material:material-icons-extended:1.5.4 - -// Architecture Components -androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1 -androidx.work:work-runtime-ktx:2.9.0 -androidx.datastore:datastore-preferences:1.0.0 +``` +MVVM · Jetpack Compose · WorkManager · DataStore · Kotlin Coroutines ``` -## 🔧 Requisitos - -- **Android SDK 24+** (Android 7.0 Nougat o superior) -- **Target SDK 36** -- **Kotlin 2.0.21** -- **Gradle 9.0.1** - -## 🎨 Diseño - -### Paleta de Colores - -- **Primary**: Indigo vibrante (#6366F1) -- **Secondary**: Rosa motivador (#EC4899) -- **Tertiary**: Púrpura (#8B5CF6) -- **Success**: Verde (#10B981) -- **Error**: Rojo (#EF4444) - -### Tipografía -- Fuentes Material Design 3 -- Énfasis en títulos grandes y legibles -- Texto secundario con contraste óptimo - -## 📱 Permisos - -La aplicación solicita los siguientes permisos: - -- `POST_NOTIFICATIONS` (Android 13+): Para mostrar recordatorios -- `VIBRATE`: Para alertas con vibración -- `RECEIVE_BOOT_COMPLETED`: Para mantener recordatorios después de reiniciar - -## 🔄 Flujo de la Aplicación - -1. **Inicio**: Pantalla principal con tareas predeterminadas -2. **Agregar Tarea**: El usuario crea una nueva tarea con sus metas -3. **Configuración**: Personaliza notificaciones y sonido -4. **Recordatorios Automáticos**: WorkManager envía notificaciones diarias -5. **Interacción**: Usuario puede pausar, reanudar o eliminar tareas - -## 🏗️ Estructura del Proyecto +### Estructura del proyecto ``` app/src/main/java/com/manalejandro/motivame/ ├── data/ -│ ├── Task.kt # Modelo de datos -│ └── TaskRepository.kt # Repositorio de persistencia +│ ├── Task.kt # Modelo de datos +│ └── TaskRepository.kt # Persistencia con DataStore ├── notifications/ -│ └── NotificationHelper.kt # Gestión de notificaciones +│ └── NotificationHelper.kt # Envío de notificaciones (Ringtone independiente del canal) ├── ui/ │ ├── screens/ -│ │ ├── MainScreen.kt # Pantalla principal -│ │ ├── AddTaskScreen.kt # Pantalla agregar tarea -│ │ └── SettingsScreen.kt # Pantalla configuración +│ │ ├── MainScreen.kt # Lista de tareas +│ │ ├── AddTaskScreen.kt # Crear / editar tarea +│ │ └── SettingsScreen.kt # Configuración (idioma, notificaciones, sonido) │ ├── theme/ -│ │ ├── Color.kt # Definición de colores -│ │ ├── Theme.kt # Tema de la aplicación -│ │ └── Type.kt # Tipografía +│ │ ├── Color.kt +│ │ ├── Theme.kt +│ │ └── Type.kt │ └── viewmodel/ -│ └── TaskViewModel.kt # ViewModel principal +│ └── TaskViewModel.kt # Estado y lógica de negocio +├── util/ +│ └── LocaleHelper.kt # Cambio de idioma en tiempo de ejecución ├── worker/ -│ └── DailyReminderWorker.kt # Worker para recordatorios -└── MainActivity.kt # Actividad principal +│ └── DailyReminderWorker.kt # WorkManager: ejecuta recordatorios programados +├── MotivameApplication.kt # Application: inicializa el canal de notificación +└── MainActivity.kt # Actividad principal + navegación Compose ``` -## 🚀 Compilación - -```bash -# Compilar versión de depuración -./gradlew assembleDebug - -# Compilar versión de lanzamiento -./gradlew assembleRelease - -# Ejecutar tests -./gradlew test - -# Compilar e instalar -./gradlew installDebug -``` - -## 💡 Casos de Uso - -1. **Estudiante**: Recordatorios para estudiar materias específicas con metas académicas -2. **Fitness**: Mantener rutina de ejercicio con objetivos de salud -3. **Desarrollo Personal**: Hábitos diarios como lectura, meditación, etc. -4. **Productividad**: Tareas profesionales con objetivos de carrera - -## 📝 Tareas Predeterminadas - -La app incluye 3 tareas de ejemplo: - -1. **Hacer ejercicio** - - Mejorar salud cardiovascular - - Sentirse más energético - - Alcanzar peso ideal - -2. **Estudiar inglés** - - Mejores oportunidades laborales - - Viajar sin limitaciones - - Expandir conocimiento - -3. **Leer 30 minutos** - - Desarrollar hábito de lectura - - Aprender cosas nuevas - - Reducir tiempo en redes sociales - -## 🎯 Roadmap Futuro - -- [ ] Estadísticas de cumplimiento -- [ ] Múltiples recordatorios por día -- [ ] Widgets de pantalla de inicio -- [ ] Compartir progreso -- [ ] Temas personalizables -- [ ] Backup en la nube -- [ ] Recordatorios inteligentes basados en ubicación - -## 👨‍💻 Autor - -Desarrollado con ❤️ para ayudar a las personas a mantener su motivación - -## 📄 Licencia - -Este proyecto es de código abierto y está disponible bajo la licencia MIT. - --- -**¡Mantente motivado y alcanza tus metas! 🚀** +## 🌐 Idiomas soportados +| Código | Idioma | +|---|---| +| `es` | 🇪🇸 Español *(predeterminado)* | +| `en` | 🇬🇧 English | +| `zh` | 🇨🇳 中文 | +| `fr` | 🇫🇷 Français | +| `de` | 🇩🇪 Deutsch | +| `pt` | 🇵🇹 Português | +| `ja` | 🇯🇵 日本語 | +| `ko` | 🇰🇷 한국어 | + +El idioma se selecciona desde **Configuración → Idioma** y se aplica instantáneamente sin necesidad de reiniciar el dispositivo. + +--- + +## 🔔 Sistema de notificaciones + +- **Franja horaria**: 9:00–21:00 +- **Horas aleatorias únicas**: cada aviso del ciclo tiene una hora distinta a las demás +- **Distribución en días**: los avisos se reparten entre los días del ciclo para no coincidir todos el mismo día +- **Sonido independiente del canal**: el sonido se reproduce con `RingtoneManager` directamente, sin depender del estado interno del canal de Android — garantiza comportamiento consistente en todos los dispositivos y versiones +- **Canal único con `setSilent(true)`**: la notificación visual se envía siempre silenciosa a nivel de canal; el sonido se controla únicamente desde la preferencia del usuario + +--- + +## 📦 Dependencias principales + +| Librería | Versión | +|---|---| +| Kotlin | 2.0.21 | +| Jetpack Compose BOM | 2024.09.00 | +| Activity Compose | 1.8.0 | +| Lifecycle / ViewModel | 2.6.1 | +| WorkManager | 2.9.0 | +| DataStore Preferences | 1.0.0 | +| Material Icons Extended | 1.5.4 | +| Core KTX | 1.10.1 | + +--- + +## 🔧 Requisitos + +- **Android 7.0+** (API 24) +- **Target SDK**: 36 +- **Gradle**: 9.0.1 + +--- + +## 🔐 Permisos + +| Permiso | Motivo | +|---|---| +| `POST_NOTIFICATIONS` *(Android 13+)* | Mostrar recordatorios | +| `VIBRATE` | Vibración en las notificaciones | +| `RECEIVE_BOOT_COMPLETED` | Reprogramar avisos tras reinicio del dispositivo | + +--- + +## 🛠️ Compilación + +```bash +# Debug +./gradlew assembleDebug + +# Release +./gradlew assembleRelease + +# Instalar en dispositivo conectado +./gradlew installDebug + +# Tests unitarios +./gradlew test +``` + +--- + +## 💡 Casos de uso + +- **Estudiante** — Recordatorios de estudio con metas académicas concretas +- **Fitness** — Mantener rutina de ejercicio con objetivos de salud +- **Desarrollo personal** — Lectura, meditación, idiomas… +- **Productividad profesional** — Tareas con objetivos de carrera + +--- + +## 🗺️ Roadmap + +- [x] Gestión de tareas (crear, editar, eliminar, pausar) +- [x] Múltiples avisos por día con horas aleatorias +- [x] Ciclo de días configurable +- [x] Multiidioma (8 idiomas) +- [x] Sonido configurable independiente del canal Android +- [ ] Estadísticas de cumplimiento +- [ ] Widget de pantalla de inicio +- [ ] Backup en la nube +- [ ] Temas personalizables (claro / oscuro / AMOLED) +- [ ] Recordatorios con imagen motivacional + +--- + +## 👨‍💻 Autor + +Desarrollado por **[manalejandro.com](https://manalejandro.com)** + +--- + +## 📄 Licencia + +Este proyecto está disponible bajo la licencia **MIT**. +Puedes usarlo, modificarlo y distribuirlo libremente citando al autor. + +--- + +

¡Mantente motivado y alcanza tus metas! 🚀

diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ca25991..bcae6c5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ scheduleAllReminders(enabled) }) } } } @@ -40,19 +56,21 @@ class MainActivity : ComponentActivity() { /** * Cancela todos los workers anteriores y programa nuevos recordatorios * para cada tarea activa, distribuyendo los avisos entre las 9:00 y las 21:00. + * @param notificationsEnabled valor ya conocido, para evitar condición de carrera con DataStore. + * Si es null, se lee del DataStore (solo al arrancar la app). */ - fun scheduleAllReminders() { + fun scheduleAllReminders(notificationsEnabled: Boolean? = null) { CoroutineScope(Dispatchers.IO).launch { val repository = TaskRepository(applicationContext) - val tasks = repository.tasks.first() - val notificationEnabled = repository.notificationEnabled.first() // Cancelar todos los workers existentes de recordatorios de tareas WorkManager.getInstance(applicationContext) .cancelAllWorkByTag("task_reminder") - if (!notificationEnabled) return@launch + val enabled = notificationsEnabled ?: repository.notificationEnabled.first() + if (!enabled) return@launch + val tasks = repository.tasks.first() tasks.filter { it.isActive }.forEach { task -> scheduleRemindersForTask(task) } @@ -64,32 +82,40 @@ class MainActivity : ComponentActivity() { val cycleDays = task.repeatEveryDays.coerceIn(1, 30) val workManager = WorkManager.getInstance(applicationContext) - // Ventana de notificaciones: 9:00 a 21:00 (12 horas = 720 minutos) - val windowStartHour = 9 - val windowEndHour = 21 - val windowMinutes = (windowEndHour - windowStartHour) * 60 // 720 min + // Ventana de notificaciones: 9:00 a 21:00 (720 minutos disponibles) + val windowStartMinute = 9 * 60 // 540 + val windowEndMinute = 21 * 60 // 1260 + val windowSize = windowEndMinute - windowStartMinute // 720 - // Distribuir los N avisos a lo largo del ciclo de 'cycleDays' días, - // repartidos uniformemente para que no coincidan todos el mismo día. - // Cada aviso cae en un día y hora distintos dentro del ciclo. - val totalSlots = cycleDays // un aviso por día máximo - val step = totalSlots.toDouble() / reminders // paso fraccionario entre avisos + // Distribuir los N avisos en días distintos dentro del ciclo. + // Si reminders <= cycleDays cada aviso va a un día diferente; + // si hay más avisos que días, se reparten de forma ciclica. + val dayAssignments = (0 until reminders).map { i -> i % cycleDays } + + // Generar horas aleatorias únicas (en minutos desde medianoche) + // Para cada aviso elegimos un minuto al azar dentro de [540, 1260) + // asegurándonos de que no coincida con ningún otro aviso ya asignado. + val usedMinutes = mutableSetOf() + val minuteAssignments = mutableListOf() + + repeat(reminders) { + var candidate: Int + var attempts = 0 + do { + candidate = windowStartMinute + Random.nextInt(windowSize) + attempts++ + // Tras muchos intentos (espacio muy saturado) relajamos la condición + // exigiendo sólo minutos distintos en el mismo día + } while (usedMinutes.contains(candidate) && attempts < windowSize) + usedMinutes.add(candidate) + minuteAssignments.add(candidate) + } for (i in 0 until reminders) { - // Día dentro del ciclo (0-based), distribuido uniformemente - val slotIndex = (i * step).toInt() - val dayOffset = slotIndex % cycleDays - - // Hora dentro de la ventana: distribuida para que los avisos del mismo día - // no se solapen, o usando posición i para variar la hora entre días - val offsetMinutes = if (reminders == 1) { - windowMinutes / 2 // Al mediodía si solo hay 1 aviso - } else { - ((windowMinutes * i) / reminders).coerceIn(0, windowMinutes - 30) - } - - val targetHour = windowStartHour + offsetMinutes / 60 - val targetMinute = offsetMinutes % 60 + val dayOffset = dayAssignments[i] + val totalMinutes = minuteAssignments[i] + val targetHour = totalMinutes / 60 + val targetMinute = totalMinutes % 60 val delayMs = calculateDelayToTimeWithDayOffset(targetHour, targetMinute, dayOffset) @@ -106,11 +132,6 @@ class MainActivity : ComponentActivity() { } } - /** - * Calcula el retardo en milisegundos hasta la próxima ocurrencia de la hora indicada. - */ - private fun calculateDelayToTime(hour: Int, minute: Int): Long = - calculateDelayToTimeWithDayOffset(hour, minute, 0) /** * Calcula el retardo hasta la hora indicada más un desplazamiento de días. @@ -141,25 +162,52 @@ class MainActivity : ComponentActivity() { } @Composable -fun MotivameApp(onRescheduleReminders: () -> Unit = {}) { +fun MotivameApp(onRescheduleReminders: (Boolean) -> Unit = {}) { val viewModel: TaskViewModel = viewModel() + val context = LocalContext.current var currentScreen by remember { mutableStateOf("main") } + var taskToEdit by remember { mutableStateOf(null) } // Registrar callback para reprogramar avisos cuando cambian las tareas LaunchedEffect(viewModel) { - viewModel.onRescheduleReminders = onRescheduleReminders + viewModel.onRescheduleReminders = { enabled -> onRescheduleReminders(enabled) } + } + + // Interceptar el botón físico Atrás del sistema + BackHandler(enabled = currentScreen != "main") { + taskToEdit = null + currentScreen = "main" + } + // En la pantalla principal, minimizar en lugar de cerrar + BackHandler(enabled = currentScreen == "main") { + (context as? ComponentActivity)?.moveTaskToBack(true) } when (currentScreen) { "main" -> MainScreen( viewModel = viewModel, - onNavigateToAddTask = { currentScreen = "add_task" }, - onNavigateToSettings = { currentScreen = "settings" } + onNavigateToAddTask = { + taskToEdit = null + currentScreen = "add_task" + }, + onNavigateToSettings = { currentScreen = "settings" }, + onEditTask = { task -> + taskToEdit = task + currentScreen = "edit_task" + } ) "add_task" -> AddTaskScreen( viewModel = viewModel, onNavigateBack = { currentScreen = "main" } ) + "edit_task" -> AddTaskScreen( + viewModel = viewModel, + onNavigateBack = { + taskToEdit = null + currentScreen = "main" + }, + taskToEdit = taskToEdit + ) "settings" -> SettingsScreen( viewModel = viewModel, onNavigateBack = { currentScreen = "main" } diff --git a/app/src/main/java/com/manalejandro/motivame/MotivameApplication.kt b/app/src/main/java/com/manalejandro/motivame/MotivameApplication.kt new file mode 100644 index 0000000..48819b0 --- /dev/null +++ b/app/src/main/java/com/manalejandro/motivame/MotivameApplication.kt @@ -0,0 +1,45 @@ +package com.manalejandro.motivame + +import android.app.Application +import android.app.NotificationChannel +import android.app.NotificationManager +import android.media.AudioAttributes +import android.media.RingtoneManager +import android.os.Build +import com.manalejandro.motivame.notifications.NotificationHelper + +class MotivameApplication : Application() { + + override fun onCreate() { + super.onCreate() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + + // Limpiar canales obsoletos de versiones anteriores + nm.deleteNotificationChannel("motivame_channel") + nm.deleteNotificationChannel(NotificationHelper.CHANNEL_ID_NO_SOUND) + + // Canal con sonido: crear solo si no existe aún + if (nm.getNotificationChannel(NotificationHelper.CHANNEL_ID_SOUND) == null) { + val soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + val soundChannel = NotificationChannel( + NotificationHelper.CHANNEL_ID_SOUND, + getString(R.string.notification_channel_name), + NotificationManager.IMPORTANCE_HIGH + ).apply { + description = getString(R.string.notification_channel_description) + enableVibration(true) + vibrationPattern = longArrayOf(0, 500, 250, 500) + setSound( + soundUri, + AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .build() + ) + } + nm.createNotificationChannel(soundChannel) + } + } + } +} diff --git a/app/src/main/java/com/manalejandro/motivame/data/TaskRepository.kt b/app/src/main/java/com/manalejandro/motivame/data/TaskRepository.kt index 6d52180..f010c53 100644 --- a/app/src/main/java/com/manalejandro/motivame/data/TaskRepository.kt +++ b/app/src/main/java/com/manalejandro/motivame/data/TaskRepository.kt @@ -19,6 +19,7 @@ class TaskRepository(private val context: Context) { private val TASKS_KEY = stringPreferencesKey("tasks") private val NOTIFICATION_ENABLED_KEY = stringPreferencesKey("notification_enabled") private val SOUND_ENABLED_KEY = stringPreferencesKey("sound_enabled") + private val LANGUAGE_KEY = stringPreferencesKey("language") val DEFAULT_TASKS = listOf( Task( @@ -68,6 +69,11 @@ class TaskRepository(private val context: Context) { preferences[SOUND_ENABLED_KEY]?.toBoolean() ?: true } + val language: Flow = context.dataStore.data + .map { preferences -> + preferences[LANGUAGE_KEY] ?: "es" + } + suspend fun saveTasks(tasks: List) { context.dataStore.edit { preferences -> preferences[TASKS_KEY] = tasksToJson(tasks) @@ -110,6 +116,12 @@ class TaskRepository(private val context: Context) { } } + suspend fun setLanguage(languageCode: String) { + context.dataStore.edit { preferences -> + preferences[LANGUAGE_KEY] = languageCode + } + } + private fun tasksToJson(tasks: List): String { val jsonArray = JSONArray() tasks.forEach { task -> diff --git a/app/src/main/java/com/manalejandro/motivame/notifications/NotificationHelper.kt b/app/src/main/java/com/manalejandro/motivame/notifications/NotificationHelper.kt index 6eea9e9..1643a14 100644 --- a/app/src/main/java/com/manalejandro/motivame/notifications/NotificationHelper.kt +++ b/app/src/main/java/com/manalejandro/motivame/notifications/NotificationHelper.kt @@ -1,12 +1,10 @@ package com.manalejandro.motivame.notifications -import android.app.NotificationChannel -import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.content.Intent +import android.media.Ringtone import android.media.RingtoneManager -import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.manalejandro.motivame.MainActivity @@ -17,77 +15,57 @@ import kotlin.random.Random class NotificationHelper(private val context: Context) { companion object { - private const val CHANNEL_ID = "motivame_channel" - private const val CHANNEL_NAME = "Recordatorios de Tareas" - private const val CHANNEL_DESCRIPTION = "Notificaciones para recordarte tus tareas pendientes" - } - - init { - createNotificationChannel() - } - - private fun createNotificationChannel() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val importance = NotificationManager.IMPORTANCE_HIGH - val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, importance).apply { - description = CHANNEL_DESCRIPTION - enableVibration(true) - vibrationPattern = longArrayOf(0, 500, 250, 500) - } - - val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - notificationManager.createNotificationChannel(channel) - } + const val CHANNEL_ID_SOUND = "motivame_channel_sound" + const val CHANNEL_ID_NO_SOUND = "motivame_channel_silent" } fun sendTaskReminder(task: Task, withSound: Boolean = true) { val intent = Intent(context, MainActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } - val pendingIntent = PendingIntent.getActivity( - context, - 0, - intent, + context, 0, intent, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT ) - val motivationalMessage = if (task.goals.isNotEmpty()) { - task.goals.random() - } else { - "¡Recuerda completar esta tarea!" - } + val motivationalMessage = if (task.goals.isNotEmpty()) task.goals.random() + else context.getString(R.string.notification_default_message) - val notificationBuilder = NotificationCompat.Builder(context, CHANNEL_ID) + // La notificación siempre silenciosa: el sonido lo manejamos nosotros + // directamente con Ringtone para evitar cualquier interferencia del canal. + val notificationBuilder = NotificationCompat.Builder(context, CHANNEL_ID_SOUND) .setSmallIcon(R.drawable.ic_launcher_foreground) .setContentTitle("⏰ ${task.title}") .setContentText(motivationalMessage) - .setStyle(NotificationCompat.BigTextStyle().bigText( - "📝 Tarea: ${task.title}\n\n🎯 Recuerda: $motivationalMessage" - )) + .setStyle( + NotificationCompat.BigTextStyle().bigText( + context.getString(R.string.notification_big_text, task.title, motivationalMessage) + ) + ) .setPriority(NotificationCompat.PRIORITY_HIGH) .setContentIntent(pendingIntent) .setAutoCancel(true) .setVibrate(longArrayOf(0, 500, 250, 500)) - - if (withSound) { - val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) - notificationBuilder.setSound(defaultSoundUri) - } + .setSilent(true) // siempre silenciosa a nivel canal try { - val notificationManager = NotificationManagerCompat.from(context) - notificationManager.notify(Random.nextInt(), notificationBuilder.build()) - } catch (e: SecurityException) { - // El usuario no ha concedido permisos de notificación + NotificationManagerCompat.from(context) + .notify(Random.nextInt(), notificationBuilder.build()) + } catch (_: SecurityException) { } + + // Reproducir el sonido directamente si está activado + if (withSound) { + try { + val soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + val ringtone: Ringtone = RingtoneManager.getRingtone(context, soundUri) + ringtone.play() + } catch (_: Exception) { } } } fun sendMotivationalReminder(tasks: List, withSound: Boolean = true) { if (tasks.isEmpty()) return - val activeTask = tasks.firstOrNull { it.isActive } ?: return sendTaskReminder(activeTask, withSound) } } - diff --git a/app/src/main/java/com/manalejandro/motivame/ui/screens/AddTaskScreen.kt b/app/src/main/java/com/manalejandro/motivame/ui/screens/AddTaskScreen.kt index a408345..44e4828 100644 --- a/app/src/main/java/com/manalejandro/motivame/ui/screens/AddTaskScreen.kt +++ b/app/src/main/java/com/manalejandro/motivame/ui/screens/AddTaskScreen.kt @@ -1,5 +1,6 @@ package com.manalejandro.motivame.ui.screens +import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed @@ -11,8 +12,10 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import com.manalejandro.motivame.R import com.manalejandro.motivame.data.Task import com.manalejandro.motivame.ui.viewmodel.TaskViewModel @@ -20,21 +23,30 @@ import com.manalejandro.motivame.ui.viewmodel.TaskViewModel @Composable fun AddTaskScreen( viewModel: TaskViewModel, - onNavigateBack: () -> Unit + onNavigateBack: () -> Unit, + taskToEdit: Task? = null ) { - var taskTitle by remember { mutableStateOf("") } + val isEditing = taskToEdit != null + var taskTitle by remember { mutableStateOf(taskToEdit?.title ?: "") } var currentGoal by remember { mutableStateOf("") } - var goals by remember { mutableStateOf(listOf()) } - var dailyReminders by remember { mutableStateOf(3) } - var repeatEveryDays by remember { mutableStateOf(3) } + var goals by remember { mutableStateOf(taskToEdit?.goals ?: listOf()) } + var dailyReminders by remember { mutableStateOf(taskToEdit?.dailyReminders ?: 3) } + var repeatEveryDays by remember { mutableStateOf(taskToEdit?.repeatEveryDays ?: 3) } + + BackHandler { onNavigateBack() } Scaffold( topBar = { TopAppBar( - title = { Text("Nueva Tarea") }, + title = { + Text( + if (isEditing) stringResource(R.string.edit_task_title) + else stringResource(R.string.new_task_title) + ) + }, navigationIcon = { IconButton(onClick = onNavigateBack) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Volver") + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back_content_desc)) } }, colors = TopAppBarDefaults.topAppBarColors( @@ -62,7 +74,7 @@ fun AddTaskScreen( .padding(16.dp) ) { Text( - text = "📝 ¿Qué debes recordar?", + text = stringResource(R.string.what_to_remember), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary @@ -71,8 +83,8 @@ fun AddTaskScreen( OutlinedTextField( value = taskTitle, onValueChange = { taskTitle = it }, - label = { Text("Título de la tarea") }, - placeholder = { Text("Ej: Hacer ejercicio") }, + label = { Text(stringResource(R.string.task_title_label)) }, + placeholder = { Text(stringResource(R.string.task_title_placeholder)) }, modifier = Modifier.fillMaxWidth(), singleLine = true, leadingIcon = { @@ -94,7 +106,7 @@ fun AddTaskScreen( .padding(16.dp) ) { Text( - text = "🎯 ¿Qué esperas alcanzar?", + text = stringResource(R.string.what_to_achieve), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary @@ -104,8 +116,8 @@ fun AddTaskScreen( OutlinedTextField( value = currentGoal, onValueChange = { currentGoal = it }, - label = { Text("Nueva meta") }, - placeholder = { Text("Ej: Mejorar mi salud") }, + label = { Text(stringResource(R.string.new_goal_label)) }, + placeholder = { Text(stringResource(R.string.new_goal_placeholder)) }, modifier = Modifier.fillMaxWidth(), trailingIcon = { IconButton( @@ -117,7 +129,7 @@ fun AddTaskScreen( }, enabled = currentGoal.isNotBlank() ) { - Icon(Icons.Default.Add, contentDescription = "Agregar meta") + Icon(Icons.Default.Add, contentDescription = stringResource(R.string.add_goal_desc)) } } ) @@ -128,7 +140,7 @@ fun AddTaskScreen( if (goals.isNotEmpty()) { item { Text( - text = "Metas agregadas:", + text = stringResource(R.string.goals_added), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurfaceVariant @@ -173,7 +185,7 @@ fun AddTaskScreen( ) { Icon( imageVector = Icons.Default.Delete, - contentDescription = "Eliminar meta", + contentDescription = stringResource(R.string.delete_goal_desc), tint = MaterialTheme.colorScheme.error ) } @@ -193,14 +205,14 @@ fun AddTaskScreen( .padding(16.dp) ) { Text( - text = "🔔 Avisos diarios", + text = stringResource(R.string.daily_reminders_title), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary ) Spacer(modifier = Modifier.height(4.dp)) Text( - text = "Número de recordatorios entre las 9:00 y las 21:00", + text = stringResource(R.string.daily_reminders_subtitle), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -217,7 +229,7 @@ fun AddTaskScreen( ) { Icon( imageVector = Icons.Default.KeyboardArrowDown, - contentDescription = "Reducir", + contentDescription = stringResource(R.string.decrease_desc), tint = if (dailyReminders > 1) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant ) @@ -231,7 +243,8 @@ fun AddTaskScreen( color = MaterialTheme.colorScheme.primary ) Text( - text = if (dailyReminders == 1) "aviso" else "avisos", + text = if (dailyReminders == 1) stringResource(R.string.reminder_singular) + else stringResource(R.string.reminder_plural), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -243,7 +256,7 @@ fun AddTaskScreen( ) { Icon( imageVector = Icons.Default.KeyboardArrowUp, - contentDescription = "Aumentar", + contentDescription = stringResource(R.string.increase_desc), tint = if (dailyReminders < 10) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant ) @@ -254,7 +267,7 @@ fun AddTaskScreen( Spacer(modifier = Modifier.height(8.dp)) val intervalMinutes = 720 / (dailyReminders - 1) Text( - text = "⏱️ Un aviso cada ${formatInterval(intervalMinutes)} aprox.", + text = stringResource(R.string.interval_hint, formatInterval(intervalMinutes)), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -274,14 +287,14 @@ fun AddTaskScreen( .padding(16.dp) ) { Text( - text = "📅 Cada cuántos días", + text = stringResource(R.string.repeat_days_title), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary ) Spacer(modifier = Modifier.height(4.dp)) Text( - text = "Intervalo de días entre cada ciclo de avisos", + text = stringResource(R.string.repeat_days_subtitle), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -298,7 +311,7 @@ fun AddTaskScreen( ) { Icon( imageVector = Icons.Default.KeyboardArrowDown, - contentDescription = "Reducir días", + contentDescription = stringResource(R.string.decrease_days_desc), tint = if (repeatEveryDays > 1) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant ) @@ -312,7 +325,8 @@ fun AddTaskScreen( color = MaterialTheme.colorScheme.primary ) Text( - text = if (repeatEveryDays == 1) "día" else "días", + text = if (repeatEveryDays == 1) stringResource(R.string.day_singular) + else stringResource(R.string.day_plural), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -324,7 +338,7 @@ fun AddTaskScreen( ) { Icon( imageVector = Icons.Default.KeyboardArrowUp, - contentDescription = "Aumentar días", + contentDescription = stringResource(R.string.increase_days_desc), tint = if (repeatEveryDays < 30) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant ) @@ -333,8 +347,8 @@ fun AddTaskScreen( Spacer(modifier = Modifier.height(8.dp)) Text( - text = if (repeatEveryDays == 1) "🔁 Avisos todos los días" - else "🔁 Avisos cada $repeatEveryDays días, repartidos para no coincidir", + text = if (repeatEveryDays == 1) stringResource(R.string.repeat_every_day) + else stringResource(R.string.repeat_every_n_days, repeatEveryDays), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -347,14 +361,26 @@ fun AddTaskScreen( Button( onClick = { if (taskTitle.isNotBlank()) { - val newTask = Task( - title = taskTitle.trim(), - goals = goals, - isActive = true, - dailyReminders = dailyReminders, - repeatEveryDays = repeatEveryDays - ) - viewModel.addTask(newTask) + val existing = taskToEdit + if (existing != null) { + viewModel.updateTask( + existing.copy( + title = taskTitle.trim(), + goals = goals, + dailyReminders = dailyReminders, + repeatEveryDays = repeatEveryDays + ) + ) + } else { + val newTask = Task( + title = taskTitle.trim(), + goals = goals, + isActive = true, + dailyReminders = dailyReminders, + repeatEveryDays = repeatEveryDays + ) + viewModel.addTask(newTask) + } onNavigateBack() } }, @@ -367,7 +393,8 @@ fun AddTaskScreen( Icon(Icons.Default.Check, contentDescription = null) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Guardar Tarea", + text = if (isEditing) stringResource(R.string.update_task) + else stringResource(R.string.save_task), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold ) diff --git a/app/src/main/java/com/manalejandro/motivame/ui/screens/MainScreen.kt b/app/src/main/java/com/manalejandro/motivame/ui/screens/MainScreen.kt index 9ab0bdd..e93b21f 100644 --- a/app/src/main/java/com/manalejandro/motivame/ui/screens/MainScreen.kt +++ b/app/src/main/java/com/manalejandro/motivame/ui/screens/MainScreen.kt @@ -1,6 +1,8 @@ package com.manalejandro.motivame.ui.screens +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -13,8 +15,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import com.manalejandro.motivame.R import com.manalejandro.motivame.data.Task import com.manalejandro.motivame.ui.viewmodel.TaskViewModel @@ -23,7 +27,8 @@ import com.manalejandro.motivame.ui.viewmodel.TaskViewModel fun MainScreen( viewModel: TaskViewModel, onNavigateToAddTask: () -> Unit, - onNavigateToSettings: () -> Unit + onNavigateToSettings: () -> Unit, + onEditTask: (Task) -> Unit = {} ) { val tasks by viewModel.tasks.collectAsState() @@ -32,14 +37,14 @@ fun MainScreen( TopAppBar( title = { Text( - "Motívame", + stringResource(R.string.app_name), style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold ) }, actions = { IconButton(onClick = onNavigateToSettings) { - Icon(Icons.Default.Settings, contentDescription = "Configuración") + Icon(Icons.Default.Settings, contentDescription = stringResource(R.string.settings_content_desc)) } }, colors = TopAppBarDefaults.topAppBarColors( @@ -53,7 +58,7 @@ fun MainScreen( onClick = onNavigateToAddTask, containerColor = MaterialTheme.colorScheme.primary ) { - Icon(Icons.Default.Add, contentDescription = "Agregar tarea") + Icon(Icons.Default.Add, contentDescription = stringResource(R.string.add_task_content_desc)) } } ) { paddingValues -> @@ -71,7 +76,8 @@ fun MainScreen( TaskCard( task = task, onToggleActive = { viewModel.updateTask(task.copy(isActive = !task.isActive)) }, - onDelete = { viewModel.deleteTask(task.id) } + onDelete = { viewModel.deleteTask(task.id) }, + onEdit = { onEditTask(task) } ) } } @@ -79,16 +85,23 @@ fun MainScreen( } } +@OptIn(ExperimentalFoundationApi::class) @Composable fun TaskCard( task: Task, onToggleActive: () -> Unit, - onDelete: () -> Unit + onDelete: () -> Unit, + onEdit: () -> Unit = {} ) { var showDeleteDialog by remember { mutableStateOf(false) } Card( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .combinedClickable( + onClick = {}, + onLongClick = onEdit + ), elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), shape = RoundedCornerShape(16.dp) ) { @@ -98,8 +111,10 @@ fun TaskCard( .background( brush = Brush.verticalGradient( colors = listOf( - MaterialTheme.colorScheme.surfaceVariant, - MaterialTheme.colorScheme.surface + if (task.isActive) MaterialTheme.colorScheme.surfaceVariant + else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), + if (task.isActive) MaterialTheme.colorScheme.surface + else MaterialTheme.colorScheme.surface.copy(alpha = 0.5f) ) ) ) @@ -117,30 +132,51 @@ fun TaskCard( Icon( imageVector = Icons.Default.Star, contentDescription = null, - tint = MaterialTheme.colorScheme.primary, + tint = if (task.isActive) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), modifier = Modifier.size(24.dp) ) Spacer(modifier = Modifier.width(8.dp)) - Text( - text = task.title, - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface - ) + Column { + Text( + text = task.title, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = if (task.isActive) MaterialTheme.colorScheme.onSurface + else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f) + ) + Text( + text = stringResource(R.string.long_press_hint), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + } } Row { + // Botón pausa/reanudar IconButton(onClick = onToggleActive) { Icon( - imageVector = if (task.isActive) Icons.Default.Check else Icons.Default.Close, - contentDescription = "Toggle activo", - tint = if (task.isActive) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error + imageVector = if (task.isActive) Icons.Default.Pause else Icons.Default.PlayArrow, + contentDescription = if (task.isActive) stringResource(R.string.pause_task_desc) + else stringResource(R.string.resume_task_desc), + tint = if (task.isActive) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.tertiary ) } + // Botón editar + IconButton(onClick = onEdit) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = stringResource(R.string.edit_task_title), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + // Botón eliminar IconButton(onClick = { showDeleteDialog = true }) { Icon( imageVector = Icons.Default.Delete, - contentDescription = "Eliminar", + contentDescription = stringResource(R.string.delete_task_desc), tint = MaterialTheme.colorScheme.error ) } @@ -150,10 +186,11 @@ fun TaskCard( if (task.goals.isNotEmpty()) { Spacer(modifier = Modifier.height(12.dp)) Text( - text = "🎯 Metas:", + text = stringResource(R.string.goals_label), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.primary + color = if (task.isActive) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) ) Spacer(modifier = Modifier.height(8.dp)) @@ -165,18 +202,48 @@ fun TaskCard( Text( text = "•", style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary, + color = if (task.isActive) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), modifier = Modifier.padding(end = 8.dp) ) Text( text = goal, style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = if (task.isActive) MaterialTheme.colorScheme.onSurfaceVariant + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) ) } } } + // Resumen de avisos + Spacer(modifier = Modifier.height(10.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(6.dp)) + .background(MaterialTheme.colorScheme.primaryContainer.copy(alpha = if (task.isActive) 1f else 0.4f)) + .padding(horizontal = 10.dp, vertical = 4.dp) + ) { + Icon( + imageVector = Icons.Default.Notifications, + contentDescription = null, + modifier = Modifier.size(14.dp), + tint = if (task.isActive) MaterialTheme.colorScheme.onPrimaryContainer + else MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.5f) + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = if (task.repeatEveryDays == 1) + stringResource(R.string.task_summary_reminders_daily, task.dailyReminders) + else + stringResource(R.string.task_summary_reminders, task.dailyReminders, task.repeatEveryDays), + style = MaterialTheme.typography.labelSmall, + color = if (task.isActive) MaterialTheme.colorScheme.onPrimaryContainer + else MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.5f) + ) + } + if (!task.isActive) { Spacer(modifier = Modifier.height(8.dp)) Box( @@ -186,7 +253,7 @@ fun TaskCard( .padding(horizontal = 12.dp, vertical = 6.dp) ) { Text( - text = "⏸️ Pausada", + text = stringResource(R.string.task_paused), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onErrorContainer ) @@ -198,8 +265,8 @@ fun TaskCard( if (showDeleteDialog) { AlertDialog( onDismissRequest = { showDeleteDialog = false }, - title = { Text("Eliminar tarea") }, - text = { Text("¿Estás seguro de que quieres eliminar '${task.title}'?") }, + title = { Text(stringResource(R.string.delete_task_title)) }, + text = { Text(stringResource(R.string.delete_task_confirm, task.title)) }, confirmButton = { TextButton( onClick = { @@ -207,12 +274,12 @@ fun TaskCard( showDeleteDialog = false } ) { - Text("Eliminar", color = MaterialTheme.colorScheme.error) + Text(stringResource(R.string.delete), color = MaterialTheme.colorScheme.error) } }, dismissButton = { TextButton(onClick = { showDeleteDialog = false }) { - Text("Cancelar") + Text(stringResource(R.string.cancel)) } } ) @@ -236,17 +303,16 @@ fun EmptyState(modifier: Modifier = Modifier) { ) Spacer(modifier = Modifier.height(16.dp)) Text( - text = "¡Comienza tu viaje!", + text = stringResource(R.string.empty_state_title), style = MaterialTheme.typography.headlineMedium, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Agrega tu primera tarea y metas para mantenerte motivado", + text = stringResource(R.string.empty_state_subtitle), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant ) } } - diff --git a/app/src/main/java/com/manalejandro/motivame/ui/screens/SettingsScreen.kt b/app/src/main/java/com/manalejandro/motivame/ui/screens/SettingsScreen.kt index adaef9e..33ddbe0 100644 --- a/app/src/main/java/com/manalejandro/motivame/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/com/manalejandro/motivame/ui/screens/SettingsScreen.kt @@ -1,10 +1,15 @@ package com.manalejandro.motivame.ui.screens import android.Manifest +import android.app.Activity +import android.content.Intent import android.content.pm.PackageManager +import android.net.Uri import android.os.Build +import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons @@ -15,12 +20,15 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat -import com.manalejandro.motivame.data.TaskRepository +import com.manalejandro.motivame.R import com.manalejandro.motivame.notifications.NotificationHelper import com.manalejandro.motivame.ui.viewmodel.TaskViewModel +import com.manalejandro.motivame.util.LocaleHelper import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @@ -34,6 +42,9 @@ fun SettingsScreen( val notificationEnabled by viewModel.notificationEnabled.collectAsState() val soundEnabled by viewModel.soundEnabled.collectAsState() val tasks by viewModel.tasks.collectAsState() + val currentLanguage by viewModel.language.collectAsState() + + BackHandler { onNavigateBack() } var hasNotificationPermission by remember { mutableStateOf( @@ -54,13 +65,15 @@ fun SettingsScreen( hasNotificationPermission = isGranted } + var languageMenuExpanded by remember { mutableStateOf(false) } + Scaffold( topBar = { TopAppBar( - title = { Text("Configuración") }, + title = { Text(stringResource(R.string.settings_title)) }, navigationIcon = { IconButton(onClick = onNavigateBack) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Volver") + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back_content_desc)) } }, colors = TopAppBarDefaults.topAppBarColors( @@ -77,6 +90,7 @@ fun SettingsScreen( .padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { + // --- Idioma --- item { Card( modifier = Modifier.fillMaxWidth(), @@ -88,7 +102,87 @@ fun SettingsScreen( .padding(16.dp) ) { Text( - text = "🔔 Notificaciones", + text = stringResource(R.string.language_section), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = stringResource(R.string.language_desc), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(12.dp)) + + val selectedLang = LocaleHelper.SUPPORTED_LANGUAGES + .firstOrNull { it.code == currentLanguage } + ?: LocaleHelper.SUPPORTED_LANGUAGES.first() + + Box { + OutlinedButton( + onClick = { languageMenuExpanded = true }, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "${selectedLang.flag} ${selectedLang.nativeName}", + modifier = Modifier.weight(1f) + ) + Icon(Icons.Default.ArrowDropDown, contentDescription = null) + } + DropdownMenu( + expanded = languageMenuExpanded, + onDismissRequest = { languageMenuExpanded = false } + ) { + LocaleHelper.SUPPORTED_LANGUAGES.forEach { lang -> + DropdownMenuItem( + text = { + Row(verticalAlignment = Alignment.CenterVertically) { + Text(text = lang.flag, style = MaterialTheme.typography.bodyLarge) + Spacer(modifier = Modifier.width(8.dp)) + Text(text = lang.nativeName) + if (lang.code == currentLanguage) { + Spacer(modifier = Modifier.width(8.dp)) + Icon( + Icons.Default.Check, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.primary + ) + } + } + }, + onClick = { + languageMenuExpanded = false + if (lang.code != currentLanguage) { + scope.launch { + viewModel.setLanguage(lang.code) + // Recrear la Activity para aplicar el nuevo locale + (context as? Activity)?.recreate() + } + } + } + ) + } + } + } + } + } + } + + // --- Notificaciones --- + item { + Card( + modifier = Modifier.fillMaxWidth(), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text( + text = stringResource(R.string.notifications_section), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary @@ -102,12 +196,12 @@ fun SettingsScreen( ) { Column(modifier = Modifier.weight(1f)) { Text( - text = "Recordatorios diarios", + text = stringResource(R.string.daily_reminders_setting), style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium ) Text( - text = "Recibe notificaciones para motivarte", + text = stringResource(R.string.daily_reminders_desc), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -135,12 +229,12 @@ fun SettingsScreen( ) { Column(modifier = Modifier.weight(1f)) { Text( - text = "Sonido", + text = stringResource(R.string.sound_setting), style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium ) Text( - text = "Reproducir sonido con las notificaciones", + text = stringResource(R.string.sound_desc), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -154,6 +248,7 @@ fun SettingsScreen( } } + // --- Prueba --- item { Card( modifier = Modifier.fillMaxWidth(), @@ -165,14 +260,14 @@ fun SettingsScreen( .padding(16.dp) ) { Text( - text = "🧪 Prueba", + text = stringResource(R.string.test_section), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Envía una notificación de prueba para verificar que todo funciona correctamente", + text = stringResource(R.string.test_desc), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -194,13 +289,13 @@ fun SettingsScreen( ) { Icon(Icons.Default.Notifications, contentDescription = null) Spacer(modifier = Modifier.width(8.dp)) - Text("Enviar notificación de prueba") + Text(stringResource(R.string.send_test_notification)) } if (tasks.isEmpty()) { Spacer(modifier = Modifier.height(8.dp)) Text( - text = "⚠️ Agrega al menos una tarea para probar las notificaciones", + text = stringResource(R.string.add_task_to_test), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.error ) @@ -209,6 +304,7 @@ fun SettingsScreen( } } + // --- Sobre la app --- item { Card( modifier = Modifier.fillMaxWidth(), @@ -223,29 +319,86 @@ fun SettingsScreen( .padding(16.dp) ) { Text( - text = "ℹ️ Sobre la app", + text = stringResource(R.string.about_section), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onTertiaryContainer ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = "Motívame v1.0", + text = stringResource(R.string.app_version), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onTertiaryContainer ) Text( - text = "Tu compañero para mantener la motivación en tus tareas diarias", + text = stringResource(R.string.app_description), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.7f) ) + + Spacer(modifier = Modifier.height(12.dp)) + HorizontalDivider(color = MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.2f)) + Spacer(modifier = Modifier.height(12.dp)) + + // Desarrollado por + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = Icons.Default.Person, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.8f) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = stringResource(R.string.developed_by) + " ", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.8f) + ) + Text( + text = stringResource(R.string.developer_url), + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onTertiaryContainer, + textDecoration = TextDecoration.Underline, + modifier = Modifier.clickable { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://manalejandro.com")) + context.startActivity(intent) + } + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + // Repositorio GitHub + val githubUrl = stringResource(R.string.github_url) + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = Icons.Default.Code, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.8f) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = stringResource(R.string.github_label) + ": ", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.8f) + ) + Text( + text = stringResource(R.string.github_url), + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onTertiaryContainer, + textDecoration = TextDecoration.Underline, + modifier = Modifier.clickable { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(githubUrl)) + context.startActivity(intent) + } + ) + } } } } } } } - - - - 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 7a8012e..0e65c1b 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 @@ -23,8 +23,12 @@ class TaskViewModel(application: Application) : AndroidViewModel(application) { private val _soundEnabled = MutableStateFlow(true) val soundEnabled: StateFlow = _soundEnabled.asStateFlow() - /** Callback que se invoca tras cualquier cambio en las tareas para reprogramar avisos */ - var onRescheduleReminders: (() -> Unit)? = null + private val _language = MutableStateFlow("es") + val language: StateFlow = _language.asStateFlow() + + /** Callback que se invoca tras cualquier cambio en las tareas para reprogramar avisos. + * Recibe el valor actual de notificationEnabled para evitar condiciones de carrera con DataStore. */ + var onRescheduleReminders: ((notificationsEnabled: Boolean) -> Unit)? = null init { loadTasks() @@ -50,26 +54,31 @@ class TaskViewModel(application: Application) : AndroidViewModel(application) { _soundEnabled.value = enabled } } + viewModelScope.launch { + repository.language.collect { lang -> + _language.value = lang + } + } } fun addTask(task: Task) { viewModelScope.launch { repository.addTask(task) - onRescheduleReminders?.invoke() + onRescheduleReminders?.invoke(_notificationEnabled.value) } } fun updateTask(task: Task) { viewModelScope.launch { repository.updateTask(task) - onRescheduleReminders?.invoke() + onRescheduleReminders?.invoke(_notificationEnabled.value) } } fun deleteTask(taskId: String) { viewModelScope.launch { repository.deleteTask(taskId) - onRescheduleReminders?.invoke() + onRescheduleReminders?.invoke(_notificationEnabled.value) } } @@ -77,7 +86,8 @@ class TaskViewModel(application: Application) : AndroidViewModel(application) { viewModelScope.launch { repository.setNotificationEnabled(enabled) _notificationEnabled.value = enabled - onRescheduleReminders?.invoke() + // Pasamos `enabled` directamente para no releer DataStore + onRescheduleReminders?.invoke(enabled) } } @@ -87,5 +97,9 @@ class TaskViewModel(application: Application) : AndroidViewModel(application) { _soundEnabled.value = enabled } } -} + suspend fun setLanguage(languageCode: String) { + repository.setLanguage(languageCode) + _language.value = languageCode + } +} diff --git a/app/src/main/java/com/manalejandro/motivame/util/LocaleHelper.kt b/app/src/main/java/com/manalejandro/motivame/util/LocaleHelper.kt new file mode 100644 index 0000000..d115203 --- /dev/null +++ b/app/src/main/java/com/manalejandro/motivame/util/LocaleHelper.kt @@ -0,0 +1,35 @@ +package com.manalejandro.motivame.util + +import android.content.Context +import android.content.res.Configuration +import java.util.Locale + +object LocaleHelper { + + data class Language(val code: String, val flag: String, val nativeName: String) + + val SUPPORTED_LANGUAGES = listOf( + Language("es", "🇪🇸", "Español"), + Language("en", "🇬🇧", "English"), + Language("zh", "🇨🇳", "中文"), + Language("fr", "🇫🇷", "Français"), + Language("de", "🇩🇪", "Deutsch"), + Language("pt", "🇵🇹", "Português"), + Language("ja", "🇯🇵", "日本語"), + Language("ko", "🇰🇷", "한국어") + ) + + fun applyLocale(context: Context, languageCode: String): Context { + val locale = Locale(languageCode) + Locale.setDefault(locale) + val config = Configuration(context.resources.configuration) + config.setLocale(locale) + return context.createConfigurationContext(config) + } + + fun wrap(context: Context, languageCode: String): Context { + if (languageCode.isEmpty()) return context + return applyLocale(context, languageCode) + } +} + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml new file mode 100644 index 0000000..22b696f --- /dev/null +++ b/app/src/main/res/values-de/strings.xml @@ -0,0 +1,80 @@ + + + Motivier mich + Aufgabenerinnerungen + Benachrichtigungen, die Sie an Ihre ausstehenden Aufgaben erinnern + + Einstellungen + Aufgabe hinzufügen + Beginne deine Reise! + Füge deine erste Aufgabe und Ziele hinzu, um motiviert zu bleiben + 🎯 Ziele: + ⏸️ Pausiert + Aufgabe aktivieren/pausieren + Löschen + Aufgabe löschen + Möchtest du \'%1$s\' wirklich löschen? + Löschen + Abbrechen + + Neue Aufgabe + Zurück + 📝 Was musst du dir merken? + Aufgabentitel + z.B.: Sport treiben + 🎯 Was möchtest du erreichen? + Neues Ziel + z.B.: Meine Gesundheit verbessern + Ziel hinzufügen + Hinzugefügte Ziele: + Ziel löschen + 🔔 Tägliche Erinnerungen + Anzahl der Erinnerungen zwischen 9:00 und 21:00 Uhr + Erinnerung + Erinnerungen + Verringern + Erhöhen + ⏱️ Eine Erinnerung alle %1$s ca. + 📅 Alle wie viele Tage + Tagesintervall zwischen jedem Erinnerungszyklus + Tag + Tage + Tage verringern + Tage erhöhen + 🔁 Täglich Erinnerungen + 🔁 Erinnerungen alle %1$d Tage, verteilt um Überschneidungen zu vermeiden + Aufgabe speichern + Aufgabe bearbeiten + Änderungen speichern + Aufgabe pausieren + Aufgabe fortsetzen + Lang drücken zum Bearbeiten + %1$d Erinnerung/Tag · alle %2$d Tage + %1$d Erinnerung/Tag · täglich + + Einstellungen + 🔔 Benachrichtigungen + Tägliche Erinnerungen + Benachrichtigungen erhalten, um dich zu motivieren + Ton + Ton bei Benachrichtigungen abspielen + 🧪 Test + Eine Test-Benachrichtigung senden, um zu prüfen, ob alles funktioniert + Test-Benachrichtigung senden + ⚠️ Füge mindestens eine Aufgabe hinzu, um Benachrichtigungen zu testen + ℹ️ Über die App + Motivier mich v1.0 + Dein Begleiter, um in deinen täglichen Aufgaben motiviert zu bleiben + Entwickelt von + manalejandro.com + GitHub-Repository + https://github.com/manalejandro/motivame + + 🌐 Sprache + Wähle die Sprache der Anwendung + Die App wird neu gestartet, um die Sprache anzuwenden + + Denke daran, diese Aufgabe abzuschließen! + 📝 Aufgabe: %1$s\n\n🎯 Denk daran: %2$s + + diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml new file mode 100644 index 0000000..e7c30c9 --- /dev/null +++ b/app/src/main/res/values-en/strings.xml @@ -0,0 +1,86 @@ + + + Motivate Me + Task Reminders + Notifications to remind you of your pending tasks + + + Settings + Add task + Start your journey! + Add your first task and goals to stay motivated + 🎯 Goals: + ⏸️ Paused + Toggle task active + Delete + Delete task + Are you sure you want to delete \'%1$s\'? + Delete + Cancel + + + New Task + Back + 📝 What do you need to remember? + Task title + E.g.: Exercise + 🎯 What do you expect to achieve? + New goal + E.g.: Improve my health + Add goal + Added goals: + Delete goal + 🔔 Daily reminders + Number of reminders between 9:00 and 21:00 + reminder + reminders + Decrease + Increase + ⏱️ One reminder every %1$s approx. + 📅 Every how many days + Interval of days between each reminder cycle + day + days + Decrease days + Increase days + 🔁 Reminders every day + 🔁 Reminders every %1$d days, spread out to avoid overlapping + Save Task + Edit Task + Save Changes + Pause task + Resume task + Long press to edit + %1$d reminder/day · every %2$d days + %1$d reminder/day · every day + + + Settings + 🔔 Notifications + Daily reminders + Receive notifications to motivate yourself + Sound + Play sound with notifications + 🧪 Test + Send a test notification to verify everything works correctly + Send test notification + ⚠️ Add at least one task to test notifications + ℹ️ About the app + Motivate Me v1.0 + Your companion to stay motivated on your daily tasks + Developed by + manalejandro.com + GitHub Repository + https://github.com/manalejandro/motivame + + + 🌐 Language + Select the application language + The app will restart to apply the language + + + Remember to complete this task! + 📝 Task: %1$s\n\n🎯 Remember: %2$s + + + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000..1cc287b --- /dev/null +++ b/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,80 @@ + + + Motivez-moi + Rappels de tâches + Notifications pour vous rappeler vos tâches en attente + + Paramètres + Ajouter une tâche + Commencez votre voyage ! + Ajoutez votre première tâche et vos objectifs pour rester motivé + 🎯 Objectifs : + ⏸️ En pause + Activer/mettre en pause la tâche + Supprimer + Supprimer la tâche + Voulez-vous vraiment supprimer \'%1$s\' ? + Supprimer + Annuler + + Nouvelle tâche + Retour + 📝 Que devez-vous retenir ? + Titre de la tâche + Ex : Faire du sport + 🎯 Qu\'espérez-vous accomplir ? + Nouvel objectif + Ex : Améliorer ma santé + Ajouter un objectif + Objectifs ajoutés : + Supprimer l\'objectif + 🔔 Rappels quotidiens + Nombre de rappels entre 9h00 et 21h00 + rappel + rappels + Diminuer + Augmenter + ⏱️ Un rappel toutes les %1$s environ. + 📅 Tous les combien de jours + Intervalle de jours entre chaque cycle de rappels + jour + jours + Réduire les jours + Augmenter les jours + 🔁 Rappels tous les jours + 🔁 Rappels tous les %1$d jours, répartis pour ne pas se chevaucher + Enregistrer la tâche + Modifier la tâche + Enregistrer les modifications + Mettre en pause + Reprendre + Appui long pour modifier + %1$d rappel/jour · tous les %2$d jours + %1$d rappel/jour · chaque jour + + Paramètres + 🔔 Notifications + Rappels quotidiens + Recevoir des notifications pour vous motiver + Son + Jouer un son avec les notifications + 🧪 Test + Envoyer une notification de test pour vérifier que tout fonctionne + Envoyer une notification de test + ⚠️ Ajoutez au moins une tâche pour tester les notifications + ℹ️ À propos + Motivez-moi v1.0 + Votre compagnon pour rester motivé dans vos tâches quotidiennes + Développé par + manalejandro.com + Dépôt GitHub + https://github.com/manalejandro/motivame + + 🌐 Langue + Sélectionnez la langue de l\'application + L\'app redémarrera pour appliquer la langue + + N\'oubliez pas de terminer cette tâche ! + 📝 Tâche : %1$s\n\n🎯 Rappel : %2$s + + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml new file mode 100644 index 0000000..026d285 --- /dev/null +++ b/app/src/main/res/values-ja/strings.xml @@ -0,0 +1,80 @@ + + + やる気アップ + タスクリマインダー + 保留中のタスクを通知するお知らせ + + 設定 + タスクを追加 + 旅を始めよう! + 最初のタスクと目標を追加してモチベーションを維持しよう + 🎯 目標: + ⏸️ 一時停止中 + タスクの有効/一時停止を切り替え + 削除 + タスクを削除 + \'%1$s\' を削除してもよろしいですか? + 削除 + キャンセル + + 新しいタスク + 戻る + 📝 何を覚えておく必要がありますか? + タスクのタイトル + 例:運動する + 🎯 何を達成したいですか? + 新しい目標 + 例:健康を改善する + 目標を追加 + 追加された目標: + 目標を削除 + 🔔 毎日のリマインダー + 9:00〜21:00の間のリマインダー数 + 回のリマインダー + 回のリマインダー + 減らす + 増やす + ⏱️ 約%1$sごとに1回のリマインダー + 📅 何日ごとに繰り返す + 各リマインダーサイクル間の日数 + + + 日数を減らす + 日数を増やす + 🔁 毎日リマインダー + 🔁 %1$d日ごとにリマインダー、重複しないよう分散 + タスクを保存 + タスクを編集 + 変更を保存 + タスクを一時停止 + タスクを再開 + 長押しで編集 + %1$d回/日 · %2$d日ごと + %1$d回/日 · 毎日 + + 設定 + 🔔 通知 + 毎日のリマインダー + モチベーションを高める通知を受け取る + サウンド + 通知時にサウンドを再生する + 🧪 テスト + すべてが正常に機能していることを確認するためにテスト通知を送信する + テスト通知を送信 + ⚠️ 通知をテストするには少なくとも1つのタスクを追加してください + ℹ️ アプリについて + やる気アップ v1.0 + 日常のタスクでモチベーションを維持するためのコンパニオン + 開発者 + manalejandro.com + GitHubリポジトリ + https://github.com/manalejandro/motivame + + 🌐 言語 + アプリケーションの言語を選択してください + 言語を適用するためにアプリが再起動します + + このタスクを完了することを忘れずに! + 📝 タスク:%1$s\n\n🎯 リマインダー:%2$s + + diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml new file mode 100644 index 0000000..c5692c1 --- /dev/null +++ b/app/src/main/res/values-ko/strings.xml @@ -0,0 +1,80 @@ + + + 동기부여 + 작업 알림 + 대기 중인 작업을 상기시키는 알림 + + 설정 + 작업 추가 + 여정을 시작하세요! + 첫 번째 작업과 목표를 추가하여 동기를 유지하세요 + 🎯 목표: + ⏸️ 일시 중지됨 + 작업 활성화/일시 중지 + 삭제 + 작업 삭제 + \'%1$s\'을(를) 정말 삭제하시겠습니까? + 삭제 + 취소 + + 새 작업 + 뒤로 + 📝 무엇을 기억해야 하나요? + 작업 제목 + 예: 운동하기 + 🎯 무엇을 달성하고 싶나요? + 새 목표 + 예: 건강 개선 + 목표 추가 + 추가된 목표: + 목표 삭제 + 🔔 일일 알림 + 오전 9시부터 오후 9시 사이의 알림 횟수 + 회 알림 + 회 알림 + 줄이기 + 늘리기 + ⏱️ 약 %1$s마다 알림 1회 + 📅 며칠마다 반복 + 각 알림 주기 사이의 일수 간격 + + + 일수 줄이기 + 일수 늘리기 + 🔁 매일 알림 + 🔁 %1$d일마다 알림, 겹치지 않게 분산 + 작업 저장 + 작업 편집 + 변경 사항 저장 + 작업 일시 중지 + 작업 재개 + 길게 눌러 편집 + %1$d회/일 · %2$d일마다 + %1$d회/일 · 매일 + + 설정 + 🔔 알림 + 일일 알림 + 동기 부여 알림 받기 + 소리 + 알림과 함께 소리 재생 + 🧪 테스트 + 모든 것이 올바르게 작동하는지 확인하기 위해 테스트 알림을 보냅니다 + 테스트 알림 보내기 + ⚠️ 알림을 테스트하려면 최소 하나의 작업을 추가하세요 + ℹ️ 앱 정보 + 동기부여 v1.0 + 일상 작업에서 동기를 유지하는 데 도움을 주는 동반자 + 개발자 + manalejandro.com + GitHub 저장소 + https://github.com/manalejandro/motivame + + 🌐 언어 + 애플리케이션 언어를 선택하세요 + 언어 적용을 위해 앱이 재시작됩니다 + + 이 작업을 완료하는 것을 잊지 마세요! + 📝 작업: %1$s\n\n🎯 알림: %2$s + + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml new file mode 100644 index 0000000..43e168f --- /dev/null +++ b/app/src/main/res/values-pt/strings.xml @@ -0,0 +1,80 @@ + + + Motiva-me + Lembretes de Tarefas + Notificações para lembrá-lo das suas tarefas pendentes + + Configurações + Adicionar tarefa + Comece sua jornada! + Adicione sua primeira tarefa e metas para se manter motivado + 🎯 Metas: + ⏸️ Pausada + Ativar/pausar tarefa + Excluir + Excluir tarefa + Tem certeza que deseja excluir \'%1$s\'? + Excluir + Cancelar + + Nova Tarefa + Voltar + 📝 O que você precisa lembrar? + Título da tarefa + Ex: Fazer exercício + 🎯 O que você espera alcançar? + Nova meta + Ex: Melhorar minha saúde + Adicionar meta + Metas adicionadas: + Excluir meta + 🔔 Lembretes diários + Número de lembretes entre 9h00 e 21h00 + lembrete + lembretes + Diminuir + Aumentar + ⏱️ Um lembrete a cada %1$s aprox. + 📅 De quantos em quantos dias + Intervalo de dias entre cada ciclo de lembretes + dia + dias + Diminuir dias + Aumentar dias + 🔁 Lembretes todos os dias + 🔁 Lembretes a cada %1$d dias, distribuídos para não coincidir + Salvar Tarefa + Editar Tarefa + Salvar Alterações + Pausar tarefa + Retomar tarefa + Pressione longo para editar + %1$d lembrete/dia · a cada %2$d dias + %1$d lembrete/dia · todos os dias + + Configurações + 🔔 Notificações + Lembretes diários + Receba notificações para se motivar + Som + Reproduzir som com as notificações + 🧪 Teste + Envie uma notificação de teste para verificar se tudo funciona corretamente + Enviar notificação de teste + ⚠️ Adicione pelo menos uma tarefa para testar as notificações + ℹ️ Sobre o app + Motiva-me v1.0 + Seu companheiro para manter a motivação nas suas tarefas diárias + Desenvolvido por + manalejandro.com + Repositório no GitHub + https://github.com/manalejandro/motivame + + 🌐 Idioma + Selecione o idioma do aplicativo + O app será reiniciado para aplicar o idioma + + Lembre-se de completar esta tarefa! + 📝 Tarefa: %1$s\n\n🎯 Lembre-se: %2$s + + diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml new file mode 100644 index 0000000..42ec491 --- /dev/null +++ b/app/src/main/res/values-zh/strings.xml @@ -0,0 +1,86 @@ + + + 激励我 + 任务提醒 + 提醒您完成待办任务的通知 + + + 设置 + 添加任务 + 开始你的旅程! + 添加你的第一个任务和目标,保持动力 + 🎯 目标: + ⏸️ 已暂停 + 切换任务状态 + 删除 + 删除任务 + 确定要删除 \'%1$s\' 吗? + 删除 + 取消 + + + 新任务 + 返回 + 📝 你需要记住什么? + 任务标题 + 例:锻炼身体 + 🎯 你希望达到什么目标? + 新目标 + 例:改善健康 + 添加目标 + 已添加目标: + 删除目标 + 🔔 每日提醒 + 9:00 至 21:00 之间的提醒次数 + 次提醒 + 次提醒 + 减少 + 增加 + ⏱️ 大约每 %1$s 一次提醒 + 📅 每隔几天 + 每个提醒周期之间的天数间隔 + + + 减少天数 + 增加天数 + 🔁 每天提醒 + 🔁 每 %1$d 天提醒,分散安排避免重叠 + 保存任务 + 编辑任务 + 保存更改 + 暂停任务 + 恢复任务 + 长按以编辑 + 每天 %1$d 次提醒 · 每 %2$d 天 + 每天 %1$d 次提醒 · 每天 + + + 设置 + 🔔 通知 + 每日提醒 + 接收通知以激励自己 + 声音 + 通知时播放声音 + 🧪 测试 + 发送测试通知以验证一切正常运行 + 发送测试通知 + ⚠️ 请至少添加一个任务以测试通知 + ℹ️ 关于应用 + 激励我 v1.0 + 帮助你在日常任务中保持动力的好伴侣 + 开发者 + manalejandro.com + GitHub 仓库 + https://github.com/manalejandro/motivame + + + 🌐 语言 + 选择应用程序语言 + 应用将重启以应用语言更改 + + + 记得完成这个任务! + 📝 任务:%1$s\n\n🎯 提醒:%2$s + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ca86b07..7f0083c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,4 +2,82 @@ Motívame Recordatorios de Tareas Notificaciones para recordarte tus tareas pendientes + + + Configuración + Agregar tarea + ¡Comienza tu viaje! + Agrega tu primera tarea y metas para mantenerte motivado + 🎯 Metas: + ⏸️ Pausada + Activar/pausar tarea + Eliminar + Eliminar tarea + ¿Estás seguro de que quieres eliminar \'%1$s\'? + Eliminar + Cancelar + + + Nueva Tarea + Volver + 📝 ¿Qué debes recordar? + Título de la tarea + Ej: Hacer ejercicio + 🎯 ¿Qué esperas alcanzar? + Nueva meta + Ej: Mejorar mi salud + Agregar meta + Metas agregadas: + Eliminar meta + 🔔 Avisos diarios + Número de recordatorios entre las 9:00 y las 21:00 + aviso + avisos + Reducir + Aumentar + ⏱️ Un aviso cada %1$s aprox. + 📅 Cada cuántos días + Intervalo de días entre cada ciclo de avisos + día + días + Reducir días + Aumentar días + 🔁 Avisos todos los días + 🔁 Avisos cada %1$d días, repartidos para no coincidir + Guardar Tarea + Editar Tarea + Guardar Cambios + Pausar tarea + Reanudar tarea + Mantén pulsado para editar + %1$d aviso/día · cada %2$d días + %1$d aviso/día · todos los días + + + Configuración + 🔔 Notificaciones + Recordatorios diarios + Recibe notificaciones para motivarte + Sonido + Reproducir sonido con las notificaciones + 🧪 Prueba + Envía una notificación de prueba para verificar que todo funciona correctamente + Enviar notificación de prueba + ⚠️ Agrega al menos una tarea para probar las notificaciones + ℹ️ Sobre la app + Motívame v1.0 + Tu compañero para mantener la motivación en tus tareas diarias + Desarrollado por + manalejandro.com + Repositorio en GitHub + https://github.com/manalejandro/motivame + + + 🌐 Idioma + Selecciona el idioma de la aplicación + La app se reiniciará para aplicar el idioma + + + ¡Recuerda completar esta tarea! + 📝 Tarea: %1$s\n\n🎯 Recuerda: %2$s \ No newline at end of file