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