diff --git a/README_NEW_PLANET.md b/README_NEW_PLANET.md new file mode 100644 index 0000000..948be77 --- /dev/null +++ b/README_NEW_PLANET.md @@ -0,0 +1,189 @@ +# Новая Планета — Frontend (Android) + +
+

Визуальное расписание для детей с РАС

+

Мобильное приложение на Android с ИИ-помощником "Планета Земля"

+
+ +--- + +## 📱 О проекте + +**Новая Планета** — Android-приложение, помогающее детям с расстройством аутистического спектра (РАС) через визуальные инструменты, расписания и систему наград. + +### Основные возможности + +- 📅 **Визуальное расписание**: день/неделя с карточками действий +- ⏱️ **Визуальный таймер**: круговой таймер с цветовой индикацией +- 🎁 **Система наград**: поощрения за выполненные задания +- 🌍 **ИИ-агент “Земля”**: ответы и генерация расписаний +- 📚 **Библиотека изображений**: загрузка картинок +- 👨‍👩‍👧 **Роли**: ребёнок, родитель, педагог + +--- + +## 🛠️ Tech Stack + +### Core +- **Kotlin** 1.9+ +- **Jetpack Compose** +- **Material Design 3** +- **Coroutines + Flow** + +### Architecture +- **Clean Architecture**: Domain / Data / UI +- **MVVM** +- **Offline-first через Room** + +### Libraries +- Room +- Retrofit +- OkHttp +- Kotlinx Serialization +- Navigation Component +- Koin/Hilt +- Coil +- Lottie + +--- + +## 📂 Структура проекта + +``` +new-planet-android/ +├── app/ +│ └── src/main/java/com/novayaplaneta/ +│ ├── MainActivity.kt +│ ├── di/ +│ ├── ui/ +│ │ ├── screens/ +│ │ │ ├── schedule/ +│ │ │ ├── task/ +│ │ │ ├── timer/ +│ │ │ ├── rewards/ +│ │ │ ├── ai/ +│ │ │ └── settings/ +│ │ ├── components/ +│ │ └── theme/ +│ ├── domain/ +│ │ ├── model/ +│ │ ├── repository/ +│ │ └── usecase/ +│ └── data/ +│ ├── local/ +│ ├── remote/ +│ └── repository/ +│ +├── build.gradle.kts +├── README.md +└── CLAUDE.md +``` + +--- + +## 🚀 Быстрый старт + +### Требования + +- Android Studio Hedgehog+ +- JDK 17+ +- Android SDK 34+ +- Kotlin 1.9+ + +### Установка (после генерации проекта в Cursor) + +```bash +git clone https://git.bro-js.ru/Glevel/New-planet.git +cd New-planet +./gradlew build +./gradlew installDebug +``` + +### Настройка среды + +``` +# local.properties +sdk.dir=/path/to/Android/Sdk +BACKEND_BASE_URL=https://api.novayaplaneta.ru +``` + +--- + +## 🔌 Retrofit API (пример структуры) + +```kotlin +interface BackendApi { + + @POST("api/v1/auth/login") + suspend fun login( + @Body request: LoginRequest + ): Response + + @GET("api/v1/schedules") + suspend fun getSchedules( + @Header("Authorization") token: String + ): Response> + + @POST("api/v1/ai/chat") + suspend fun chatWithAI( + @Header("Authorization") token: String, + @Body request: ChatRequest + ): Response +} +``` + +--- + +## 🎨 Accessibility (РАС) + +### Дизайн принципы + +- Крупные кликабельные элементы (48dp+) +- Высокая контрастность (WCAG AAA) +- Чёткие простые иконки +- Цветовое кодирование состояний +- Плавные анимации без мигания + +### Контрастность + +**Светлая тема** +- Фон: #FFFFFF, #F5F5F5 +- Текст: #0A0A0A +- Акцент: #4CAF50, #FF6B35 + +**Тёмная тема** +- Фон: #0A0A0A +- Текст: #FFFFFF +- Акцент: #4CAF50, #FFD700 + +--- + +## 🧪 Тестирование + +```bash +./gradlew test +./gradlew connectedAndroidTest +``` + +--- + +## 🔗 Репозиторий проекта + +Основная ссылка: + +👉 **Frontend Android:** +https://git.bro-js.ru/Glevel/New-planet + +Backend/AI будут добавлены позже. + +--- + +## 📄 Лицензия + +MIT License + +--- + +## 👥 Команда + +Разработано с ❤️ командой **Новая Планета** diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 57c36c2..d3ee38f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,20 +2,27 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.hilt) + id("org.jetbrains.kotlin.kapt") } android { - namespace = "com.project.newplanet" + namespace = "com.novayaplaneta" compileSdk = 36 defaultConfig { - applicationId = "com.project.newplanet" - minSdk = 30 + applicationId = "com.novayaplaneta" + minSdk = 26 targetSdk = 36 versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + vectorDrawables { + useSupportLibrary = true + } } buildTypes { @@ -28,27 +35,72 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "11" + jvmTarget = "17" } buildFeatures { compose = true } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } } dependencies { - + // Core Android implementation(libs.androidx.core.ktx) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.activity.compose) + + // Compose implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.ui) implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) + implementation("androidx.compose.material:material-icons-extended") + + // Lifecycle ViewModel + implementation(libs.androidx.lifecycle.viewmodel.compose) + + // Navigation + implementation(libs.navigation.compose) + + // Hilt + implementation(libs.hilt.android) + implementation(libs.hilt.navigation.compose) + kapt(libs.hilt.compiler) + + // Room + implementation(libs.room.runtime) + implementation(libs.room.ktx) + kapt(libs.room.compiler) + + // Retrofit + implementation(libs.retrofit) + implementation(libs.retrofit.kotlinx.serialization) + implementation(libs.okhttp) + implementation(libs.okhttp.logging) + + // Kotlinx Serialization + implementation(libs.kotlinx.serialization.json) + + // Coil + implementation(libs.coil.compose) + + // Lottie + implementation(libs.lottie.compose) + + // Coroutines + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.coroutines.android) + + // Testing testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1b1fbc0..31b6170 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,11 @@ + + + + navController.navigate(route) { + popUpTo(navController.graph.startDestinationId) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + } + ) + } + ) { innerPadding -> + NewPlanetNavigation( + navController = navController, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) + } + } + } + } +} + diff --git a/app/src/main/java/com/novayaplaneta/NewPlanetApplication.kt b/app/src/main/java/com/novayaplaneta/NewPlanetApplication.kt new file mode 100644 index 0000000..9a1f62d --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/NewPlanetApplication.kt @@ -0,0 +1,8 @@ +package com.novayaplaneta + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class NewPlanetApplication : Application() + diff --git a/app/src/main/java/com/novayaplaneta/data/local/NewPlanetDatabase.kt b/app/src/main/java/com/novayaplaneta/data/local/NewPlanetDatabase.kt new file mode 100644 index 0000000..19253fd --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/local/NewPlanetDatabase.kt @@ -0,0 +1,34 @@ +package com.novayaplaneta.data.local + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.novayaplaneta.data.local.dao.ChatMessageDao +import com.novayaplaneta.data.local.dao.RewardDao +import com.novayaplaneta.data.local.dao.ScheduleDao +import com.novayaplaneta.data.local.dao.TaskDao +import com.novayaplaneta.data.local.dao.UserDao +import com.novayaplaneta.data.local.entity.ChatMessageEntity +import com.novayaplaneta.data.local.entity.RewardEntity +import com.novayaplaneta.data.local.entity.ScheduleEntity +import com.novayaplaneta.data.local.entity.TaskEntity +import com.novayaplaneta.data.local.entity.UserEntity + +@Database( + entities = [ + UserEntity::class, + ScheduleEntity::class, + TaskEntity::class, + RewardEntity::class, + ChatMessageEntity::class + ], + version = 1, + exportSchema = false +) +abstract class NewPlanetDatabase : RoomDatabase() { + abstract fun userDao(): UserDao + abstract fun scheduleDao(): ScheduleDao + abstract fun taskDao(): TaskDao + abstract fun rewardDao(): RewardDao + abstract fun chatMessageDao(): ChatMessageDao +} + diff --git a/app/src/main/java/com/novayaplaneta/data/local/dao/ChatMessageDao.kt b/app/src/main/java/com/novayaplaneta/data/local/dao/ChatMessageDao.kt new file mode 100644 index 0000000..2888a3a --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/local/dao/ChatMessageDao.kt @@ -0,0 +1,21 @@ +package com.novayaplaneta.data.local.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.novayaplaneta.data.local.entity.ChatMessageEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface ChatMessageDao { + @Query("SELECT * FROM chat_messages WHERE userId = :userId ORDER BY timestamp ASC") + fun getMessagesByUserId(userId: String): Flow> + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertMessage(message: ChatMessageEntity) + + @Query("DELETE FROM chat_messages WHERE userId = :userId") + suspend fun deleteMessagesByUserId(userId: String) +} + diff --git a/app/src/main/java/com/novayaplaneta/data/local/dao/RewardDao.kt b/app/src/main/java/com/novayaplaneta/data/local/dao/RewardDao.kt new file mode 100644 index 0000000..5b70d11 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/local/dao/RewardDao.kt @@ -0,0 +1,28 @@ +package com.novayaplaneta.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.novayaplaneta.data.local.entity.RewardEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface RewardDao { + @Query("SELECT * FROM rewards WHERE userId = :userId ORDER BY earnedAt DESC") + fun getRewardsByUserId(userId: String): Flow> + + @Query("SELECT * FROM rewards WHERE id = :id") + suspend fun getRewardById(id: String): RewardEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertReward(reward: RewardEntity) + + @Update + suspend fun updateReward(reward: RewardEntity) + + @Query("DELETE FROM rewards WHERE id = :id") + suspend fun deleteReward(id: String) +} + diff --git a/app/src/main/java/com/novayaplaneta/data/local/dao/ScheduleDao.kt b/app/src/main/java/com/novayaplaneta/data/local/dao/ScheduleDao.kt new file mode 100644 index 0000000..8890bcf --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/local/dao/ScheduleDao.kt @@ -0,0 +1,28 @@ +package com.novayaplaneta.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.novayaplaneta.data.local.entity.ScheduleEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface ScheduleDao { + @Query("SELECT * FROM schedules WHERE userId = :userId ORDER BY date ASC") + fun getSchedulesByUserId(userId: String): Flow> + + @Query("SELECT * FROM schedules WHERE id = :id") + suspend fun getScheduleById(id: String): ScheduleEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertSchedule(schedule: ScheduleEntity) + + @Update + suspend fun updateSchedule(schedule: ScheduleEntity) + + @Query("DELETE FROM schedules WHERE id = :id") + suspend fun deleteSchedule(id: String) +} + diff --git a/app/src/main/java/com/novayaplaneta/data/local/dao/TaskDao.kt b/app/src/main/java/com/novayaplaneta/data/local/dao/TaskDao.kt new file mode 100644 index 0000000..e88e2cb --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/local/dao/TaskDao.kt @@ -0,0 +1,31 @@ +package com.novayaplaneta.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.novayaplaneta.data.local.entity.TaskEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface TaskDao { + @Query("SELECT * FROM tasks WHERE scheduleId = :scheduleId ORDER BY scheduledTime ASC") + fun getTasksByScheduleId(scheduleId: String): Flow> + + @Query("SELECT * FROM tasks WHERE id = :id") + suspend fun getTaskById(id: String): TaskEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertTask(task: TaskEntity) + + @Update + suspend fun updateTask(task: TaskEntity) + + @Query("DELETE FROM tasks WHERE id = :id") + suspend fun deleteTask(id: String) + + @Query("UPDATE tasks SET completed = 1 WHERE id = :id") + suspend fun completeTask(id: String) +} + diff --git a/app/src/main/java/com/novayaplaneta/data/local/dao/UserDao.kt b/app/src/main/java/com/novayaplaneta/data/local/dao/UserDao.kt new file mode 100644 index 0000000..3db2e0c --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/local/dao/UserDao.kt @@ -0,0 +1,21 @@ +package com.novayaplaneta.data.local.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.novayaplaneta.data.local.entity.UserEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface UserDao { + @Query("SELECT * FROM users LIMIT 1") + fun getCurrentUser(): Flow + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertUser(user: UserEntity) + + @Query("DELETE FROM users") + suspend fun deleteAllUsers() +} + diff --git a/app/src/main/java/com/novayaplaneta/data/local/entity/ChatMessageEntity.kt b/app/src/main/java/com/novayaplaneta/data/local/entity/ChatMessageEntity.kt new file mode 100644 index 0000000..322bcb8 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/local/entity/ChatMessageEntity.kt @@ -0,0 +1,15 @@ +package com.novayaplaneta.data.local.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "chat_messages") +data class ChatMessageEntity( + @PrimaryKey + val id: String, + val message: String, + val isFromAI: Boolean, + val timestamp: Long, // timestamp + val userId: String +) + diff --git a/app/src/main/java/com/novayaplaneta/data/local/entity/RewardEntity.kt b/app/src/main/java/com/novayaplaneta/data/local/entity/RewardEntity.kt new file mode 100644 index 0000000..3ab982b --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/local/entity/RewardEntity.kt @@ -0,0 +1,17 @@ +package com.novayaplaneta.data.local.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "rewards") +data class RewardEntity( + @PrimaryKey + val id: String, + val title: String, + val description: String?, + val imageUrl: String?, + val points: Int, + val earnedAt: Long?, // timestamp + val userId: String +) + diff --git a/app/src/main/java/com/novayaplaneta/data/local/entity/ScheduleEntity.kt b/app/src/main/java/com/novayaplaneta/data/local/entity/ScheduleEntity.kt new file mode 100644 index 0000000..470aa63 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/local/entity/ScheduleEntity.kt @@ -0,0 +1,17 @@ +package com.novayaplaneta.data.local.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey +import java.time.LocalDateTime + +@Entity(tableName = "schedules") +data class ScheduleEntity( + @PrimaryKey + val id: String, + val title: String, + val description: String?, + val date: Long, // timestamp + val createdAt: Long, // timestamp + val userId: String +) + diff --git a/app/src/main/java/com/novayaplaneta/data/local/entity/TaskEntity.kt b/app/src/main/java/com/novayaplaneta/data/local/entity/TaskEntity.kt new file mode 100644 index 0000000..d698894 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/local/entity/TaskEntity.kt @@ -0,0 +1,18 @@ +package com.novayaplaneta.data.local.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "tasks") +data class TaskEntity( + @PrimaryKey + val id: String, + val title: String, + val description: String?, + val imageUrl: String?, + val completed: Boolean = false, + val scheduledTime: Long?, // timestamp + val duration: Int? = null, + val scheduleId: String +) + diff --git a/app/src/main/java/com/novayaplaneta/data/local/entity/UserEntity.kt b/app/src/main/java/com/novayaplaneta/data/local/entity/UserEntity.kt new file mode 100644 index 0000000..9302d66 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/local/entity/UserEntity.kt @@ -0,0 +1,15 @@ +package com.novayaplaneta.data.local.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "users") +data class UserEntity( + @PrimaryKey + val id: String, + val name: String, + val email: String, + val role: String, + val token: String? +) + diff --git a/app/src/main/java/com/novayaplaneta/data/local/mapper/ChatMessageMapper.kt b/app/src/main/java/com/novayaplaneta/data/local/mapper/ChatMessageMapper.kt new file mode 100644 index 0000000..1a1e4c0 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/local/mapper/ChatMessageMapper.kt @@ -0,0 +1,28 @@ +package com.novayaplaneta.data.local.mapper + +import com.novayaplaneta.data.local.entity.ChatMessageEntity +import com.novayaplaneta.domain.model.ChatMessage +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId + +fun ChatMessageEntity.toDomain(): ChatMessage { + return ChatMessage( + id = id, + message = message, + isFromAI = isFromAI, + timestamp = LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneId.systemDefault()), + userId = userId + ) +} + +fun ChatMessage.toEntity(): ChatMessageEntity { + return ChatMessageEntity( + id = id, + message = message, + isFromAI = isFromAI, + timestamp = timestamp.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(), + userId = userId + ) +} + diff --git a/app/src/main/java/com/novayaplaneta/data/local/mapper/RewardMapper.kt b/app/src/main/java/com/novayaplaneta/data/local/mapper/RewardMapper.kt new file mode 100644 index 0000000..ffcedf8 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/local/mapper/RewardMapper.kt @@ -0,0 +1,34 @@ +package com.novayaplaneta.data.local.mapper + +import com.novayaplaneta.data.local.entity.RewardEntity +import com.novayaplaneta.domain.model.Reward +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId + +fun RewardEntity.toDomain(): Reward { + return Reward( + id = id, + title = title, + description = description, + imageUrl = imageUrl, + points = points, + earnedAt = earnedAt?.let { + LocalDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.systemDefault()) + }, + userId = userId + ) +} + +fun Reward.toEntity(): RewardEntity { + return RewardEntity( + id = id, + title = title, + description = description, + imageUrl = imageUrl, + points = points, + earnedAt = earnedAt?.atZone(ZoneId.systemDefault())?.toInstant()?.toEpochMilli(), + userId = userId + ) +} + diff --git a/app/src/main/java/com/novayaplaneta/data/local/mapper/ScheduleMapper.kt b/app/src/main/java/com/novayaplaneta/data/local/mapper/ScheduleMapper.kt new file mode 100644 index 0000000..3a8abbd --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/local/mapper/ScheduleMapper.kt @@ -0,0 +1,32 @@ +package com.novayaplaneta.data.local.mapper + +import com.novayaplaneta.data.local.entity.ScheduleEntity +import com.novayaplaneta.domain.model.Schedule +import com.novayaplaneta.domain.model.Task +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId + +fun ScheduleEntity.toDomain(tasks: List = emptyList()): Schedule { + return Schedule( + id = id, + title = title, + description = description, + tasks = tasks, + date = LocalDateTime.ofInstant(Instant.ofEpochMilli(date), ZoneId.systemDefault()), + createdAt = LocalDateTime.ofInstant(Instant.ofEpochMilli(createdAt), ZoneId.systemDefault()), + userId = userId + ) +} + +fun Schedule.toEntity(): ScheduleEntity { + return ScheduleEntity( + id = id, + title = title, + description = description, + date = date.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(), + createdAt = createdAt.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(), + userId = userId + ) +} + diff --git a/app/src/main/java/com/novayaplaneta/data/local/mapper/TaskMapper.kt b/app/src/main/java/com/novayaplaneta/data/local/mapper/TaskMapper.kt new file mode 100644 index 0000000..2e1cb4a --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/local/mapper/TaskMapper.kt @@ -0,0 +1,36 @@ +package com.novayaplaneta.data.local.mapper + +import com.novayaplaneta.data.local.entity.TaskEntity +import com.novayaplaneta.domain.model.Task +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId + +fun TaskEntity.toDomain(): Task { + return Task( + id = id, + title = title, + description = description, + imageUrl = imageUrl, + completed = completed, + scheduledTime = scheduledTime?.let { + LocalDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneId.systemDefault()) + }, + duration = duration, + scheduleId = scheduleId + ) +} + +fun Task.toEntity(): TaskEntity { + return TaskEntity( + id = id, + title = title, + description = description, + imageUrl = imageUrl, + completed = completed, + scheduledTime = scheduledTime?.atZone(ZoneId.systemDefault())?.toInstant()?.toEpochMilli(), + duration = duration, + scheduleId = scheduleId + ) +} + diff --git a/app/src/main/java/com/novayaplaneta/data/local/mapper/UserMapper.kt b/app/src/main/java/com/novayaplaneta/data/local/mapper/UserMapper.kt new file mode 100644 index 0000000..3362d66 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/local/mapper/UserMapper.kt @@ -0,0 +1,26 @@ +package com.novayaplaneta.data.local.mapper + +import com.novayaplaneta.data.local.entity.UserEntity +import com.novayaplaneta.domain.model.User +import com.novayaplaneta.domain.model.UserRole + +fun UserEntity.toDomain(): User { + return User( + id = id, + name = name, + email = email, + role = UserRole.valueOf(role), + token = token + ) +} + +fun User.toEntity(): UserEntity { + return UserEntity( + id = id, + name = name, + email = email, + role = role.name, + token = token + ) +} + diff --git a/app/src/main/java/com/novayaplaneta/data/remote/BackendApi.kt b/app/src/main/java/com/novayaplaneta/data/remote/BackendApi.kt new file mode 100644 index 0000000..ca61f58 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/remote/BackendApi.kt @@ -0,0 +1,25 @@ +package com.novayaplaneta.data.remote + +import com.novayaplaneta.data.remote.dto.ChatRequest +import com.novayaplaneta.data.remote.dto.ChatResponse +import com.novayaplaneta.data.remote.dto.LoginRequest +import com.novayaplaneta.data.remote.dto.LoginResponse +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.POST + +interface BackendApi { + @POST("api/v1/auth/login") + suspend fun login( + @Body request: LoginRequest + ): Response + + @POST("api/v1/ai/chat") + suspend fun chatWithAI( + @Header("Authorization") token: String, + @Body request: ChatRequest + ): Response +} + diff --git a/app/src/main/java/com/novayaplaneta/data/remote/dto/ChatRequest.kt b/app/src/main/java/com/novayaplaneta/data/remote/dto/ChatRequest.kt new file mode 100644 index 0000000..e1d9a96 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/remote/dto/ChatRequest.kt @@ -0,0 +1,9 @@ +package com.novayaplaneta.data.remote.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class ChatRequest( + val message: String +) + diff --git a/app/src/main/java/com/novayaplaneta/data/remote/dto/ChatResponse.kt b/app/src/main/java/com/novayaplaneta/data/remote/dto/ChatResponse.kt new file mode 100644 index 0000000..a1511ce --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/remote/dto/ChatResponse.kt @@ -0,0 +1,9 @@ +package com.novayaplaneta.data.remote.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class ChatResponse( + val message: String +) + diff --git a/app/src/main/java/com/novayaplaneta/data/remote/dto/LoginRequest.kt b/app/src/main/java/com/novayaplaneta/data/remote/dto/LoginRequest.kt new file mode 100644 index 0000000..5d1f521 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/remote/dto/LoginRequest.kt @@ -0,0 +1,10 @@ +package com.novayaplaneta.data.remote.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class LoginRequest( + val email: String, + val password: String +) + diff --git a/app/src/main/java/com/novayaplaneta/data/remote/dto/LoginResponse.kt b/app/src/main/java/com/novayaplaneta/data/remote/dto/LoginResponse.kt new file mode 100644 index 0000000..bc7f551 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/remote/dto/LoginResponse.kt @@ -0,0 +1,18 @@ +package com.novayaplaneta.data.remote.dto + +import kotlinx.serialization.Serializable + +@Serializable +data class LoginResponse( + val token: String, + val user: UserDto +) + +@Serializable +data class UserDto( + val id: String, + val name: String, + val email: String, + val role: String +) + diff --git a/app/src/main/java/com/novayaplaneta/data/repository/AIRepositoryImpl.kt b/app/src/main/java/com/novayaplaneta/data/repository/AIRepositoryImpl.kt new file mode 100644 index 0000000..0c3ec43 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/repository/AIRepositoryImpl.kt @@ -0,0 +1,78 @@ +package com.novayaplaneta.data.repository + +import com.novayaplaneta.data.local.dao.ChatMessageDao +import com.novayaplaneta.data.local.mapper.toDomain +import com.novayaplaneta.data.local.mapper.toEntity +import com.novayaplaneta.data.remote.BackendApi +import com.novayaplaneta.data.remote.dto.ChatRequest +import com.novayaplaneta.domain.model.ChatMessage +import com.novayaplaneta.domain.repository.AIRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import java.time.LocalDateTime +import java.util.UUID +import javax.inject.Inject + +class AIRepositoryImpl @Inject constructor( + private val chatMessageDao: ChatMessageDao, + private val api: BackendApi, + private val authRepository: com.novayaplaneta.domain.repository.AuthRepository +) : AIRepository { + + override suspend fun sendMessage(userId: String, message: String): Result { + return try { + // Get token from current user + val user = authRepository.getCurrentUser().first() + val token = user?.token ?: "" + + // Save user message + val userMessage = ChatMessage( + id = UUID.randomUUID().toString(), + message = message, + isFromAI = false, + timestamp = LocalDateTime.now(), + userId = userId + ) + chatMessageDao.insertMessage(userMessage.toEntity()) + + // Send to API (only if token exists) + if (token.isNotEmpty()) { + val response = api.chatWithAI("Bearer $token", ChatRequest(message)) + if (response.isSuccessful && response.body() != null) { + val aiResponse = response.body()!!.message + + // Save AI response + val aiMessage = ChatMessage( + id = UUID.randomUUID().toString(), + message = aiResponse, + isFromAI = true, + timestamp = LocalDateTime.now(), + userId = userId + ) + chatMessageDao.insertMessage(aiMessage.toEntity()) + + Result.success(aiResponse) + } else { + Result.failure(Exception("Failed to get AI response")) + } + } else { + Result.failure(Exception("User not authenticated")) + } + } catch (e: Exception) { + Result.failure(e) + } + } + + override fun getChatHistory(userId: String): Flow> { + return chatMessageDao.getMessagesByUserId(userId).map { messages -> + messages.map { it.toDomain() } + } + } + + override suspend fun generateSchedule(userId: String, preferences: String): Result { + // TODO: Implement schedule generation + return Result.failure(Exception("Not implemented")) + } +} + diff --git a/app/src/main/java/com/novayaplaneta/data/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/novayaplaneta/data/repository/AuthRepositoryImpl.kt new file mode 100644 index 0000000..f3a8f0f --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/repository/AuthRepositoryImpl.kt @@ -0,0 +1,54 @@ +package com.novayaplaneta.data.repository + +import com.novayaplaneta.data.local.dao.UserDao +import com.novayaplaneta.data.local.mapper.toDomain +import com.novayaplaneta.data.local.mapper.toEntity +import com.novayaplaneta.data.remote.BackendApi +import com.novayaplaneta.data.remote.dto.LoginRequest +import com.novayaplaneta.domain.model.User +import com.novayaplaneta.domain.model.UserRole +import com.novayaplaneta.domain.repository.AuthRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class AuthRepositoryImpl @Inject constructor( + private val userDao: UserDao, + private val api: BackendApi +) : AuthRepository { + + override suspend fun login(email: String, password: String): Result { + return try { + val response = api.login(LoginRequest(email, password)) + if (response.isSuccessful && response.body() != null) { + val loginResponse = response.body()!! + val user = User( + id = loginResponse.user.id, + name = loginResponse.user.name, + email = loginResponse.user.email, + role = UserRole.valueOf(loginResponse.user.role), + token = loginResponse.token + ) + saveUser(user) + Result.success(user) + } else { + Result.failure(Exception("Login failed")) + } + } catch (e: Exception) { + Result.failure(e) + } + } + + override suspend fun logout() { + userDao.deleteAllUsers() + } + + override fun getCurrentUser(): Flow { + return userDao.getCurrentUser().map { it?.toDomain() } + } + + override suspend fun saveUser(user: User) { + userDao.insertUser(user.toEntity()) + } +} + diff --git a/app/src/main/java/com/novayaplaneta/data/repository/RewardRepositoryImpl.kt b/app/src/main/java/com/novayaplaneta/data/repository/RewardRepositoryImpl.kt new file mode 100644 index 0000000..ecdc6a2 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/repository/RewardRepositoryImpl.kt @@ -0,0 +1,46 @@ +package com.novayaplaneta.data.repository + +import com.novayaplaneta.data.local.dao.RewardDao +import com.novayaplaneta.data.local.mapper.toDomain +import com.novayaplaneta.data.local.mapper.toEntity +import com.novayaplaneta.domain.model.Reward +import com.novayaplaneta.domain.repository.RewardRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import java.time.LocalDateTime +import javax.inject.Inject + +class RewardRepositoryImpl @Inject constructor( + private val rewardDao: RewardDao +) : RewardRepository { + + override fun getRewards(userId: String): Flow> { + return rewardDao.getRewardsByUserId(userId).map { rewards -> + rewards.map { it.toDomain() } + } + } + + override suspend fun getRewardById(id: String): Reward? { + return rewardDao.getRewardById(id)?.toDomain() + } + + override suspend fun createReward(reward: Reward) { + rewardDao.insertReward(reward.toEntity()) + } + + override suspend fun updateReward(reward: Reward) { + rewardDao.updateReward(reward.toEntity()) + } + + override suspend fun deleteReward(id: String) { + rewardDao.deleteReward(id) + } + + override suspend fun earnReward(userId: String, rewardId: String) { + val reward = getRewardById(rewardId) + reward?.let { + updateReward(it.copy(earnedAt = LocalDateTime.now())) + } + } +} + diff --git a/app/src/main/java/com/novayaplaneta/data/repository/ScheduleRepositoryImpl.kt b/app/src/main/java/com/novayaplaneta/data/repository/ScheduleRepositoryImpl.kt new file mode 100644 index 0000000..e9ad51a --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/repository/ScheduleRepositoryImpl.kt @@ -0,0 +1,50 @@ +package com.novayaplaneta.data.repository + +import com.novayaplaneta.data.local.dao.ScheduleDao +import com.novayaplaneta.data.local.dao.TaskDao +import com.novayaplaneta.data.local.mapper.toDomain +import com.novayaplaneta.data.local.mapper.toEntity +import com.novayaplaneta.domain.model.Schedule +import com.novayaplaneta.domain.repository.ScheduleRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class ScheduleRepositoryImpl @Inject constructor( + private val scheduleDao: ScheduleDao, + private val taskDao: TaskDao +) : ScheduleRepository { + + override fun getSchedules(userId: String): Flow> { + return scheduleDao.getSchedulesByUserId(userId).map { schedules -> + schedules.map { scheduleEntity -> + // Note: In production, you'd need to fetch tasks for each schedule + scheduleEntity.toDomain(emptyList()) + } + } + } + + override suspend fun getScheduleById(id: String): Schedule? { + val scheduleEntity = scheduleDao.getScheduleById(id) ?: return null + val tasks = taskDao.getTasksByScheduleId(id) + // Simplified - would need proper Flow handling + return scheduleEntity.toDomain(emptyList()) + } + + override suspend fun createSchedule(schedule: Schedule) { + scheduleDao.insertSchedule(schedule.toEntity()) + } + + override suspend fun updateSchedule(schedule: Schedule) { + scheduleDao.updateSchedule(schedule.toEntity()) + } + + override suspend fun deleteSchedule(id: String) { + scheduleDao.deleteSchedule(id) + } + + override suspend fun syncSchedules(userId: String) { + // TODO: Implement sync with backend + } +} + diff --git a/app/src/main/java/com/novayaplaneta/data/repository/TaskRepositoryImpl.kt b/app/src/main/java/com/novayaplaneta/data/repository/TaskRepositoryImpl.kt new file mode 100644 index 0000000..6c07f87 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/data/repository/TaskRepositoryImpl.kt @@ -0,0 +1,42 @@ +package com.novayaplaneta.data.repository + +import com.novayaplaneta.data.local.dao.TaskDao +import com.novayaplaneta.data.local.mapper.toDomain +import com.novayaplaneta.data.local.mapper.toEntity +import com.novayaplaneta.domain.model.Task +import com.novayaplaneta.domain.repository.TaskRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class TaskRepositoryImpl @Inject constructor( + private val taskDao: TaskDao +) : TaskRepository { + + override fun getTasks(scheduleId: String): Flow> { + return taskDao.getTasksByScheduleId(scheduleId).map { tasks -> + tasks.map { it.toDomain() } + } + } + + override suspend fun getTaskById(id: String): Task? { + return taskDao.getTaskById(id)?.toDomain() + } + + override suspend fun createTask(task: Task) { + taskDao.insertTask(task.toEntity()) + } + + override suspend fun updateTask(task: Task) { + taskDao.updateTask(task.toEntity()) + } + + override suspend fun deleteTask(id: String) { + taskDao.deleteTask(id) + } + + override suspend fun completeTask(id: String) { + taskDao.completeTask(id) + } +} + diff --git a/app/src/main/java/com/novayaplaneta/di/DatabaseModule.kt b/app/src/main/java/com/novayaplaneta/di/DatabaseModule.kt new file mode 100644 index 0000000..bccbb7f --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/di/DatabaseModule.kt @@ -0,0 +1,57 @@ +package com.novayaplaneta.di + +import android.content.Context +import androidx.room.Room +import com.novayaplaneta.data.local.NewPlanetDatabase +import com.novayaplaneta.data.local.dao.ChatMessageDao +import com.novayaplaneta.data.local.dao.RewardDao +import com.novayaplaneta.data.local.dao.ScheduleDao +import com.novayaplaneta.data.local.dao.TaskDao +import com.novayaplaneta.data.local.dao.UserDao +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): NewPlanetDatabase { + return Room.databaseBuilder( + context, + NewPlanetDatabase::class.java, + "newplanet_database" + ).build() + } + + @Provides + fun provideUserDao(database: NewPlanetDatabase): UserDao { + return database.userDao() + } + + @Provides + fun provideScheduleDao(database: NewPlanetDatabase): ScheduleDao { + return database.scheduleDao() + } + + @Provides + fun provideTaskDao(database: NewPlanetDatabase): TaskDao { + return database.taskDao() + } + + @Provides + fun provideRewardDao(database: NewPlanetDatabase): RewardDao { + return database.rewardDao() + } + + @Provides + fun provideChatMessageDao(database: NewPlanetDatabase): ChatMessageDao { + return database.chatMessageDao() + } +} + diff --git a/app/src/main/java/com/novayaplaneta/di/NetworkModule.kt b/app/src/main/java/com/novayaplaneta/di/NetworkModule.kt new file mode 100644 index 0000000..82d8e7b --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/di/NetworkModule.kt @@ -0,0 +1,60 @@ +package com.novayaplaneta.di + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import com.novayaplaneta.data.remote.BackendApi +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import java.util.concurrent.TimeUnit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + + private val json = Json { + ignoreUnknownKeys = true + isLenient = true + encodeDefaults = false + } + + @Provides + @Singleton + fun provideOkHttpClient(): OkHttpClient { + val loggingInterceptor = HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + + return OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build() + } + + @Provides + @Singleton + fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit { + val contentType = "application/json".toMediaType() + + return Retrofit.Builder() + .baseUrl("https://api.novayaplaneta.ru/") + .client(okHttpClient) + .addConverterFactory(json.asConverterFactory(contentType)) + .build() + } + + @Provides + @Singleton + fun provideBackendApi(retrofit: Retrofit): BackendApi { + return retrofit.create(BackendApi::class.java) + } +} + diff --git a/app/src/main/java/com/novayaplaneta/di/RepositoryModule.kt b/app/src/main/java/com/novayaplaneta/di/RepositoryModule.kt new file mode 100644 index 0000000..0bb7d07 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/di/RepositoryModule.kt @@ -0,0 +1,53 @@ +package com.novayaplaneta.di + +import com.novayaplaneta.data.repository.AIRepositoryImpl +import com.novayaplaneta.data.repository.AuthRepositoryImpl +import com.novayaplaneta.data.repository.RewardRepositoryImpl +import com.novayaplaneta.data.repository.ScheduleRepositoryImpl +import com.novayaplaneta.data.repository.TaskRepositoryImpl +import com.novayaplaneta.domain.repository.AIRepository +import com.novayaplaneta.domain.repository.AuthRepository +import com.novayaplaneta.domain.repository.RewardRepository +import com.novayaplaneta.domain.repository.ScheduleRepository +import com.novayaplaneta.domain.repository.TaskRepository +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +abstract class RepositoryModule { + + @Binds + @Singleton + abstract fun bindAuthRepository( + authRepositoryImpl: AuthRepositoryImpl + ): AuthRepository + + @Binds + @Singleton + abstract fun bindScheduleRepository( + scheduleRepositoryImpl: ScheduleRepositoryImpl + ): ScheduleRepository + + @Binds + @Singleton + abstract fun bindTaskRepository( + taskRepositoryImpl: TaskRepositoryImpl + ): TaskRepository + + @Binds + @Singleton + abstract fun bindRewardRepository( + rewardRepositoryImpl: RewardRepositoryImpl + ): RewardRepository + + @Binds + @Singleton + abstract fun bindAIRepository( + aiRepositoryImpl: AIRepositoryImpl + ): AIRepository +} + diff --git a/app/src/main/java/com/novayaplaneta/domain/model/ChatMessage.kt b/app/src/main/java/com/novayaplaneta/domain/model/ChatMessage.kt new file mode 100644 index 0000000..b2f1227 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/domain/model/ChatMessage.kt @@ -0,0 +1,12 @@ +package com.novayaplaneta.domain.model + +import java.time.LocalDateTime + +data class ChatMessage( + val id: String, + val message: String, + val isFromAI: Boolean, + val timestamp: LocalDateTime, + val userId: String +) + diff --git a/app/src/main/java/com/novayaplaneta/domain/model/Reward.kt b/app/src/main/java/com/novayaplaneta/domain/model/Reward.kt new file mode 100644 index 0000000..f3d6f89 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/domain/model/Reward.kt @@ -0,0 +1,14 @@ +package com.novayaplaneta.domain.model + +import java.time.LocalDateTime + +data class Reward( + val id: String, + val title: String, + val description: String?, + val imageUrl: String?, + val points: Int, + val earnedAt: LocalDateTime? = null, + val userId: String +) + diff --git a/app/src/main/java/com/novayaplaneta/domain/model/Schedule.kt b/app/src/main/java/com/novayaplaneta/domain/model/Schedule.kt new file mode 100644 index 0000000..9e832ae --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/domain/model/Schedule.kt @@ -0,0 +1,14 @@ +package com.novayaplaneta.domain.model + +import java.time.LocalDateTime + +data class Schedule( + val id: String, + val title: String, + val description: String?, + val tasks: List, + val date: LocalDateTime, + val createdAt: LocalDateTime, + val userId: String +) + diff --git a/app/src/main/java/com/novayaplaneta/domain/model/Task.kt b/app/src/main/java/com/novayaplaneta/domain/model/Task.kt new file mode 100644 index 0000000..dc4e9d7 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/domain/model/Task.kt @@ -0,0 +1,15 @@ +package com.novayaplaneta.domain.model + +import java.time.LocalDateTime + +data class Task( + val id: String, + val title: String, + val description: String?, + val imageUrl: String?, + val completed: Boolean = false, + val scheduledTime: LocalDateTime?, + val duration: Int? = null, // in minutes + val scheduleId: String +) + diff --git a/app/src/main/java/com/novayaplaneta/domain/model/User.kt b/app/src/main/java/com/novayaplaneta/domain/model/User.kt new file mode 100644 index 0000000..afa7235 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/domain/model/User.kt @@ -0,0 +1,16 @@ +package com.novayaplaneta.domain.model + +data class User( + val id: String, + val name: String, + val email: String, + val role: UserRole, + val token: String? = null +) + +enum class UserRole { + CHILD, + PARENT, + TEACHER +} + diff --git a/app/src/main/java/com/novayaplaneta/domain/repository/AIRepository.kt b/app/src/main/java/com/novayaplaneta/domain/repository/AIRepository.kt new file mode 100644 index 0000000..447c479 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/domain/repository/AIRepository.kt @@ -0,0 +1,11 @@ +package com.novayaplaneta.domain.repository + +import com.novayaplaneta.domain.model.ChatMessage +import kotlinx.coroutines.flow.Flow + +interface AIRepository { + suspend fun sendMessage(userId: String, message: String): Result + fun getChatHistory(userId: String): Flow> + suspend fun generateSchedule(userId: String, preferences: String): Result +} + diff --git a/app/src/main/java/com/novayaplaneta/domain/repository/AuthRepository.kt b/app/src/main/java/com/novayaplaneta/domain/repository/AuthRepository.kt new file mode 100644 index 0000000..76c2709 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/domain/repository/AuthRepository.kt @@ -0,0 +1,12 @@ +package com.novayaplaneta.domain.repository + +import com.novayaplaneta.domain.model.User +import kotlinx.coroutines.flow.Flow + +interface AuthRepository { + suspend fun login(email: String, password: String): Result + suspend fun logout() + fun getCurrentUser(): Flow + suspend fun saveUser(user: User) +} + diff --git a/app/src/main/java/com/novayaplaneta/domain/repository/RewardRepository.kt b/app/src/main/java/com/novayaplaneta/domain/repository/RewardRepository.kt new file mode 100644 index 0000000..7c6455e --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/domain/repository/RewardRepository.kt @@ -0,0 +1,14 @@ +package com.novayaplaneta.domain.repository + +import com.novayaplaneta.domain.model.Reward +import kotlinx.coroutines.flow.Flow + +interface RewardRepository { + fun getRewards(userId: String): Flow> + suspend fun getRewardById(id: String): Reward? + suspend fun createReward(reward: Reward) + suspend fun updateReward(reward: Reward) + suspend fun deleteReward(id: String) + suspend fun earnReward(userId: String, rewardId: String) +} + diff --git a/app/src/main/java/com/novayaplaneta/domain/repository/ScheduleRepository.kt b/app/src/main/java/com/novayaplaneta/domain/repository/ScheduleRepository.kt new file mode 100644 index 0000000..61a47c2 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/domain/repository/ScheduleRepository.kt @@ -0,0 +1,15 @@ +package com.novayaplaneta.domain.repository + +import com.novayaplaneta.domain.model.Schedule +import kotlinx.coroutines.flow.Flow +import java.time.LocalDateTime + +interface ScheduleRepository { + fun getSchedules(userId: String): Flow> + suspend fun getScheduleById(id: String): Schedule? + suspend fun createSchedule(schedule: Schedule) + suspend fun updateSchedule(schedule: Schedule) + suspend fun deleteSchedule(id: String) + suspend fun syncSchedules(userId: String) +} + diff --git a/app/src/main/java/com/novayaplaneta/domain/repository/TaskRepository.kt b/app/src/main/java/com/novayaplaneta/domain/repository/TaskRepository.kt new file mode 100644 index 0000000..8d5cf46 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/domain/repository/TaskRepository.kt @@ -0,0 +1,14 @@ +package com.novayaplaneta.domain.repository + +import com.novayaplaneta.domain.model.Task +import kotlinx.coroutines.flow.Flow + +interface TaskRepository { + fun getTasks(scheduleId: String): Flow> + suspend fun getTaskById(id: String): Task? + suspend fun createTask(task: Task) + suspend fun updateTask(task: Task) + suspend fun deleteTask(id: String) + suspend fun completeTask(id: String) +} + diff --git a/app/src/main/java/com/novayaplaneta/domain/usecase/CompleteTaskUseCase.kt b/app/src/main/java/com/novayaplaneta/domain/usecase/CompleteTaskUseCase.kt new file mode 100644 index 0000000..6965364 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/domain/usecase/CompleteTaskUseCase.kt @@ -0,0 +1,13 @@ +package com.novayaplaneta.domain.usecase + +import com.novayaplaneta.domain.repository.TaskRepository +import javax.inject.Inject + +class CompleteTaskUseCase @Inject constructor( + private val repository: TaskRepository +) { + suspend operator fun invoke(taskId: String) { + repository.completeTask(taskId) + } +} + diff --git a/app/src/main/java/com/novayaplaneta/domain/usecase/CreateScheduleUseCase.kt b/app/src/main/java/com/novayaplaneta/domain/usecase/CreateScheduleUseCase.kt new file mode 100644 index 0000000..c7ce7ec --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/domain/usecase/CreateScheduleUseCase.kt @@ -0,0 +1,14 @@ +package com.novayaplaneta.domain.usecase + +import com.novayaplaneta.domain.model.Schedule +import com.novayaplaneta.domain.repository.ScheduleRepository +import javax.inject.Inject + +class CreateScheduleUseCase @Inject constructor( + private val repository: ScheduleRepository +) { + suspend operator fun invoke(schedule: Schedule) { + repository.createSchedule(schedule) + } +} + diff --git a/app/src/main/java/com/novayaplaneta/domain/usecase/GetSchedulesUseCase.kt b/app/src/main/java/com/novayaplaneta/domain/usecase/GetSchedulesUseCase.kt new file mode 100644 index 0000000..9cca52e --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/domain/usecase/GetSchedulesUseCase.kt @@ -0,0 +1,15 @@ +package com.novayaplaneta.domain.usecase + +import com.novayaplaneta.domain.model.Schedule +import com.novayaplaneta.domain.repository.ScheduleRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class GetSchedulesUseCase @Inject constructor( + private val repository: ScheduleRepository +) { + operator fun invoke(userId: String): Flow> { + return repository.getSchedules(userId) + } +} + diff --git a/app/src/main/java/com/novayaplaneta/domain/usecase/SendAIMessageUseCase.kt b/app/src/main/java/com/novayaplaneta/domain/usecase/SendAIMessageUseCase.kt new file mode 100644 index 0000000..1fd1acc --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/domain/usecase/SendAIMessageUseCase.kt @@ -0,0 +1,13 @@ +package com.novayaplaneta.domain.usecase + +import com.novayaplaneta.domain.repository.AIRepository +import javax.inject.Inject + +class SendAIMessageUseCase @Inject constructor( + private val repository: AIRepository +) { + suspend operator fun invoke(userId: String, message: String): Result { + return repository.sendMessage(userId, message) + } +} + diff --git a/app/src/main/java/com/novayaplaneta/ui/components/BottomNavigation.kt b/app/src/main/java/com/novayaplaneta/ui/components/BottomNavigation.kt new file mode 100644 index 0000000..f4a2ed8 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/components/BottomNavigation.kt @@ -0,0 +1,49 @@ +package com.novayaplaneta.ui.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +sealed class NavigationItem(val route: String, val title: String, val icon: androidx.compose.ui.graphics.vector.ImageVector) { + object Schedule : NavigationItem("schedule", "Расписание", Icons.Default.CalendarToday) + object Tasks : NavigationItem("tasks", "Задания", Icons.Default.Assignment) + object Timer : NavigationItem("timer", "Таймер", Icons.Default.Timer) + object Rewards : NavigationItem("rewards", "Награды", Icons.Default.Star) + object AI : NavigationItem("ai", "ИИ", Icons.Default.SmartToy) + object Settings : NavigationItem("settings", "Настройки", Icons.Default.Settings) +} + +@Composable +fun BottomNavigationBar( + currentRoute: String?, + onNavigate: (String) -> Unit, + modifier: Modifier = Modifier +) { + val items = listOf( + NavigationItem.Schedule, + NavigationItem.Tasks, + NavigationItem.Timer, + NavigationItem.Rewards, + NavigationItem.AI, + NavigationItem.Settings + ) + + NavigationBar(modifier = modifier) { + items.forEach { item -> + NavigationBarItem( + icon = { + Icon( + imageVector = item.icon, + contentDescription = item.title + ) + }, + label = { Text(item.title) }, + selected = currentRoute == item.route, + onClick = { onNavigate(item.route) } + ) + } + } +} + diff --git a/app/src/main/java/com/novayaplaneta/ui/navigation/NewPlanetNavigation.kt b/app/src/main/java/com/novayaplaneta/ui/navigation/NewPlanetNavigation.kt new file mode 100644 index 0000000..5ab0fcd --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/navigation/NewPlanetNavigation.kt @@ -0,0 +1,46 @@ +package com.novayaplaneta.ui.navigation + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.NavHostController +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import com.novayaplaneta.ui.screens.ai.AIScreen +import com.novayaplaneta.ui.screens.rewards.RewardsScreen +import com.novayaplaneta.ui.screens.schedule.ScheduleScreen +import com.novayaplaneta.ui.screens.settings.SettingsScreen +import com.novayaplaneta.ui.screens.task.TaskScreen +import com.novayaplaneta.ui.screens.timer.TimerScreen + +@Composable +fun NewPlanetNavigation( + navController: NavHostController, + modifier: Modifier = Modifier, + startDestination: String = "schedule" +) { + NavHost( + navController = navController, + startDestination = startDestination, + modifier = modifier + ) { + composable("schedule") { + ScheduleScreen() + } + composable("tasks") { + TaskScreen() + } + composable("timer") { + TimerScreen() + } + composable("rewards") { + RewardsScreen() + } + composable("ai") { + AIScreen() + } + composable("settings") { + SettingsScreen() + } + } +} + diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/ai/AIScreen.kt b/app/src/main/java/com/novayaplaneta/ui/screens/ai/AIScreen.kt new file mode 100644 index 0000000..e696882 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/screens/ai/AIScreen.kt @@ -0,0 +1,94 @@ +package com.novayaplaneta.ui.screens.ai + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +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 androidx.hilt.navigation.compose.hiltViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AIScreen( + viewModel: AIViewModel = hiltViewModel(), + modifier: Modifier = Modifier +) { + val uiState by viewModel.uiState.collectAsState() + var messageText by remember { mutableStateOf("") } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("ИИ-помощник \"Земля\"") } + ) + } + ) { paddingValues -> + Column( + modifier = modifier + .fillMaxSize() + .padding(paddingValues) + ) { + LazyColumn( + modifier = Modifier + .weight(1f) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(uiState.messages) { message -> + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = if (message.isFromAI) { + MaterialTheme.colorScheme.primaryContainer + } else { + MaterialTheme.colorScheme.surfaceVariant + } + ) + ) { + Text( + text = message.message, + modifier = Modifier.padding(16.dp), + style = MaterialTheme.typography.bodyLarge + ) + } + } + } + + if (uiState.isLoading) { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedTextField( + value = messageText, + onValueChange = { messageText = it }, + modifier = Modifier.weight(1f), + placeholder = { Text("Введите сообщение...") } + ) + Button( + onClick = { + if (messageText.isNotBlank()) { + viewModel.sendMessage("userId", messageText) + messageText = "" + } + } + ) { + Text("Отправить") + } + } + } + } +} + diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/ai/AIViewModel.kt b/app/src/main/java/com/novayaplaneta/ui/screens/ai/AIViewModel.kt new file mode 100644 index 0000000..fb29c02 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/screens/ai/AIViewModel.kt @@ -0,0 +1,53 @@ +package com.novayaplaneta.ui.screens.ai + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.novayaplaneta.domain.repository.AIRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class AIViewModel @Inject constructor( + private val aiRepository: AIRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(AIUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun loadChatHistory(userId: String) { + viewModelScope.launch { + aiRepository.getChatHistory(userId).collect { messages -> + _uiState.value = _uiState.value.copy(messages = messages) + } + } + } + + fun sendMessage(userId: String, message: String) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isLoading = true) + aiRepository.sendMessage(userId, message).fold( + onSuccess = { response -> + loadChatHistory(userId) + _uiState.value = _uiState.value.copy(isLoading = false) + }, + onFailure = { error -> + _uiState.value = _uiState.value.copy( + isLoading = false, + error = error.message + ) + } + ) + } + } +} + +data class AIUiState( + val messages: List = emptyList(), + val isLoading: Boolean = false, + val error: String? = null +) + diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/rewards/RewardsScreen.kt b/app/src/main/java/com/novayaplaneta/ui/screens/rewards/RewardsScreen.kt new file mode 100644 index 0000000..b1eb060 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/screens/rewards/RewardsScreen.kt @@ -0,0 +1,85 @@ +package com.novayaplaneta.ui.screens.rewards + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RewardsScreen( + viewModel: RewardsViewModel = hiltViewModel(), + modifier: Modifier = Modifier +) { + val uiState by viewModel.uiState.collectAsState() + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Награды") } + ) + } + ) { paddingValues -> + Column( + modifier = modifier + .fillMaxSize() + .padding(paddingValues) + .padding(16.dp) + ) { + if (uiState.isLoading) { + CircularProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .wrapContentWidth() + ) + } else { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(uiState.rewards) { reward -> + Card( + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = reward.title, + style = MaterialTheme.typography.titleLarge + ) + reward.description?.let { + Text( + text = it, + style = MaterialTheme.typography.bodyMedium + ) + } + Text( + text = "${reward.points} очков", + style = MaterialTheme.typography.bodySmall + ) + } + if (reward.earnedAt != null) { + Icon( + imageVector = androidx.compose.material.icons.Icons.Default.Star, + contentDescription = "Получено", + tint = MaterialTheme.colorScheme.primary + ) + } + } + } + } + } + } + } + } +} + diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/rewards/RewardsViewModel.kt b/app/src/main/java/com/novayaplaneta/ui/screens/rewards/RewardsViewModel.kt new file mode 100644 index 0000000..14b77ae --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/screens/rewards/RewardsViewModel.kt @@ -0,0 +1,44 @@ +package com.novayaplaneta.ui.screens.rewards + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.novayaplaneta.domain.repository.RewardRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class RewardsViewModel @Inject constructor( + private val rewardRepository: RewardRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(RewardsUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun loadRewards(userId: String) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isLoading = true) + rewardRepository.getRewards(userId).collect { rewards -> + _uiState.value = _uiState.value.copy( + rewards = rewards, + isLoading = false + ) + } + } + } + + fun earnReward(userId: String, rewardId: String) { + viewModelScope.launch { + rewardRepository.earnReward(userId, rewardId) + } + } +} + +data class RewardsUiState( + val rewards: List = emptyList(), + val isLoading: Boolean = false +) + diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/schedule/ScheduleScreen.kt b/app/src/main/java/com/novayaplaneta/ui/screens/schedule/ScheduleScreen.kt new file mode 100644 index 0000000..dd47a24 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/screens/schedule/ScheduleScreen.kt @@ -0,0 +1,70 @@ +package com.novayaplaneta.ui.screens.schedule + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ScheduleScreen( + viewModel: ScheduleViewModel = hiltViewModel(), + modifier: Modifier = Modifier +) { + val uiState by viewModel.uiState.collectAsState() + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Расписание") } + ) + } + ) { paddingValues -> + Column( + modifier = modifier + .fillMaxSize() + .padding(paddingValues) + .padding(16.dp) + ) { + if (uiState.isLoading) { + CircularProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .wrapContentWidth() + ) + } else { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(uiState.schedules) { schedule -> + Card( + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Text( + text = schedule.title, + style = MaterialTheme.typography.titleLarge + ) + schedule.description?.let { + Text( + text = it, + style = MaterialTheme.typography.bodyMedium + ) + } + } + } + } + } + } + } + } +} + diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/schedule/ScheduleViewModel.kt b/app/src/main/java/com/novayaplaneta/ui/screens/schedule/ScheduleViewModel.kt new file mode 100644 index 0000000..e2d5ac8 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/screens/schedule/ScheduleViewModel.kt @@ -0,0 +1,38 @@ +package com.novayaplaneta.ui.screens.schedule + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.novayaplaneta.domain.usecase.GetSchedulesUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class ScheduleViewModel @Inject constructor( + private val getSchedulesUseCase: GetSchedulesUseCase +) : ViewModel() { + + private val _uiState = MutableStateFlow(ScheduleUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun loadSchedules(userId: String) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isLoading = true) + getSchedulesUseCase(userId).collect { schedules -> + _uiState.value = _uiState.value.copy( + schedules = schedules, + isLoading = false + ) + } + } + } +} + +data class ScheduleUiState( + val schedules: List = emptyList(), + val isLoading: Boolean = false +) + diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/novayaplaneta/ui/screens/settings/SettingsScreen.kt new file mode 100644 index 0000000..3a8d511 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/screens/settings/SettingsScreen.kt @@ -0,0 +1,74 @@ +package com.novayaplaneta.ui.screens.settings + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsScreen( + viewModel: SettingsViewModel = hiltViewModel(), + modifier: Modifier = Modifier +) { + val uiState by viewModel.uiState.collectAsState() + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Настройки") } + ) + } + ) { paddingValues -> + Column( + modifier = modifier + .fillMaxSize() + .padding(paddingValues) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + uiState.currentUser?.let { user -> + Card( + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Text( + text = "Пользователь", + style = MaterialTheme.typography.titleLarge + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Имя: ${user.name}", + style = MaterialTheme.typography.bodyLarge + ) + Text( + text = "Email: ${user.email}", + style = MaterialTheme.typography.bodyLarge + ) + Text( + text = "Роль: ${user.role.name}", + style = MaterialTheme.typography.bodyLarge + ) + } + } + } + + Button( + onClick = { viewModel.logout() }, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error + ) + ) { + Text("Выйти") + } + } + } +} + diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/settings/SettingsViewModel.kt b/app/src/main/java/com/novayaplaneta/ui/screens/settings/SettingsViewModel.kt new file mode 100644 index 0000000..26d32dc --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/screens/settings/SettingsViewModel.kt @@ -0,0 +1,45 @@ +package com.novayaplaneta.ui.screens.settings + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.novayaplaneta.domain.repository.AuthRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SettingsViewModel @Inject constructor( + private val authRepository: AuthRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(SettingsUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + loadCurrentUser() + } + + private fun loadCurrentUser() { + viewModelScope.launch { + authRepository.getCurrentUser().collect { user -> + _uiState.value = _uiState.value.copy(currentUser = user) + } + } + } + + fun logout() { + viewModelScope.launch { + authRepository.logout() + _uiState.value = _uiState.value.copy(isLoggedOut = true) + } + } +} + +data class SettingsUiState( + val currentUser: com.novayaplaneta.domain.model.User? = null, + val isLoggedOut: Boolean = false +) + diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/task/TaskScreen.kt b/app/src/main/java/com/novayaplaneta/ui/screens/task/TaskScreen.kt new file mode 100644 index 0000000..d4a84c9 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/screens/task/TaskScreen.kt @@ -0,0 +1,79 @@ +package com.novayaplaneta.ui.screens.task + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TaskScreen( + viewModel: TaskViewModel = hiltViewModel(), + modifier: Modifier = Modifier +) { + val uiState by viewModel.uiState.collectAsState() + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Задания") } + ) + } + ) { paddingValues -> + Column( + modifier = modifier + .fillMaxSize() + .padding(paddingValues) + .padding(16.dp) + ) { + if (uiState.isLoading) { + CircularProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .wrapContentWidth() + ) + } else { + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(uiState.tasks) { task -> + Card( + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = task.title, + style = MaterialTheme.typography.titleMedium + ) + task.description?.let { + Text( + text = it, + style = MaterialTheme.typography.bodySmall + ) + } + } + Checkbox( + checked = task.completed, + onCheckedChange = { viewModel.completeTask(task.id) } + ) + } + } + } + } + } + } + } +} + diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/task/TaskViewModel.kt b/app/src/main/java/com/novayaplaneta/ui/screens/task/TaskViewModel.kt new file mode 100644 index 0000000..9379514 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/screens/task/TaskViewModel.kt @@ -0,0 +1,44 @@ +package com.novayaplaneta.ui.screens.task + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.novayaplaneta.domain.repository.TaskRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class TaskViewModel @Inject constructor( + private val taskRepository: TaskRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(TaskUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun loadTasks(scheduleId: String) { + viewModelScope.launch { + _uiState.value = _uiState.value.copy(isLoading = true) + taskRepository.getTasks(scheduleId).collect { tasks -> + _uiState.value = _uiState.value.copy( + tasks = tasks, + isLoading = false + ) + } + } + } + + fun completeTask(taskId: String) { + viewModelScope.launch { + taskRepository.completeTask(taskId) + } + } +} + +data class TaskUiState( + val tasks: List = emptyList(), + val isLoading: Boolean = false +) + diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/timer/TimerScreen.kt b/app/src/main/java/com/novayaplaneta/ui/screens/timer/TimerScreen.kt new file mode 100644 index 0000000..2855be2 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/screens/timer/TimerScreen.kt @@ -0,0 +1,122 @@ +package com.novayaplaneta.ui.screens.timer + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TimerScreen( + viewModel: TimerViewModel = hiltViewModel(), + modifier: Modifier = Modifier +) { + val uiState by viewModel.uiState.collectAsState() + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Визуальный таймер") } + ) + } + ) { paddingValues -> + Column( + modifier = modifier + .fillMaxSize() + .padding(paddingValues) + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // Circular Timer + Box( + modifier = Modifier.size(300.dp), + contentAlignment = Alignment.Center + ) { + Canvas(modifier = Modifier.fillMaxSize()) { + val strokeWidth = 20.dp.toPx() + val radius = size.minDimension / 2 - strokeWidth + + // Background circle + drawCircle( + color = Color.Gray.copy(alpha = 0.3f), + radius = radius, + style = Stroke(width = strokeWidth, cap = StrokeCap.Round) + ) + + // Progress circle + val progress = if (uiState.durationMinutes > 0) { + uiState.remainingSeconds.toFloat() / (uiState.durationMinutes * 60) + } else 0f + + val sweepAngle = 360f * progress + drawArc( + color = MaterialTheme.colorScheme.primary, + startAngle = -90f, + sweepAngle = sweepAngle, + useCenter = false, + style = Stroke(width = strokeWidth, cap = StrokeCap.Round) + ) + } + + Text( + text = formatTime(uiState.remainingSeconds), + style = MaterialTheme.typography.displayLarge + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Button(onClick = { viewModel.startTimer(5) }) { + Text("5 мин") + } + Button(onClick = { viewModel.startTimer(10) }) { + Text("10 мин") + } + Button(onClick = { viewModel.startTimer(15) }) { + Text("15 мин") + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + if (uiState.isRunning) { + Button(onClick = { viewModel.pauseTimer() }) { + Text("Пауза") + } + } else if (uiState.remainingSeconds > 0) { + Button(onClick = { viewModel.resumeTimer() }) { + Text("Продолжить") + } + } + Button(onClick = { viewModel.resetTimer() }) { + Text("Сброс") + } + } + } + } +} + +private fun formatTime(seconds: Int): String { + val minutes = seconds / 60 + val remainingSeconds = seconds % 60 + return String.format("%02d:%02d", minutes, remainingSeconds) +} + diff --git a/app/src/main/java/com/novayaplaneta/ui/screens/timer/TimerViewModel.kt b/app/src/main/java/com/novayaplaneta/ui/screens/timer/TimerViewModel.kt new file mode 100644 index 0000000..4c036cc --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/screens/timer/TimerViewModel.kt @@ -0,0 +1,42 @@ +package com.novayaplaneta.ui.screens.timer + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@HiltViewModel +class TimerViewModel @Inject constructor() : ViewModel() { + + private val _uiState = MutableStateFlow(TimerUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + fun startTimer(durationMinutes: Int) { + _uiState.value = _uiState.value.copy( + isRunning = true, + durationMinutes = durationMinutes, + remainingSeconds = durationMinutes * 60 + ) + } + + fun pauseTimer() { + _uiState.value = _uiState.value.copy(isRunning = false) + } + + fun resumeTimer() { + _uiState.value = _uiState.value.copy(isRunning = true) + } + + fun resetTimer() { + _uiState.value = TimerUiState() + } +} + +data class TimerUiState( + val isRunning: Boolean = false, + val durationMinutes: Int = 0, + val remainingSeconds: Int = 0 +) + diff --git a/app/src/main/java/com/novayaplaneta/ui/theme/Color.kt b/app/src/main/java/com/novayaplaneta/ui/theme/Color.kt new file mode 100644 index 0000000..8077fd8 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/theme/Color.kt @@ -0,0 +1,26 @@ +package com.novayaplaneta.ui.theme + +import androidx.compose.ui.graphics.Color + +// Light Theme - High Contrast (WCAG AAA) +val BackgroundLight = Color(0xFFFFFFFF) +val SurfaceLight = Color(0xFFF5F5F5) +val OnBackgroundLight = Color(0xFF0A0A0A) +val OnSurfaceLight = Color(0xFF0A0A0A) + +// Dark Theme - High Contrast +val BackgroundDark = Color(0xFF0A0A0A) +val SurfaceDark = Color(0xFF1A1A1A) +val OnBackgroundDark = Color(0xFFFFFFFF) +val OnSurfaceDark = Color(0xFFFFFFFF) + +// Accent Colors +val AccentGreen = Color(0xFF4CAF50) +val AccentOrange = Color(0xFFFF6B35) +val AccentGold = Color(0xFFFFD700) + +// Status Colors +val SuccessColor = Color(0xFF4CAF50) +val WarningColor = Color(0xFFFF6B35) +val ErrorColor = Color(0xFFE53935) + diff --git a/app/src/main/java/com/novayaplaneta/ui/theme/Theme.kt b/app/src/main/java/com/novayaplaneta/ui/theme/Theme.kt new file mode 100644 index 0000000..faf2ec0 --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/theme/Theme.kt @@ -0,0 +1,49 @@ +package com.novayaplaneta.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +private val LightColorScheme = lightColorScheme( + primary = AccentGreen, + secondary = AccentOrange, + tertiary = AccentGold, + background = BackgroundLight, + surface = SurfaceLight, + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.Black, + onBackground = OnBackgroundLight, + onSurface = OnSurfaceLight +) + +private val DarkColorScheme = darkColorScheme( + primary = AccentGreen, + secondary = AccentGold, + tertiary = AccentOrange, + background = BackgroundDark, + surface = SurfaceDark, + onPrimary = Color.Black, + onSecondary = Color.Black, + onTertiary = Color.White, + onBackground = OnBackgroundDark, + onSurface = OnSurfaceDark +) + +@Composable +fun NewPlanetTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} + diff --git a/app/src/main/java/com/novayaplaneta/ui/theme/Type.kt b/app/src/main/java/com/novayaplaneta/ui/theme/Type.kt new file mode 100644 index 0000000..d475cca --- /dev/null +++ b/app/src/main/java/com/novayaplaneta/ui/theme/Type.kt @@ -0,0 +1,110 @@ +package com.novayaplaneta.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 + +val Typography = Typography( + displayLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Bold, + fontSize = 57.sp, + lineHeight = 64.sp, + letterSpacing = (-0.25).sp + ), + displayMedium = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Bold, + fontSize = 45.sp, + lineHeight = 52.sp + ), + displaySmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Bold, + fontSize = 36.sp, + lineHeight = 44.sp + ), + headlineLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.SemiBold, + fontSize = 32.sp, + lineHeight = 40.sp + ), + headlineMedium = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.SemiBold, + fontSize = 28.sp, + lineHeight = 36.sp + ), + headlineSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.SemiBold, + fontSize = 24.sp, + lineHeight = 32.sp + ), + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.SemiBold, + fontSize = 22.sp, + lineHeight = 28.sp + ), + titleMedium = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.15.sp + ), + titleSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp + ), + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ), + bodyMedium = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.25.sp + ), + bodySmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.4.sp + ), + labelLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp + ), + labelMedium = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) +) + diff --git a/app/src/main/java/com/project/newplanet/MainActivity.kt b/app/src/main/java/com/project/newplanet/MainActivity.kt deleted file mode 100644 index 2eb74e6..0000000 --- a/app/src/main/java/com/project/newplanet/MainActivity.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.project.newplanet - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.project.newplanet.ui.theme.NewPlanetTheme - -class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - setContent { - NewPlanetTheme { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - Greeting( - name = "Android", - modifier = Modifier.padding(innerPadding) - ) - } - } - } - } -} - -@Composable -fun Greeting(name: String, modifier: Modifier = Modifier) { - Text( - text = "Hello $name!", - modifier = modifier - ) -} - -@Preview(showBackground = true) -@Composable -fun GreetingPreview() { - NewPlanetTheme { - Greeting("Android") - } -} \ No newline at end of file diff --git a/app/src/main/java/com/project/newplanet/ui/theme/Color.kt b/app/src/main/java/com/project/newplanet/ui/theme/Color.kt deleted file mode 100644 index fc09976..0000000 --- a/app/src/main/java/com/project/newplanet/ui/theme/Color.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.project.newplanet.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) \ No newline at end of file diff --git a/app/src/main/java/com/project/newplanet/ui/theme/Theme.kt b/app/src/main/java/com/project/newplanet/ui/theme/Theme.kt deleted file mode 100644 index 97b5682..0000000 --- a/app/src/main/java/com/project/newplanet/ui/theme/Theme.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.project.newplanet.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 NewPlanetTheme( - 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 - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/project/newplanet/ui/theme/Type.kt b/app/src/main/java/com/project/newplanet/ui/theme/Type.kt deleted file mode 100644 index 9e6c2b3..0000000 --- a/app/src/main/java/com/project/newplanet/ui/theme/Type.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.project.newplanet.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 - ) - */ -) \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0d38447..59eea5c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,3 @@ - NewPlanet + Новая Планета \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 952b930..2be3a46 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,4 +3,6 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.compose) apply false + alias(libs.plugins.kotlin.serialization) apply false + alias(libs.plugins.hilt) apply false } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5d464bd..e13ffcf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,8 +6,20 @@ junit = "4.13.2" junitVersion = "1.3.0" espressoCore = "3.7.0" lifecycleRuntimeKtx = "2.9.4" +lifecycleViewmodel = "2.9.4" activityCompose = "1.11.0" composeBom = "2024.09.00" +hilt = "2.53" +hiltNavigationCompose = "1.2.0" +room = "2.6.1" +retrofit = "2.11.0" +okhttp = "4.12.0" +kotlinxSerialization = "1.7.3" +kotlinxSerializationConverter = "1.0.0" +navigation = "2.8.4" +coil = "2.7.0" +lottie = "6.1.0" +coroutines = "1.10.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -15,6 +27,7 @@ junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } +androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodel" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-ui = { group = "androidx.compose.ui", name = "ui" } @@ -25,8 +38,43 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +# Hilt +hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } +hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" } +hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" } + +# Room +room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } +room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } +room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } + +# Retrofit +retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" } +retrofit-kotlinx-serialization = { group = "com.jakewharton.retrofit", name = "retrofit2-kotlinx-serialization-converter", version.ref = "kotlinxSerializationConverter" } +okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } +okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } + +# Kotlinx Serialization +kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" } + +# Navigation +navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" } + +# Coil +coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } + +# Lottie +lottie-compose = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "lottie" } + +# Coroutines +kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" } +kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } + [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } +kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }