fix gitignore

This commit is contained in:
2026-02-26 21:32:08 -05:00
parent a6a76c90b9
commit 06873ca27b
83 changed files with 2675 additions and 2 deletions
+61
View File
@@ -0,0 +1,61 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ksp)
}
android {
namespace = "com.example.todokotlin"
compileSdk = 35
defaultConfig {
applicationId = "com.example.todokotlin"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "1.0"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(libs.androidx.core.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)
debugImplementation(libs.androidx.compose.ui.tooling)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.room.runtime)
implementation(libs.room.ktx)
ksp(libs.room.compiler)
}
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:label="Todo Kotlin"
android:supportsRtl="true"
android:theme="@style/Theme.TodoKotlin">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.TodoKotlin">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,22 @@
package com.example.todokotlin
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.todokotlin.ui.TodoApp
import com.example.todokotlin.ui.theme.TodoKotlinTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
TodoKotlinTheme {
val viewModel: TodoViewModel = viewModel()
TodoApp(viewModel = viewModel)
}
}
}
}
@@ -0,0 +1,84 @@
package com.example.todokotlin
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.example.todokotlin.data.Todo
import com.example.todokotlin.data.TodoDatabase
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
enum class TodoFilter { All, Active, Done }
data class TodoUiState(
val todos: List<Todo> = emptyList(),
val filter: TodoFilter = TodoFilter.All,
val allCount: Int = 0,
val activeCount: Int = 0,
val doneCount: Int = 0
)
class TodoViewModel(application: Application) : AndroidViewModel(application) {
private val dao = TodoDatabase.getDatabase(application).todoDao()
private val _filter = MutableStateFlow(TodoFilter.All)
val uiState: StateFlow<TodoUiState> = combine(
dao.getAllTodos(),
_filter
) { todos, filter ->
val filtered = when (filter) {
TodoFilter.All -> todos
TodoFilter.Active -> todos.filter { !it.completed }
TodoFilter.Done -> todos.filter { it.completed }
}
TodoUiState(
todos = filtered,
filter = filter,
allCount = todos.size,
activeCount = todos.count { !it.completed },
doneCount = todos.count { it.completed }
)
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = TodoUiState()
)
fun setFilter(filter: TodoFilter) {
_filter.value = filter
}
fun addTodo(title: String, category: String, priority: String) {
viewModelScope.launch {
dao.insert(
Todo(
title = title,
category = category,
priority = priority
)
)
}
}
fun updateTodo(todo: Todo) {
viewModelScope.launch {
dao.update(todo)
}
}
fun toggleCompleted(todo: Todo) {
viewModelScope.launch {
dao.update(todo.copy(completed = !todo.completed))
}
}
fun deleteTodo(todo: Todo) {
viewModelScope.launch {
dao.delete(todo)
}
}
}
@@ -0,0 +1,15 @@
package com.example.todokotlin.data
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "todos")
data class Todo(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val title: String,
val completed: Boolean = false,
val category: String = "Personal",
val priority: String = "Medium",
val createdAt: Long = System.currentTimeMillis()
)
@@ -0,0 +1,26 @@
package com.example.todokotlin.data
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
@Dao
interface TodoDao {
@Query("SELECT * FROM todos ORDER BY createdAt DESC")
fun getAllTodos(): Flow<List<Todo>>
@Insert
suspend fun insert(todo: Todo)
@Update
suspend fun update(todo: Todo)
@Delete
suspend fun delete(todo: Todo)
@Query("DELETE FROM todos WHERE id = :id")
suspend fun deleteById(id: Long)
}
@@ -0,0 +1,28 @@
package com.example.todokotlin.data
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [Todo::class], version = 1, exportSchema = false)
abstract class TodoDatabase : RoomDatabase() {
abstract fun todoDao(): TodoDao
companion object {
@Volatile
private var INSTANCE: TodoDatabase? = null
fun getDatabase(context: Context): TodoDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
TodoDatabase::class.java,
"todo_database"
).build()
INSTANCE = instance
instance
}
}
}
}
@@ -0,0 +1,42 @@
package com.example.todokotlin.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
val categories = listOf("Personal", "Work", "Shopping", "Health", "Learning")
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun CategoryPicker(
selected: String,
onSelect: (String) -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier = modifier.fillMaxWidth()) {
Text(
text = "Category",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
categories.forEach { category ->
FilterChip(
selected = selected == category,
onClick = { onSelect(category) },
label = { Text(category) }
)
}
}
}
}
@@ -0,0 +1,39 @@
package com.example.todokotlin.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@Composable
fun EmptyState(modifier: Modifier = Modifier) {
Column(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "No todos yet",
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Tap the + button to add your first todo",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
textAlign = TextAlign.Center
)
}
}
@@ -0,0 +1,45 @@
package com.example.todokotlin.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.todokotlin.TodoFilter
@Composable
fun FilterBar(
currentFilter: TodoFilter,
allCount: Int,
activeCount: Int,
doneCount: Int,
onFilterChange: (TodoFilter) -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
FilterChip(
selected = currentFilter == TodoFilter.All,
onClick = { onFilterChange(TodoFilter.All) },
label = { Text("All ($allCount)") }
)
FilterChip(
selected = currentFilter == TodoFilter.Active,
onClick = { onFilterChange(TodoFilter.Active) },
label = { Text("Active ($activeCount)") }
)
FilterChip(
selected = currentFilter == TodoFilter.Done,
onClick = { onFilterChange(TodoFilter.Done) },
label = { Text("Done ($doneCount)") }
)
}
}
@@ -0,0 +1,56 @@
package com.example.todokotlin.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
data class PriorityOption(val name: String, val color: Color)
val priorities = listOf(
PriorityOption("Low", Color(0xFF43A047)),
PriorityOption("Medium", Color(0xFFFB8C00)),
PriorityOption("High", Color(0xFFE53935))
)
fun priorityColor(priority: String): Color {
return priorities.find { it.name == priority }?.color ?: Color(0xFFFB8C00)
}
@Composable
fun PriorityPicker(
selected: String,
onSelect: (String) -> Unit,
modifier: Modifier = Modifier
) {
Column(modifier = modifier.fillMaxWidth()) {
Text(
text = "Priority",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
priorities.forEach { priority ->
FilterChip(
selected = selected == priority.name,
onClick = { onSelect(priority.name) },
label = { Text(priority.name) },
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = priority.color.copy(alpha = 0.2f),
selectedLabelColor = priority.color
)
)
}
}
}
}
@@ -0,0 +1,106 @@
package com.example.todokotlin.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.example.todokotlin.TodoViewModel
import com.example.todokotlin.data.Todo
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TodoApp(viewModel: TodoViewModel) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
var showAddDialog by remember { mutableStateOf(false) }
var editingTodo by remember { mutableStateOf<Todo?>(null) }
Scaffold(
topBar = {
TopAppBar(
title = {
Text("Todos \u2013 Kotlin Android")
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primary,
titleContentColor = MaterialTheme.colorScheme.onPrimary
)
)
},
floatingActionButton = {
FloatingActionButton(
onClick = { showAddDialog = true },
containerColor = MaterialTheme.colorScheme.primary
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add Todo",
tint = MaterialTheme.colorScheme.onPrimary
)
}
}
) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding)) {
FilterBar(
currentFilter = uiState.filter,
allCount = uiState.allCount,
activeCount = uiState.activeCount,
doneCount = uiState.doneCount,
onFilterChange = { viewModel.setFilter(it) }
)
if (uiState.todos.isEmpty()) {
EmptyState()
} else {
TodoList(
todos = uiState.todos,
onToggle = { viewModel.toggleCompleted(it) },
onTap = { editingTodo = it },
onDelete = { viewModel.deleteTodo(it) }
)
}
}
}
if (showAddDialog) {
TodoFormDialog(
onDismiss = { showAddDialog = false },
onSave = { title, category, priority ->
viewModel.addTodo(title, category, priority)
showAddDialog = false
}
)
}
editingTodo?.let { todo ->
TodoFormDialog(
todo = todo,
onDismiss = { editingTodo = null },
onSave = { title, category, priority ->
viewModel.updateTodo(
todo.copy(
title = title,
category = category,
priority = priority
)
)
editingTodo = null
}
)
}
}
@@ -0,0 +1,75 @@
package com.example.todokotlin.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.todokotlin.data.Todo
@Composable
fun TodoFormDialog(
todo: Todo? = null,
onDismiss: () -> Unit,
onSave: (title: String, category: String, priority: String) -> Unit
) {
var title by remember { mutableStateOf(todo?.title ?: "") }
var category by remember { mutableStateOf(todo?.category ?: "Personal") }
var priority by remember { mutableStateOf(todo?.priority ?: "Medium") }
val isEditing = todo != null
val dialogTitle = if (isEditing) "Edit Todo" else "Add Todo"
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(dialogTitle) },
text = {
Column {
OutlinedTextField(
value = title,
onValueChange = { title = it },
label = { Text("Title") },
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
CategoryPicker(
selected = category,
onSelect = { category = it }
)
Spacer(modifier = Modifier.height(12.dp))
PriorityPicker(
selected = priority,
onSelect = { priority = it }
)
}
},
confirmButton = {
TextButton(
onClick = {
if (title.isNotBlank()) {
onSave(title.trim(), category, priority)
}
},
enabled = title.isNotBlank()
) {
Text("Save")
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Cancel")
}
}
)
}
@@ -0,0 +1,102 @@
package com.example.todokotlin.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.example.todokotlin.data.Todo
@Composable
fun TodoItem(
todo: Todo,
onToggle: () -> Unit,
onTap: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier
.fillMaxWidth()
.clickable { onTap() },
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = todo.completed,
onCheckedChange = { onToggle() }
)
Spacer(modifier = Modifier.width(8.dp))
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = todo.title,
style = MaterialTheme.typography.bodyLarge,
textDecoration = if (todo.completed) TextDecoration.LineThrough else TextDecoration.None,
color = if (todo.completed)
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
else
MaterialTheme.colorScheme.onSurface,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
SuggestionChip(
onClick = { },
label = {
Text(
text = todo.category,
style = MaterialTheme.typography.labelSmall
)
}
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
Box(
modifier = Modifier
.size(10.dp)
.clip(CircleShape)
.background(priorityColor(todo.priority))
)
Text(
text = todo.priority,
style = MaterialTheme.typography.labelSmall,
color = priorityColor(todo.priority)
)
}
}
}
}
}
}
@@ -0,0 +1,85 @@
package com.example.todokotlin.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SwipeToDismissBox
import androidx.compose.material3.SwipeToDismissBoxValue
import androidx.compose.material3.rememberSwipeToDismissBoxState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.example.todokotlin.data.Todo
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TodoList(
todos: List<Todo>,
onToggle: (Todo) -> Unit,
onTap: (Todo) -> Unit,
onDelete: (Todo) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(
items = todos,
key = { it.id }
) { todo ->
val dismissState = rememberSwipeToDismissBoxState(
confirmValueChange = { value ->
if (value == SwipeToDismissBoxValue.EndToStart) {
onDelete(todo)
true
} else {
false
}
}
)
SwipeToDismissBox(
state = dismissState,
backgroundContent = {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0xFFE53935))
.padding(horizontal = 20.dp),
contentAlignment = Alignment.CenterEnd
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete",
tint = Color.White
)
}
},
enableDismissFromStartToEnd = false,
enableDismissFromEndToStart = true
) {
TodoItem(
todo = todo,
onToggle = { onToggle(todo) },
onTap = { onTap(todo) }
)
}
}
}
}
@@ -0,0 +1,29 @@
package com.example.todokotlin.ui.theme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
private val LightColorScheme = lightColorScheme(
primary = Color(0xFF6200EE),
onPrimary = Color.White,
primaryContainer = Color(0xFFE8DEF8),
onPrimaryContainer = Color(0xFF21005D),
secondary = Color(0xFF03DAC6),
onSecondary = Color.Black,
background = Color(0xFFFFFBFE),
onBackground = Color(0xFF1C1B1F),
surface = Color(0xFFFFFBFE),
onSurface = Color(0xFF1C1B1F),
surfaceVariant = Color(0xFFE7E0EC),
onSurfaceVariant = Color(0xFF49454F)
)
@Composable
fun TodoKotlinTheme(content: @Composable () -> Unit) {
MaterialTheme(
colorScheme = LightColorScheme,
content = content
)
}
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.TodoKotlin" parent="android:Theme.Material.Light.NoActionBar" />
</resources>