Renamed old app into Donext
Added DonextV2 module with Compose and Room Set up clean architecture Add DI with Hilt Setup initial database Display task lists on main activity
1
donextv2/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
71
donextv2/build.gradle.kts
Normal file
@@ -0,0 +1,71 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("org.jetbrains.kotlin.plugin.compose")
|
||||
id("com.google.devtools.ksp")
|
||||
id("com.google.dagger.hilt.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.wismna.geoffroy.donext"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.wismna.geoffroy.donext"
|
||||
minSdk = 26
|
||||
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
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
viewBinding = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.1.1"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.3")
|
||||
implementation("androidx.activity:activity-compose:1.10.1")
|
||||
implementation(platform("androidx.compose:compose-bom:2025.08.01"))
|
||||
implementation("androidx.compose.ui:ui")
|
||||
implementation("androidx.compose.ui:ui-graphics")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||
implementation("androidx.compose.material3:material3")
|
||||
implementation("androidx.navigation:navigation-compose:2.9.4")
|
||||
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
|
||||
androidTestImplementation(platform("androidx.compose:compose-bom:2025.08.01"))
|
||||
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
||||
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
||||
|
||||
val roomVersion = "2.8.0"
|
||||
implementation("androidx.room:room-runtime:$roomVersion")
|
||||
ksp("androidx.room:room-compiler:$roomVersion")
|
||||
|
||||
val hiltVersion = "2.57.1"
|
||||
implementation("com.google.dagger:hilt-android:$hiltVersion")
|
||||
ksp("com.google.dagger:hilt-android-compiler:$hiltVersion")
|
||||
}
|
||||
|
21
donextv2/proguard-rules.pro
vendored
Normal file
@@ -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
|
@@ -0,0 +1,24 @@
|
||||
package com.wismna.geoffroy.donext
|
||||
|
||||
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.wismna.geoffroy.donext", appContext.packageName)
|
||||
}
|
||||
}
|
18
donextv2/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:name=".DonextApplication">
|
||||
<activity
|
||||
android:name="com.wismna.geoffroy.donext.presentation.MainActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/title_activity_main"
|
||||
android:theme="@style/Theme.DoNext">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
@@ -0,0 +1,8 @@
|
||||
package com.wismna.geoffroy.donext
|
||||
|
||||
import android.app.Application
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
@HiltAndroidApp
|
||||
class DonextApplication: Application() {
|
||||
}
|
@@ -0,0 +1,17 @@
|
||||
package com.wismna.geoffroy.donext.data
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import java.time.Instant
|
||||
|
||||
|
||||
class Converters {
|
||||
@TypeConverter
|
||||
fun fromTimestamp(value: Long?): Instant? {
|
||||
return value?.let { Instant.ofEpochMilli(it) }
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun instantToTimestamp(instant: Instant?): Long? {
|
||||
return instant?.toEpochMilli()
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
package com.wismna.geoffroy.donext.data
|
||||
|
||||
import com.wismna.geoffroy.donext.data.entities.TaskEntity
|
||||
import com.wismna.geoffroy.donext.data.entities.TaskListEntity
|
||||
import com.wismna.geoffroy.donext.domain.model.Task
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import java.time.Instant
|
||||
|
||||
fun TaskEntity.toDomain() = Task(
|
||||
id = id,
|
||||
name = name,
|
||||
isDone = isDone,
|
||||
taskListId = taskListId,
|
||||
description = description,
|
||||
cycles = cycles,
|
||||
isDeleted = isDeleted,
|
||||
updateDate = Instant.ofEpochMilli(updateDate)
|
||||
)
|
||||
|
||||
fun Task.toEntity() = TaskEntity(
|
||||
id = id,
|
||||
name = name,
|
||||
isDone = isDone,
|
||||
taskListId = taskListId,
|
||||
description = description,
|
||||
cycles = cycles,
|
||||
isDeleted = isDeleted,
|
||||
updateDate = updateDate.toEpochMilli()
|
||||
)
|
||||
|
||||
fun TaskListEntity.toDomain() = TaskList(
|
||||
id = id,
|
||||
name = name,
|
||||
isDeleted = isDeleted
|
||||
)
|
||||
|
||||
fun TaskList.toEntity() = TaskListEntity(
|
||||
id = id,
|
||||
name = name,
|
||||
isDeleted = isDeleted
|
||||
)
|
@@ -0,0 +1,22 @@
|
||||
package com.wismna.geoffroy.donext.data.entities
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "tasks")
|
||||
data class TaskEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Long = 0,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val cycles: Int = 0,
|
||||
@ColumnInfo(name = "done")
|
||||
val isDone: Boolean = false,
|
||||
@ColumnInfo(name = "deleted")
|
||||
val isDeleted: Boolean = false,
|
||||
@ColumnInfo(name = "task_list_id")
|
||||
val taskListId: Long,
|
||||
@ColumnInfo(name = "update_date")
|
||||
val updateDate: Long = System.currentTimeMillis()
|
||||
)
|
@@ -0,0 +1,12 @@
|
||||
package com.wismna.geoffroy.donext.data.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "task_lists")
|
||||
data class TaskListEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Long = 0,
|
||||
val name: String,
|
||||
val isDeleted: Boolean = false
|
||||
)
|
@@ -0,0 +1,31 @@
|
||||
package com.wismna.geoffroy.donext.data.injection
|
||||
|
||||
import android.content.Context
|
||||
import com.wismna.geoffroy.donext.data.local.AppDatabase
|
||||
import com.wismna.geoffroy.donext.data.local.dao.TaskDao
|
||||
import com.wismna.geoffroy.donext.data.local.dao.TaskListDao
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DatabaseModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDatabase(
|
||||
@ApplicationContext context: Context
|
||||
): AppDatabase {
|
||||
return AppDatabase.buildDatabase(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideTaskDao(db: AppDatabase): TaskDao = db.taskDao()
|
||||
|
||||
@Provides
|
||||
fun provideTaskListDao(db: AppDatabase): TaskListDao = db.taskListDao()
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
package com.wismna.geoffroy.donext.data.injection
|
||||
|
||||
import com.wismna.geoffroy.donext.data.local.repository.TaskRepositoryImpl
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
abstract class RepositoryModule {
|
||||
|
||||
@Binds
|
||||
abstract fun bindTaskRepository(
|
||||
impl: TaskRepositoryImpl
|
||||
): TaskRepository
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
package com.wismna.geoffroy.donext.data.local
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import com.wismna.geoffroy.donext.data.Converters
|
||||
import com.wismna.geoffroy.donext.data.local.dao.TaskDao
|
||||
import com.wismna.geoffroy.donext.data.local.dao.TaskListDao
|
||||
import com.wismna.geoffroy.donext.data.entities.TaskEntity
|
||||
import com.wismna.geoffroy.donext.data.entities.TaskListEntity
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Database(
|
||||
entities = [TaskEntity::class, TaskListEntity::class],
|
||||
version = 7
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun taskDao(): TaskDao
|
||||
abstract fun taskListDao(): TaskListDao
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var DB_INSTANCE: AppDatabase? = null
|
||||
|
||||
val MIGRATION_6_7 = object : Migration(6, 7) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
// TODO: migrate from old Donext database (v6)
|
||||
}
|
||||
}
|
||||
|
||||
fun buildDatabase(context: Context): AppDatabase {
|
||||
return DB_INSTANCE ?: synchronized(this) {
|
||||
val instance = Room.databaseBuilder(
|
||||
context,
|
||||
AppDatabase::class.java,
|
||||
"donext.db"
|
||||
)
|
||||
.addMigrations(MIGRATION_6_7)
|
||||
.fallbackToDestructiveMigration(false)
|
||||
.addCallback(object : Callback() {
|
||||
override fun onCreate(db: SupportSQLiteDatabase) {
|
||||
super.onCreate(db)
|
||||
// insert default lists
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val dao = DB_INSTANCE?.taskListDao()
|
||||
dao?.insertTaskList(TaskListEntity(name = "Work"))
|
||||
dao?.insertTaskList(TaskListEntity(name = "Personal"))
|
||||
dao?.insertTaskList(TaskListEntity(name = "Shopping"))
|
||||
}
|
||||
}
|
||||
})
|
||||
.build()
|
||||
DB_INSTANCE = instance
|
||||
return instance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
package com.wismna.geoffroy.donext.data.local.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import com.wismna.geoffroy.donext.data.entities.TaskEntity
|
||||
|
||||
@Dao
|
||||
interface TaskDao {
|
||||
@Query("SELECT * FROM tasks WHERE task_list_id = :listId")
|
||||
suspend fun getTasksForList(listId: Long): List<TaskEntity>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertTask(task: TaskEntity)
|
||||
|
||||
@Update
|
||||
suspend fun updateTask(task: TaskEntity)
|
||||
|
||||
@Query("UPDATE tasks SET done = :done, update_date = :updateDate WHERE id = :taskId")
|
||||
suspend fun markTaskDone(taskId: Long, done: Boolean, updateDate: Long = System.currentTimeMillis())
|
||||
|
||||
@Query("UPDATE tasks SET deleted = :deleted, update_date = :updateDate WHERE id = :taskId")
|
||||
suspend fun markTaskDeleted(taskId: Long, deleted: Boolean, updateDate: Long = System.currentTimeMillis())
|
||||
|
||||
@Query("UPDATE tasks SET cycles = cycles + 1, update_date = :updateDate WHERE id = :taskId")
|
||||
suspend fun increaseCycle(taskId: Long, updateDate: Long = System.currentTimeMillis())
|
||||
|
||||
@Query("UPDATE tasks SET deleted = :deleted WHERE id = :taskListId")
|
||||
suspend fun deleteAllTasksFromList(taskListId: Long, deleted: Boolean)
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
package com.wismna.geoffroy.donext.data.local.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import com.wismna.geoffroy.donext.data.entities.TaskListEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface TaskListDao {
|
||||
@Query("SELECT * FROM task_lists WHERE isDeleted = 0")
|
||||
fun getTaskLists(): Flow<List<TaskListEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertTaskList(taskList: TaskListEntity)
|
||||
|
||||
@Update
|
||||
suspend fun updateTaskList(taskList: TaskListEntity)
|
||||
|
||||
@Query("UPDATE task_lists SET isDeleted = :isDeleted WHERE id = :listId")
|
||||
suspend fun deleteTaskList(listId: Long, isDeleted: Boolean)
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
package com.wismna.geoffroy.donext.data.local.repository
|
||||
|
||||
import com.wismna.geoffroy.donext.data.local.dao.TaskDao
|
||||
import com.wismna.geoffroy.donext.data.local.dao.TaskListDao
|
||||
import com.wismna.geoffroy.donext.data.toDomain
|
||||
import com.wismna.geoffroy.donext.data.toEntity
|
||||
import com.wismna.geoffroy.donext.domain.model.Task
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.time.Instant
|
||||
import javax.inject.Inject
|
||||
|
||||
class TaskRepositoryImpl @Inject constructor(
|
||||
private val taskDao: TaskDao,
|
||||
private val taskListDao: TaskListDao
|
||||
): TaskRepository {
|
||||
override suspend fun getTasksForList(listId: Long): List<Task> {
|
||||
return taskDao.getTasksForList(listId).map { it.toDomain() }
|
||||
}
|
||||
|
||||
override suspend fun insertTask(task: Task) {
|
||||
taskDao.insertTask(task.toEntity())
|
||||
}
|
||||
|
||||
override suspend fun updateTask(task: Task) {
|
||||
val updated = task.copy(updateDate = Instant.now())
|
||||
taskDao.updateTask(updated.toEntity())
|
||||
}
|
||||
|
||||
override suspend fun deleteTask(taskId: Long, isDeleted: Boolean) {
|
||||
taskDao.markTaskDeleted(taskId, isDeleted)
|
||||
}
|
||||
|
||||
override suspend fun closeTask(taskId: Long, isDone: Boolean) {
|
||||
taskDao.markTaskDone(taskId, isDone)
|
||||
}
|
||||
|
||||
override suspend fun increaseTaskCycle(taskId: Long) {
|
||||
taskDao.increaseCycle(taskId)
|
||||
}
|
||||
|
||||
override fun getTaskLists(): Flow<List<TaskList>> {
|
||||
return taskListDao.getTaskLists().map {entities -> entities.map { it.toDomain() }}
|
||||
}
|
||||
|
||||
override suspend fun insertTaskList(taskList: TaskList) {
|
||||
taskListDao.insertTaskList(taskList.toEntity())
|
||||
}
|
||||
|
||||
override suspend fun deleteTaskList(taskListId: Long, isDeleted: Boolean) {
|
||||
taskDao.deleteAllTasksFromList(taskListId, isDeleted)
|
||||
taskListDao.deleteTaskList(taskListId, isDeleted)
|
||||
}
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
package com.wismna.geoffroy.donext.domain.model
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
data class Task(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val cycles: Int,
|
||||
val isDone: Boolean,
|
||||
val isDeleted: Boolean,
|
||||
val taskListId: Long,
|
||||
val updateDate: Instant = Instant.now()
|
||||
)
|
@@ -0,0 +1,7 @@
|
||||
package com.wismna.geoffroy.donext.domain.model
|
||||
|
||||
data class TaskList(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val isDeleted: Boolean
|
||||
)
|
@@ -0,0 +1,18 @@
|
||||
package com.wismna.geoffroy.donext.domain.repository
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.model.Task
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface TaskRepository {
|
||||
suspend fun getTasksForList(listId: Long): List<Task>
|
||||
suspend fun insertTask(task: Task)
|
||||
suspend fun updateTask(task: Task)
|
||||
suspend fun deleteTask(taskId: Long, isDeleted: Boolean)
|
||||
suspend fun closeTask(taskId: Long, isDone: Boolean)
|
||||
suspend fun increaseTaskCycle(taskId: Long)
|
||||
|
||||
fun getTaskLists(): Flow<List<TaskList>>
|
||||
suspend fun insertTaskList(taskList: TaskList)
|
||||
suspend fun deleteTaskList(taskListId: Long, isDeleted: Boolean)
|
||||
}
|
@@ -0,0 +1,10 @@
|
||||
package com.wismna.geoffroy.donext.domain.usecase
|
||||
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import com.wismna.geoffroy.donext.domain.repository.TaskRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetTaskListsUseCase @Inject constructor(private val repository: TaskRepository) {
|
||||
operator fun invoke(): Flow<List<TaskList>> = repository.getTaskLists()
|
||||
}
|
@@ -0,0 +1,114 @@
|
||||
package com.wismna.geoffroy.donext.presentation
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.PrimaryTabRow
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Tab
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import com.wismna.geoffroy.donext.presentation.screen.TaskListScreen
|
||||
import com.wismna.geoffroy.donext.presentation.ui.theme.DoNextTheme
|
||||
import com.wismna.geoffroy.donext.presentation.viewmodel.MainViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
DoNextTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
val viewModel: MainViewModel = hiltViewModel<MainViewModel>()
|
||||
MainScreen(
|
||||
viewModel,
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainScreen(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
||||
val navController = rememberNavController()
|
||||
|
||||
if (viewModel.isLoading) {
|
||||
// Show loading or empty state
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else {
|
||||
val startDestination = viewModel.taskLists[0]
|
||||
// TODO: get last opened tab from saved settings
|
||||
var selectedDestination by rememberSaveable { mutableIntStateOf(0) }
|
||||
|
||||
Scaffold(modifier = modifier) { contentPadding ->
|
||||
PrimaryTabRow(
|
||||
selectedTabIndex = selectedDestination,
|
||||
modifier = Modifier.padding(contentPadding)
|
||||
) {
|
||||
viewModel.taskLists.forEachIndexed { index, destination ->
|
||||
Tab(
|
||||
selected = selectedDestination == index,
|
||||
onClick = {
|
||||
navController.navigate(route = destination.name)
|
||||
selectedDestination = index
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = destination.name,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
AppNavHost(navController, startDestination, viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppNavHost(
|
||||
navController: NavHostController,
|
||||
startDestination: TaskList,
|
||||
viewModel: MainViewModel,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
NavHost(
|
||||
navController,
|
||||
startDestination = startDestination.name
|
||||
) {
|
||||
viewModel.taskLists.forEach { destination ->
|
||||
composable(destination.name) {
|
||||
TaskListScreen(destination, modifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,19 @@
|
||||
package com.wismna.geoffroy.donext.presentation.screen
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
|
||||
@Composable
|
||||
fun TaskListScreen(taskList: TaskList, modifier: Modifier = Modifier) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("${taskList.name} Screen")
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
package com.wismna.geoffroy.donext.presentation.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
@@ -0,0 +1,57 @@
|
||||
package com.wismna.geoffroy.donext.presentation.ui.theme
|
||||
|
||||
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.ui.platform.LocalContext
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
|
||||
/* Other default colors to override
|
||||
background = Color(0xFFFFFBFE),
|
||||
surface = Color(0xFFFFFBFE),
|
||||
onPrimary = Color.White,
|
||||
onSecondary = Color.White,
|
||||
onTertiary = Color.White,
|
||||
onBackground = Color(0xFF1C1B1F),
|
||||
onSurface = Color(0xFF1C1B1F),
|
||||
*/
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun DoNextTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
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
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = Typography,
|
||||
content = content
|
||||
)
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
package com.wismna.geoffroy.donext.presentation.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
|
||||
)
|
||||
*/
|
||||
)
|
@@ -0,0 +1,34 @@
|
||||
package com.wismna.geoffroy.donext.presentation.viewmodel
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.wismna.geoffroy.donext.domain.model.TaskList
|
||||
import com.wismna.geoffroy.donext.domain.usecase.GetTaskListsUseCase
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class MainViewModel @Inject constructor(
|
||||
getTaskLists: GetTaskListsUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
var taskLists by mutableStateOf<List<TaskList>>(emptyList())
|
||||
private set
|
||||
|
||||
var isLoading by mutableStateOf(true)
|
||||
private set
|
||||
|
||||
init {
|
||||
getTaskLists()
|
||||
.onEach { lists ->
|
||||
taskLists = lists
|
||||
isLoading = false
|
||||
}
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
}
|
5
donextv2/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@@ -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="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
@@ -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="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
BIN
donextv2/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
donextv2/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 547 B |
BIN
donextv2/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
donextv2/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 952 B |
BIN
donextv2/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 408 B |
BIN
donextv2/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
donextv2/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
donextv2/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 743 B |
BIN
donextv2/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
donextv2/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
donextv2/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
donextv2/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 6.4 KiB |
BIN
donextv2/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
donextv2/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
donextv2/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 9.3 KiB |
4
donextv2/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#303F9F</color>
|
||||
</resources>
|
4
donextv2/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<resources>
|
||||
<string name="app_name">DoNext</string>
|
||||
<string name="title_activity_main">MainActivity</string>
|
||||
</resources>
|
5
donextv2/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.DoNext" parent="android:Theme.Material.Light.NoActionBar" />
|
||||
</resources>
|