initial commit

Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
ale
2026-02-19 04:27:22 +01:00
commit 1a50f6147f
Se han modificado 57 ficheros con 3902 adiciones y 0 borrados

1
app/.gitignore vendido Archivo normal
Ver fichero

@@ -0,0 +1 @@
/build

62
app/build.gradle.kts Archivo normal
Ver fichero

@@ -0,0 +1,62 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "com.manalejandro.motivame"
compileSdk {
version = release(36) {
minorApiLevel = 1
}
}
defaultConfig {
applicationId = "com.manalejandro.motivame"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.compose.material.icons.extended)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.androidx.compose.ui.test.manifest)
}

21
app/proguard-rules.pro vendido Archivo normal
Ver fichero

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

Ver fichero

@@ -0,0 +1,24 @@
package com.manalejandro.motivame
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.manalejandro.motivame", appContext.packageName)
}
}

Ver fichero

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Motivame">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Motivame">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 67 KiB

Ver fichero

@@ -0,0 +1,89 @@
package com.manalejandro.motivame
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.runtime.*
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.work.*
import com.manalejandro.motivame.ui.screens.AddTaskScreen
import com.manalejandro.motivame.ui.screens.MainScreen
import com.manalejandro.motivame.ui.screens.SettingsScreen
import com.manalejandro.motivame.ui.theme.MotivameTheme
import com.manalejandro.motivame.ui.viewmodel.TaskViewModel
import com.manalejandro.motivame.worker.DailyReminderWorker
import java.util.concurrent.TimeUnit
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
// Configurar recordatorios diarios
setupDailyReminders()
setContent {
MotivameTheme {
MotivameApp()
}
}
}
private fun setupDailyReminders() {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.NOT_REQUIRED)
.build()
val dailyWorkRequest = PeriodicWorkRequestBuilder<DailyReminderWorker>(
1, TimeUnit.DAYS
)
.setConstraints(constraints)
.setInitialDelay(calculateInitialDelay(), TimeUnit.MILLISECONDS)
.build()
WorkManager.getInstance(applicationContext).enqueueUniquePeriodicWork(
"daily_reminder",
ExistingPeriodicWorkPolicy.KEEP,
dailyWorkRequest
)
}
private fun calculateInitialDelay(): Long {
val currentTime = System.currentTimeMillis()
val calendar = java.util.Calendar.getInstance().apply {
timeInMillis = currentTime
set(java.util.Calendar.HOUR_OF_DAY, 9) // 9 AM
set(java.util.Calendar.MINUTE, 0)
set(java.util.Calendar.SECOND, 0)
}
if (calendar.timeInMillis <= currentTime) {
calendar.add(java.util.Calendar.DAY_OF_YEAR, 1)
}
return calendar.timeInMillis - currentTime
}
}
@Composable
fun MotivameApp() {
val viewModel: TaskViewModel = viewModel()
var currentScreen by remember { mutableStateOf("main") }
when (currentScreen) {
"main" -> MainScreen(
viewModel = viewModel,
onNavigateToAddTask = { currentScreen = "add_task" },
onNavigateToSettings = { currentScreen = "settings" }
)
"add_task" -> AddTaskScreen(
viewModel = viewModel,
onNavigateBack = { currentScreen = "main" }
)
"settings" -> SettingsScreen(
viewModel = viewModel,
onNavigateBack = { currentScreen = "main" }
)
}
}

Ver fichero

@@ -0,0 +1,12 @@
package com.manalejandro.motivame.data
import java.util.UUID
data class Task(
val id: String = UUID.randomUUID().toString(),
val title: String,
val goals: List<String>,
val isActive: Boolean = true,
val createdAt: Long = System.currentTimeMillis()
)

Ver fichero

@@ -0,0 +1,151 @@
package com.manalejandro.motivame.data
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import org.json.JSONArray
import org.json.JSONObject
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "motivame_prefs")
class TaskRepository(private val context: Context) {
companion object {
private val TASKS_KEY = stringPreferencesKey("tasks")
private val NOTIFICATION_ENABLED_KEY = stringPreferencesKey("notification_enabled")
private val SOUND_ENABLED_KEY = stringPreferencesKey("sound_enabled")
val DEFAULT_TASKS = listOf(
Task(
title = "Hacer ejercicio",
goals = listOf(
"Mejorar mi salud cardiovascular",
"Sentirme más energético",
"Alcanzar mi peso ideal"
)
),
Task(
title = "Estudiar inglés",
goals = listOf(
"Conseguir mejores oportunidades laborales",
"Viajar sin limitaciones",
"Expandir mi conocimiento"
)
),
Task(
title = "Leer 30 minutos",
goals = listOf(
"Desarrollar el hábito de la lectura",
"Aprender cosas nuevas",
"Reducir el tiempo en redes sociales"
)
)
)
}
val tasks: Flow<List<Task>> = context.dataStore.data
.map { preferences ->
val tasksJson = preferences[TASKS_KEY]
if (tasksJson.isNullOrEmpty()) {
DEFAULT_TASKS
} else {
parseTasksFromJson(tasksJson)
}
}
val notificationEnabled: Flow<Boolean> = context.dataStore.data
.map { preferences ->
preferences[NOTIFICATION_ENABLED_KEY]?.toBoolean() ?: true
}
val soundEnabled: Flow<Boolean> = context.dataStore.data
.map { preferences ->
preferences[SOUND_ENABLED_KEY]?.toBoolean() ?: true
}
suspend fun saveTasks(tasks: List<Task>) {
context.dataStore.edit { preferences ->
preferences[TASKS_KEY] = tasksToJson(tasks)
}
}
suspend fun addTask(task: Task) {
context.dataStore.edit { preferences ->
val currentTasks = parseTasksFromJson(preferences[TASKS_KEY] ?: "")
val updatedTasks = currentTasks + task
preferences[TASKS_KEY] = tasksToJson(updatedTasks)
}
}
suspend fun updateTask(task: Task) {
context.dataStore.edit { preferences ->
val currentTasks = parseTasksFromJson(preferences[TASKS_KEY] ?: "")
val updatedTasks = currentTasks.map { if (it.id == task.id) task else it }
preferences[TASKS_KEY] = tasksToJson(updatedTasks)
}
}
suspend fun deleteTask(taskId: String) {
context.dataStore.edit { preferences ->
val currentTasks = parseTasksFromJson(preferences[TASKS_KEY] ?: "")
val updatedTasks = currentTasks.filter { it.id != taskId }
preferences[TASKS_KEY] = tasksToJson(updatedTasks)
}
}
suspend fun setNotificationEnabled(enabled: Boolean) {
context.dataStore.edit { preferences ->
preferences[NOTIFICATION_ENABLED_KEY] = enabled.toString()
}
}
suspend fun setSoundEnabled(enabled: Boolean) {
context.dataStore.edit { preferences ->
preferences[SOUND_ENABLED_KEY] = enabled.toString()
}
}
private fun tasksToJson(tasks: List<Task>): String {
val jsonArray = JSONArray()
tasks.forEach { task ->
val jsonObject = JSONObject().apply {
put("id", task.id)
put("title", task.title)
put("goals", JSONArray(task.goals))
put("isActive", task.isActive)
put("createdAt", task.createdAt)
}
jsonArray.put(jsonObject)
}
return jsonArray.toString()
}
private fun parseTasksFromJson(json: String): List<Task> {
if (json.isEmpty()) return emptyList()
return try {
val jsonArray = JSONArray(json)
List(jsonArray.length()) { index ->
val jsonObject = jsonArray.getJSONObject(index)
val goalsArray = jsonObject.getJSONArray("goals")
val goals = List(goalsArray.length()) { goalsArray.getString(it) }
Task(
id = jsonObject.getString("id"),
title = jsonObject.getString("title"),
goals = goals,
isActive = jsonObject.getBoolean("isActive"),
createdAt = jsonObject.getLong("createdAt")
)
}
} catch (e: Exception) {
emptyList()
}
}
}

Ver fichero

@@ -0,0 +1,93 @@
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.RingtoneManager
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.manalejandro.motivame.MainActivity
import com.manalejandro.motivame.R
import com.manalejandro.motivame.data.Task
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)
}
}
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,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
val motivationalMessage = if (task.goals.isNotEmpty()) {
task.goals.random()
} else {
"¡Recuerda completar esta tarea!"
}
val notificationBuilder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle("${task.title}")
.setContentText(motivationalMessage)
.setStyle(NotificationCompat.BigTextStyle().bigText(
"📝 Tarea: ${task.title}\n\n🎯 Recuerda: $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)
}
try {
val notificationManager = NotificationManagerCompat.from(context)
notificationManager.notify(Random.nextInt(), notificationBuilder.build())
} catch (e: SecurityException) {
// El usuario no ha concedido permisos de notificación
}
}
fun sendMotivationalReminder(tasks: List<Task>, withSound: Boolean = true) {
if (tasks.isEmpty()) return
val activeTask = tasks.firstOrNull { it.isActive } ?: return
sendTaskReminder(activeTask, withSound)
}
}

Ver fichero

@@ -0,0 +1,219 @@
package com.manalejandro.motivame.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.manalejandro.motivame.data.Task
import com.manalejandro.motivame.ui.viewmodel.TaskViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AddTaskScreen(
viewModel: TaskViewModel,
onNavigateBack: () -> Unit
) {
var taskTitle by remember { mutableStateOf("") }
var currentGoal by remember { mutableStateOf("") }
var goals by remember { mutableStateOf(listOf<String>()) }
Scaffold(
topBar = {
TopAppBar(
title = { Text("Nueva Tarea") },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Volver")
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
)
}
) { paddingValues ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "📝 ¿Qué debes recordar?",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = taskTitle,
onValueChange = { taskTitle = it },
label = { Text("Título de la tarea") },
placeholder = { Text("Ej: Hacer ejercicio") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
leadingIcon = {
Icon(Icons.Default.Star, contentDescription = null)
}
)
}
}
}
item {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "🎯 ¿Qué esperas alcanzar?",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = currentGoal,
onValueChange = { currentGoal = it },
label = { Text("Nueva meta") },
placeholder = { Text("Ej: Mejorar mi salud") },
modifier = Modifier.fillMaxWidth(),
trailingIcon = {
IconButton(
onClick = {
if (currentGoal.isNotBlank()) {
goals = goals + currentGoal.trim()
currentGoal = ""
}
},
enabled = currentGoal.isNotBlank()
) {
Icon(Icons.Default.Add, contentDescription = "Agregar meta")
}
}
)
}
}
}
if (goals.isNotEmpty()) {
item {
Text(
text = "Metas agregadas:",
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
itemsIndexed(goals) { index, goal ->
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = goal,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer
)
}
IconButton(
onClick = {
goals = goals.filterIndexed { i, _ -> i != index }
}
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Eliminar meta",
tint = MaterialTheme.colorScheme.error
)
}
}
}
}
}
item {
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = {
if (taskTitle.isNotBlank()) {
val newTask = Task(
title = taskTitle.trim(),
goals = goals,
isActive = true
)
viewModel.addTask(newTask)
onNavigateBack()
}
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
enabled = taskTitle.isNotBlank(),
shape = RoundedCornerShape(12.dp)
) {
Icon(Icons.Default.Check, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Guardar Tarea",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
}
}
}
}

Ver fichero

@@ -0,0 +1,252 @@
package com.manalejandro.motivame.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
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.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.manalejandro.motivame.data.Task
import com.manalejandro.motivame.ui.viewmodel.TaskViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainScreen(
viewModel: TaskViewModel,
onNavigateToAddTask: () -> Unit,
onNavigateToSettings: () -> Unit
) {
val tasks by viewModel.tasks.collectAsState()
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
"Motívame",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
},
actions = {
IconButton(onClick = onNavigateToSettings) {
Icon(Icons.Default.Settings, contentDescription = "Configuración")
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
)
},
floatingActionButton = {
FloatingActionButton(
onClick = onNavigateToAddTask,
containerColor = MaterialTheme.colorScheme.primary
) {
Icon(Icons.Default.Add, contentDescription = "Agregar tarea")
}
}
) { paddingValues ->
if (tasks.isEmpty()) {
EmptyState(modifier = Modifier.padding(paddingValues))
} else {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(tasks, key = { it.id }) { task ->
TaskCard(
task = task,
onToggleActive = { viewModel.updateTask(task.copy(isActive = !task.isActive)) },
onDelete = { viewModel.deleteTask(task.id) }
)
}
}
}
}
}
@Composable
fun TaskCard(
task: Task,
onToggleActive: () -> Unit,
onDelete: () -> Unit
) {
var showDeleteDialog by remember { mutableStateOf(false) }
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.verticalGradient(
colors = listOf(
MaterialTheme.colorScheme.surfaceVariant,
MaterialTheme.colorScheme.surface
)
)
)
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(1f)
) {
Icon(
imageVector = Icons.Default.Star,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
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
)
}
Row {
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
)
}
IconButton(onClick = { showDeleteDialog = true }) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Eliminar",
tint = MaterialTheme.colorScheme.error
)
}
}
}
if (task.goals.isNotEmpty()) {
Spacer(modifier = Modifier.height(12.dp))
Text(
text = "🎯 Metas:",
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(8.dp))
task.goals.forEach { goal ->
Row(
modifier = Modifier.padding(vertical = 4.dp),
verticalAlignment = Alignment.Top
) {
Text(
text = "",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(end = 8.dp)
)
Text(
text = goal,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
if (!task.isActive) {
Spacer(modifier = Modifier.height(8.dp))
Box(
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.background(MaterialTheme.colorScheme.errorContainer)
.padding(horizontal = 12.dp, vertical = 6.dp)
) {
Text(
text = "⏸️ Pausada",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onErrorContainer
)
}
}
}
}
if (showDeleteDialog) {
AlertDialog(
onDismissRequest = { showDeleteDialog = false },
title = { Text("Eliminar tarea") },
text = { Text("¿Estás seguro de que quieres eliminar '${task.title}'?") },
confirmButton = {
TextButton(
onClick = {
onDelete()
showDeleteDialog = false
}
) {
Text("Eliminar", color = MaterialTheme.colorScheme.error)
}
},
dismissButton = {
TextButton(onClick = { showDeleteDialog = false }) {
Text("Cancelar")
}
}
)
}
}
@Composable
fun EmptyState(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Default.Star,
contentDescription = null,
modifier = Modifier.size(100.dp),
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f)
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "¡Comienza tu viaje!",
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",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}

Ver fichero

@@ -0,0 +1,251 @@
package com.manalejandro.motivame.ui.screens
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import com.manalejandro.motivame.data.TaskRepository
import com.manalejandro.motivame.notifications.NotificationHelper
import com.manalejandro.motivame.ui.viewmodel.TaskViewModel
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
viewModel: TaskViewModel,
onNavigateBack: () -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val notificationEnabled by viewModel.notificationEnabled.collectAsState()
val soundEnabled by viewModel.soundEnabled.collectAsState()
val tasks by viewModel.tasks.collectAsState()
var hasNotificationPermission by remember {
mutableStateOf(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ContextCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
} else {
true
}
)
}
val permissionLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.RequestPermission()
) { isGranted ->
hasNotificationPermission = isGranted
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Configuración") },
navigationIcon = {
IconButton(onClick = onNavigateBack) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Volver")
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
titleContentColor = MaterialTheme.colorScheme.onPrimaryContainer
)
)
}
) { paddingValues ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "🔔 Notificaciones",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Recordatorios diarios",
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium
)
Text(
text = "Recibe notificaciones para motivarte",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Switch(
checked = notificationEnabled && hasNotificationPermission,
onCheckedChange = { enabled ->
if (enabled && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !hasNotificationPermission) {
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
} else {
viewModel.toggleNotifications(enabled)
}
}
)
}
Spacer(modifier = Modifier.height(12.dp))
HorizontalDivider()
Spacer(modifier = Modifier.height(12.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Sonido",
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium
)
Text(
text = "Reproducir sonido con las notificaciones",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Switch(
checked = soundEnabled,
onCheckedChange = { viewModel.toggleSound(it) }
)
}
}
}
}
item {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = "🧪 Prueba",
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",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(12.dp))
Button(
onClick = {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !hasNotificationPermission) {
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
} else {
scope.launch {
val notificationHelper = NotificationHelper(context)
notificationHelper.sendMotivationalReminder(tasks, soundEnabled)
}
}
},
modifier = Modifier.fillMaxWidth(),
enabled = tasks.isNotEmpty()
) {
Icon(Icons.Default.Notifications, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text("Enviar notificación de prueba")
}
if (tasks.isEmpty()) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "⚠️ Agrega al menos una tarea para probar las notificaciones",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error
)
}
}
}
}
item {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.tertiaryContainer
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = " Sobre la app",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Motívame v1.0",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onTertiaryContainer
)
Text(
text = "Tu compañero para mantener la motivación en tus tareas diarias",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onTertiaryContainer.copy(alpha = 0.7f)
)
}
}
}
}
}
}

Ver fichero

@@ -0,0 +1,28 @@
package com.manalejandro.motivame.ui.theme
import androidx.compose.ui.graphics.Color
// Colores principales - tonos vibrantes y motivadores
val Primary = Color(0xFF6366F1) // Indigo vibrante
val PrimaryDark = Color(0xFF4F46E5)
val PrimaryLight = Color(0xFFA5B4FC)
val Secondary = Color(0xFFEC4899) // Rosa motivador
val SecondaryDark = Color(0xFFDB2777)
val SecondaryLight = Color(0xFFF9A8D4)
val Tertiary = Color(0xFF8B5CF6) // Púrpura
val Background = Color(0xFFFAFAFA)
val Surface = Color(0xFFFFFFFF)
// Colores para modo oscuro
val PrimaryDarkMode = Color(0xFF818CF8)
val SecondaryDarkMode = Color(0xFFF472B6)
val TertiaryDarkMode = Color(0xFFA78BFA)
val BackgroundDark = Color(0xFF121212)
val SurfaceDark = Color(0xFF1E1E1E)
// Colores de acento
val Success = Color(0xFF10B981)
val Warning = Color(0xFFF59E0B)
val Error = Color(0xFFEF4444)

Ver fichero

@@ -0,0 +1,81 @@
package com.manalejandro.motivame.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = PrimaryDarkMode,
secondary = SecondaryDarkMode,
tertiary = TertiaryDarkMode,
background = BackgroundDark,
surface = SurfaceDark,
error = Error,
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFFE0E0E0),
onSurface = Color(0xFFE0E0E0)
)
private val LightColorScheme = lightColorScheme(
primary = Primary,
secondary = Secondary,
tertiary = Tertiary,
background = Background,
surface = Surface,
error = Error,
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1A1A1A),
onSurface = Color(0xFF1A1A1A),
primaryContainer = PrimaryLight,
secondaryContainer = SecondaryLight,
onPrimaryContainer = Color(0xFF1E1B4B),
onSecondaryContainer = Color(0xFF831843)
)
@Composable
fun MotivameTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = false, // Desactivado para usar nuestro tema personalizado
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

Ver fichero

@@ -0,0 +1,34 @@
package com.manalejandro.motivame.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

Ver fichero

@@ -0,0 +1,84 @@
package com.manalejandro.motivame.ui.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.manalejandro.motivame.data.Task
import com.manalejandro.motivame.data.TaskRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class TaskViewModel(application: Application) : AndroidViewModel(application) {
private val repository = TaskRepository(application)
private val _tasks = MutableStateFlow<List<Task>>(emptyList())
val tasks: StateFlow<List<Task>> = _tasks.asStateFlow()
private val _notificationEnabled = MutableStateFlow(true)
val notificationEnabled: StateFlow<Boolean> = _notificationEnabled.asStateFlow()
private val _soundEnabled = MutableStateFlow(true)
val soundEnabled: StateFlow<Boolean> = _soundEnabled.asStateFlow()
init {
loadTasks()
loadSettings()
}
private fun loadTasks() {
viewModelScope.launch {
repository.tasks.collect { taskList ->
_tasks.value = taskList
}
}
}
private fun loadSettings() {
viewModelScope.launch {
repository.notificationEnabled.collect { enabled ->
_notificationEnabled.value = enabled
}
}
viewModelScope.launch {
repository.soundEnabled.collect { enabled ->
_soundEnabled.value = enabled
}
}
}
fun addTask(task: Task) {
viewModelScope.launch {
repository.addTask(task)
}
}
fun updateTask(task: Task) {
viewModelScope.launch {
repository.updateTask(task)
}
}
fun deleteTask(taskId: String) {
viewModelScope.launch {
repository.deleteTask(taskId)
}
}
fun toggleNotifications(enabled: Boolean) {
viewModelScope.launch {
repository.setNotificationEnabled(enabled)
_notificationEnabled.value = enabled
}
}
fun toggleSound(enabled: Boolean) {
viewModelScope.launch {
repository.setSoundEnabled(enabled)
_soundEnabled.value = enabled
}
}
}

Ver fichero

@@ -0,0 +1,30 @@
package com.manalejandro.motivame.worker
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.manalejandro.motivame.data.TaskRepository
import com.manalejandro.motivame.notifications.NotificationHelper
import kotlinx.coroutines.flow.first
class DailyReminderWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val repository = TaskRepository(applicationContext)
val notificationHelper = NotificationHelper(applicationContext)
val tasks = repository.tasks.first()
val notificationEnabled = repository.notificationEnabled.first()
val soundEnabled = repository.soundEnabled.first()
if (notificationEnabled && tasks.isNotEmpty()) {
notificationHelper.sendMotivationalReminder(tasks, soundEnabled)
}
return Result.success()
}
}

Ver fichero

@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

Ver fichero

@@ -0,0 +1,63 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="512"
android:viewportHeight="512">
<group android:scaleX="0.8"
android:scaleY="0.8"
android:translateX="51.2"
android:translateY="51.2">
<path
android:pathData="M144,32L368,32A112,112 0,0 1,480 144L480,368A112,112 0,0 1,368 480L144,480A112,112 0,0 1,32 368L32,144A112,112 0,0 1,144 32z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="32"
android:startY="32"
android:endX="480"
android:endY="480"
android:type="linear">
<item android:offset="0" android:color="#FF6366F1"/>
<item android:offset="1" android:color="#FFEC4899"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M256,256m-160,0a160,160 0,1 1,320 0a160,160 0,1 1,-320 0"
android:strokeWidth="16"
android:fillColor="#00000000">
<aapt:attr name="android:strokeColor">
<gradient
android:startX="96"
android:startY="96"
android:endX="416"
android:endY="416"
android:type="linear">
<item android:offset="0" android:color="#F2FFFFFF"/>
<item android:offset="1" android:color="#CCFFFFFF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M256,142L286,220L368,220L302,268L326,346L256,298L186,346L210,268L144,220L226,220Z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="144"
android:startY="142"
android:endX="368"
android:endY="346"
android:type="linear">
<item android:offset="0" android:color="#F2FFFFFF"/>
<item android:offset="1" android:color="#CCFFFFFF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M210,270L242,302L312,230"
android:strokeLineJoin="round"
android:strokeWidth="18"
android:fillColor="#00000000"
android:strokeColor="#10B981"
android:strokeLineCap="round"/>
</group>
</vector>

Ver fichero

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Ver fichero

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 3.6 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 5.5 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 2.3 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 3.5 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 4.8 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 7.6 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 7.4 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 12 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 9.8 KiB

Archivo binario no mostrado.

Después

Anchura:  |  Altura:  |  Tamaño: 16 KiB

Ver fichero

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

Ver fichero

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#7B62E2</color>
</resources>

Ver fichero

@@ -0,0 +1,5 @@
<resources>
<string name="app_name">Motívame</string>
<string name="notification_channel_name">Recordatorios de Tareas</string>
<string name="notification_channel_description">Notificaciones para recordarte tus tareas pendientes</string>
</resources>

Ver fichero

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Motivame" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

Ver fichero

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>

Ver fichero

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>

Ver fichero

@@ -0,0 +1,17 @@
package com.manalejandro.motivame
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}