Initial commit2

This commit is contained in:
2025-12-13 13:11:27 +03:00
parent d0703ca008
commit 016a470680
72 changed files with 2540 additions and 157 deletions

189
README_NEW_PLANET.md Normal file
View File

@@ -0,0 +1,189 @@
# Новая Планета — Frontend (Android)
<div align="center">
<h3>Визуальное расписание для детей с РАС</h3>
<p>Мобильное приложение на Android с ИИ-помощником "Планета Земля"</p>
</div>
---
## 📱 О проекте
**Новая Планета** — 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<LoginResponse>
@GET("api/v1/schedules")
suspend fun getSchedules(
@Header("Authorization") token: String
): Response<List<Schedule>>
@POST("api/v1/ai/chat")
suspend fun chatWithAI(
@Header("Authorization") token: String,
@Body request: ChatRequest
): Response<ChatResponse>
}
```
---
## 🎨 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
---
## 👥 Команда
Разработано с ❤️ командой **Новая Планета**

View File

@@ -2,20 +2,27 @@ plugins {
alias(libs.plugins.android.application) alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.compose)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.hilt)
id("org.jetbrains.kotlin.kapt")
} }
android { android {
namespace = "com.project.newplanet" namespace = "com.novayaplaneta"
compileSdk = 36 compileSdk = 36
defaultConfig { defaultConfig {
applicationId = "com.project.newplanet" applicationId = "com.novayaplaneta"
minSdk = 30 minSdk = 26
targetSdk = 36 targetSdk = 36
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
} }
buildTypes { buildTypes {
@@ -28,27 +35,72 @@ android {
} }
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_17
} }
kotlinOptions { kotlinOptions {
jvmTarget = "11" jvmTarget = "17"
} }
buildFeatures { buildFeatures {
compose = true compose = true
} }
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
} }
dependencies { dependencies {
// Core Android
implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
// Compose
implementation(platform(libs.androidx.compose.bom)) implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui) implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3) 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) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)

View File

@@ -2,7 +2,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application <application
android:name=".NewPlanetApplication"
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"

View File

@@ -0,0 +1,55 @@
package com.novayaplaneta
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.ui.Modifier
import androidx.navigation.compose.rememberNavController
import com.novayaplaneta.ui.components.BottomNavigationBar
import com.novayaplaneta.ui.navigation.NewPlanetNavigation
import com.novayaplaneta.ui.theme.NewPlanetTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
NewPlanetTheme {
val navController = rememberNavController()
val currentRoute = navController.currentDestination?.route
Scaffold(
modifier = Modifier.fillMaxSize(),
bottomBar = {
BottomNavigationBar(
currentRoute = currentRoute,
onNavigate = { route ->
navController.navigate(route) {
popUpTo(navController.graph.startDestinationId) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
) { innerPadding ->
NewPlanetNavigation(
navController = navController,
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
)
}
}
}
}
}

View File

@@ -0,0 +1,8 @@
package com.novayaplaneta
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class NewPlanetApplication : Application()

View File

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

View File

@@ -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<List<ChatMessageEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertMessage(message: ChatMessageEntity)
@Query("DELETE FROM chat_messages WHERE userId = :userId")
suspend fun deleteMessagesByUserId(userId: String)
}

View File

@@ -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<List<RewardEntity>>
@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)
}

View File

@@ -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<List<ScheduleEntity>>
@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)
}

View File

@@ -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<List<TaskEntity>>
@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)
}

View File

@@ -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<UserEntity?>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: UserEntity)
@Query("DELETE FROM users")
suspend fun deleteAllUsers()
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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?
)

View File

@@ -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
)
}

View File

@@ -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
)
}

View File

@@ -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<Task> = 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
)
}

View File

@@ -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
)
}

View File

@@ -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
)
}

View File

@@ -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<LoginResponse>
@POST("api/v1/ai/chat")
suspend fun chatWithAI(
@Header("Authorization") token: String,
@Body request: ChatRequest
): Response<ChatResponse>
}

View File

@@ -0,0 +1,9 @@
package com.novayaplaneta.data.remote.dto
import kotlinx.serialization.Serializable
@Serializable
data class ChatRequest(
val message: String
)

View File

@@ -0,0 +1,9 @@
package com.novayaplaneta.data.remote.dto
import kotlinx.serialization.Serializable
@Serializable
data class ChatResponse(
val message: String
)

View File

@@ -0,0 +1,10 @@
package com.novayaplaneta.data.remote.dto
import kotlinx.serialization.Serializable
@Serializable
data class LoginRequest(
val email: String,
val password: String
)

View File

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

View File

@@ -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<String> {
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<List<ChatMessage>> {
return chatMessageDao.getMessagesByUserId(userId).map { messages ->
messages.map { it.toDomain() }
}
}
override suspend fun generateSchedule(userId: String, preferences: String): Result<String> {
// TODO: Implement schedule generation
return Result.failure(Exception("Not implemented"))
}
}

View File

@@ -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<User> {
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<User?> {
return userDao.getCurrentUser().map { it?.toDomain() }
}
override suspend fun saveUser(user: User) {
userDao.insertUser(user.toEntity())
}
}

View File

@@ -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<List<Reward>> {
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()))
}
}
}

View File

@@ -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<List<Schedule>> {
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
}
}

View File

@@ -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<List<Task>> {
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)
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}

View File

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

View File

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

View File

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

View File

@@ -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<Task>,
val date: LocalDateTime,
val createdAt: LocalDateTime,
val userId: String
)

View File

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

View File

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

View File

@@ -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<String>
fun getChatHistory(userId: String): Flow<List<ChatMessage>>
suspend fun generateSchedule(userId: String, preferences: String): Result<String>
}

View File

@@ -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<User>
suspend fun logout()
fun getCurrentUser(): Flow<User?>
suspend fun saveUser(user: User)
}

View File

@@ -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<List<Reward>>
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)
}

View File

@@ -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<List<Schedule>>
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)
}

View File

@@ -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<List<Task>>
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)
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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<List<Schedule>> {
return repository.getSchedules(userId)
}
}

View File

@@ -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<String> {
return repository.sendMessage(userId, message)
}
}

View File

@@ -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) }
)
}
}
}

View File

@@ -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()
}
}
}

View File

@@ -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("Отправить")
}
}
}
}
}

View File

@@ -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<AIUiState> = _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<com.novayaplaneta.domain.model.ChatMessage> = emptyList(),
val isLoading: Boolean = false,
val error: String? = null
)

View File

@@ -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
)
}
}
}
}
}
}
}
}
}

View File

@@ -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<RewardsUiState> = _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<com.novayaplaneta.domain.model.Reward> = emptyList(),
val isLoading: Boolean = false
)

View File

@@ -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
)
}
}
}
}
}
}
}
}
}

View File

@@ -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<ScheduleUiState> = _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<com.novayaplaneta.domain.model.Schedule> = emptyList(),
val isLoading: Boolean = false
)

View File

@@ -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("Выйти")
}
}
}
}

View File

@@ -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<SettingsUiState> = _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
)

View File

@@ -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) }
)
}
}
}
}
}
}
}
}

View File

@@ -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<TaskUiState> = _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<com.novayaplaneta.domain.model.Task> = emptyList(),
val isLoading: Boolean = false
)

View File

@@ -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)
}

View File

@@ -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<TimerUiState> = _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
)

View File

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

View File

@@ -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
)
}

View File

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

View File

@@ -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")
}
}

View File

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

View File

@@ -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
)
}

View File

@@ -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
)
*/
)

View File

@@ -1,3 +1,3 @@
<resources> <resources>
<string name="app_name">NewPlanet</string> <string name="app_name">Новая Планета</string>
</resources> </resources>

View File

@@ -3,4 +3,6 @@ plugins {
alias(libs.plugins.android.application) apply false alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.hilt) apply false
} }

View File

@@ -6,8 +6,20 @@ junit = "4.13.2"
junitVersion = "1.3.0" junitVersion = "1.3.0"
espressoCore = "3.7.0" espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.9.4" lifecycleRuntimeKtx = "2.9.4"
lifecycleViewmodel = "2.9.4"
activityCompose = "1.11.0" activityCompose = "1.11.0"
composeBom = "2024.09.00" 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] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } 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-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } 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-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-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" } 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-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" } 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] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", 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" }