Merge pull request 'фикс багов, интеграция с бэком+агентами' (#1) from fixesAndPreparation into master
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -21,7 +21,7 @@ import com.novayaplaneta.data.local.entity.UserEntity
|
||||
RewardEntity::class,
|
||||
ChatMessageEntity::class
|
||||
],
|
||||
version = 2,
|
||||
version = 4,
|
||||
exportSchema = false
|
||||
)
|
||||
abstract class NewPlanetDatabase : RoomDatabase() {
|
||||
|
||||
@@ -12,6 +12,7 @@ data class ScheduleEntity(
|
||||
val description: String?,
|
||||
val date: Long, // timestamp
|
||||
val createdAt: Long, // timestamp
|
||||
val userId: String
|
||||
val userId: String,
|
||||
val rewardId: String? = null
|
||||
)
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ fun ScheduleEntity.toDomain(tasks: List<Task> = emptyList()): Schedule {
|
||||
tasks = tasks,
|
||||
date = LocalDateTime.ofInstant(Instant.ofEpochMilli(date), ZoneId.systemDefault()),
|
||||
createdAt = LocalDateTime.ofInstant(Instant.ofEpochMilli(createdAt), ZoneId.systemDefault()),
|
||||
userId = userId
|
||||
userId = userId,
|
||||
rewardId = rewardId
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,7 +27,8 @@ fun Schedule.toEntity(): ScheduleEntity {
|
||||
description = description,
|
||||
date = date.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(),
|
||||
createdAt = createdAt.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(),
|
||||
userId = userId
|
||||
userId = userId,
|
||||
rewardId = rewardId
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
|
||||
data class ChatResponse(
|
||||
val response: String,
|
||||
val conversation_id: String,
|
||||
val tokens_used: Int,
|
||||
val tokens_used: Int? = null,
|
||||
val model: String
|
||||
)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import kotlinx.serialization.Serializable
|
||||
data class CreateScheduleRequest(
|
||||
val title: String,
|
||||
val date: String, // yyyy-MM-dd
|
||||
val description: String
|
||||
val description: String,
|
||||
val reward_id: String? = null
|
||||
)
|
||||
|
||||
|
||||
@@ -8,6 +8,6 @@ data class GenerateScheduleResponse(
|
||||
val schedule_id: String,
|
||||
val title: String,
|
||||
val tasks: List<JsonObject>,
|
||||
val tokens_used: Int
|
||||
val tokens_used: Int? = null
|
||||
)
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ data class ScheduleDto(
|
||||
val user_id: String,
|
||||
val created_at: String, // ISO date
|
||||
val updated_at: String, // ISO date
|
||||
val tasks: List<TaskDto> = emptyList()
|
||||
val tasks: List<TaskDto> = emptyList(),
|
||||
val reward_id: String? = null
|
||||
)
|
||||
|
||||
// TaskDto moved to separate file
|
||||
|
||||
@@ -31,7 +31,8 @@ fun ScheduleDto.toDomain(): Schedule {
|
||||
tasks = tasks.map { it.toDomain() },
|
||||
date = dateLocalDateTime,
|
||||
createdAt = createdAtDateTime,
|
||||
userId = user_id
|
||||
userId = user_id,
|
||||
rewardId = reward_id
|
||||
)
|
||||
}
|
||||
|
||||
@@ -44,7 +45,8 @@ fun Schedule.toCreateRequest(): com.novayaplaneta.data.remote.dto.CreateSchedule
|
||||
return com.novayaplaneta.data.remote.dto.CreateScheduleRequest(
|
||||
title = title,
|
||||
date = dateString,
|
||||
description = description ?: ""
|
||||
description = description ?: "",
|
||||
reward_id = rewardId
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,13 @@ class AuthRepositoryImpl @Inject constructor(
|
||||
// Сохраняем токены
|
||||
tokenManager.saveTokens(tokenResponse.access_token, tokenResponse.refresh_token)
|
||||
|
||||
// Убеждаемся, что токен действительно сохранен перед вызовом getMe()
|
||||
// Это гарантирует, что DataStore завершил сохранение
|
||||
val savedToken = tokenManager.getAccessToken()
|
||||
if (savedToken == null) {
|
||||
return Result.failure(Exception("Failed to save access token"))
|
||||
}
|
||||
|
||||
// Получаем информацию о пользователе
|
||||
val meResponse = authApi.getMe()
|
||||
if (meResponse.isSuccessful && meResponse.body() != null) {
|
||||
@@ -73,11 +80,14 @@ class AuthRepositoryImpl @Inject constructor(
|
||||
saveUser(user)
|
||||
Result.success(user)
|
||||
} else {
|
||||
Result.failure(Exception("Failed to get user info"))
|
||||
val errorCode = meResponse.code()
|
||||
val errorBody = meResponse.errorBody()?.string() ?: "Unknown error"
|
||||
Result.failure(Exception("Failed to get user info: HTTP $errorCode - $errorBody"))
|
||||
}
|
||||
} else {
|
||||
val errorBody = response.errorBody()?.string() ?: "Login failed"
|
||||
Result.failure(Exception(errorBody))
|
||||
val errorCode = response.code()
|
||||
val errorBody = response.errorBody()?.string() ?: "Unknown error"
|
||||
Result.failure(Exception("Login failed: HTTP $errorCode - $errorBody"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
|
||||
@@ -26,7 +26,9 @@ object DatabaseModule {
|
||||
context,
|
||||
NewPlanetDatabase::class.java,
|
||||
"newplanet_database"
|
||||
).build()
|
||||
)
|
||||
.fallbackToDestructiveMigration() // Пересоздает БД при изменении схемы (для разработки)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -9,6 +9,7 @@ data class Schedule(
|
||||
val tasks: List<Task>,
|
||||
val date: LocalDateTime,
|
||||
val createdAt: LocalDateTime,
|
||||
val userId: String
|
||||
val userId: String,
|
||||
val rewardId: String? = null
|
||||
)
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@ package com.novayaplaneta.ui.navigation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navArgument
|
||||
import com.novayaplaneta.ui.screens.ai.AIScreen
|
||||
import com.novayaplaneta.ui.screens.auth.ForgotPasswordScreen
|
||||
import com.novayaplaneta.ui.screens.auth.LoginScreen
|
||||
@@ -42,8 +44,17 @@ fun NewPlanetNavigation(
|
||||
composable("schedule") {
|
||||
ScheduleScreen(navController = navController)
|
||||
}
|
||||
composable("tasks") {
|
||||
TaskScreen()
|
||||
composable(
|
||||
route = "tasks/{scheduleId}",
|
||||
arguments = listOf(
|
||||
navArgument("scheduleId") { type = NavType.StringType }
|
||||
)
|
||||
) { backStackEntry ->
|
||||
val scheduleId = backStackEntry.arguments?.getString("scheduleId")
|
||||
TaskScreen(
|
||||
scheduleId = scheduleId,
|
||||
navController = navController
|
||||
)
|
||||
}
|
||||
composable("timer") {
|
||||
TimerScreen()
|
||||
|
||||
@@ -186,7 +186,7 @@ fun AIScreen(
|
||||
// Заголовок чата
|
||||
val titleSize = (screenHeightDp * 0.04f).toInt().coerceIn(32, 52).sp
|
||||
Text(
|
||||
text = "Чат с Землей",
|
||||
text = "Помощник Земля",
|
||||
fontSize = titleSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.Black,
|
||||
|
||||
@@ -22,6 +22,7 @@ class AIViewModel @Inject constructor(
|
||||
|
||||
private var conversationId: String? = null
|
||||
private var currentUserId: String? = null
|
||||
private var sendMessageJob: kotlinx.coroutines.Job? = null
|
||||
|
||||
init {
|
||||
loadUserId()
|
||||
@@ -59,7 +60,13 @@ class AIViewModel @Inject constructor(
|
||||
|
||||
fun sendMessage(message: String) {
|
||||
val userId = currentUserId ?: return
|
||||
viewModelScope.launch {
|
||||
|
||||
// Предотвращаем повторные отправки
|
||||
if (_uiState.value.isLoading || sendMessageJob?.isActive == true) {
|
||||
return
|
||||
}
|
||||
|
||||
sendMessageJob = viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true, error = null)
|
||||
|
||||
aiRepository.sendMessage(userId, message, conversationId).fold(
|
||||
@@ -70,6 +77,34 @@ class AIViewModel @Inject constructor(
|
||||
loadChatHistory(userId)
|
||||
_uiState.value = _uiState.value.copy(isLoading = false)
|
||||
},
|
||||
onFailure = { error ->
|
||||
val errorMessage = error.message ?: "Ошибка отправки сообщения"
|
||||
|
||||
// Если ошибка связана с дубликатом conversation_id, сбрасываем его
|
||||
if (errorMessage.contains("duplicate key", ignoreCase = true) ||
|
||||
errorMessage.contains("conversation_id", ignoreCase = true)) {
|
||||
conversationId = null
|
||||
// Пытаемся отправить сообщение снова без conversation_id
|
||||
retrySendMessage(userId, message)
|
||||
} else {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
error = errorMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun retrySendMessage(userId: String, message: String) {
|
||||
viewModelScope.launch {
|
||||
aiRepository.sendMessage(userId, message, null).fold(
|
||||
onSuccess = { response ->
|
||||
conversationId = response.conversationId
|
||||
loadChatHistory(userId)
|
||||
_uiState.value = _uiState.value.copy(isLoading = false)
|
||||
},
|
||||
onFailure = { error ->
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.novayaplaneta.ui.screens.schedule
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -21,6 +22,9 @@ 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.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -212,6 +216,7 @@ fun ScheduleScreen(
|
||||
} else {
|
||||
SchedulesListSection(
|
||||
schedules = uiState.schedules,
|
||||
availableRewards = uiState.availableRewards,
|
||||
dateOnly = dateOnly,
|
||||
dayOfWeek = dayOfWeek,
|
||||
dateCardColor = dateCardColor,
|
||||
@@ -220,7 +225,7 @@ fun ScheduleScreen(
|
||||
onAddClick = { viewModel.showAddDialog() },
|
||||
onGenerateClick = { viewModel.showGenerateDialog() },
|
||||
onScheduleClick = { scheduleId ->
|
||||
// TODO: Navigate to schedule details
|
||||
navController?.navigate("tasks/$scheduleId")
|
||||
},
|
||||
onDeleteClick = { scheduleId ->
|
||||
viewModel.deleteSchedule(scheduleId)
|
||||
@@ -236,9 +241,12 @@ fun ScheduleScreen(
|
||||
title = uiState.newScheduleTitle,
|
||||
description = uiState.newScheduleDescription,
|
||||
date = uiState.newScheduleDate,
|
||||
selectedRewardId = uiState.selectedRewardId,
|
||||
availableRewards = uiState.availableRewards,
|
||||
onTitleChange = { viewModel.updateNewScheduleTitle(it) },
|
||||
onDescriptionChange = { viewModel.updateNewScheduleDescription(it) },
|
||||
onDateChange = { viewModel.updateNewScheduleDate(it) },
|
||||
onRewardSelected = { viewModel.updateSelectedReward(it) },
|
||||
onCreate = { viewModel.createScheduleFromDialog() },
|
||||
onDismiss = { viewModel.hideAddDialog() },
|
||||
accentGreen = accentGreen,
|
||||
@@ -308,6 +316,7 @@ fun NavItem(
|
||||
@Composable
|
||||
fun SchedulesListSection(
|
||||
schedules: List<com.novayaplaneta.domain.model.Schedule>,
|
||||
availableRewards: List<com.novayaplaneta.domain.model.Reward>,
|
||||
dateOnly: String,
|
||||
dayOfWeek: String,
|
||||
dateCardColor: Color,
|
||||
@@ -403,8 +412,12 @@ fun SchedulesListSection(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
items(schedules) { schedule ->
|
||||
val reward = schedule.rewardId?.let { rewardId ->
|
||||
availableRewards.find { it.id == rewardId }
|
||||
}
|
||||
ScheduleCard(
|
||||
schedule = schedule,
|
||||
reward = reward,
|
||||
screenHeightDp = screenHeightDp,
|
||||
accentGreen = accentGreen,
|
||||
dateCardColor = dateCardColor,
|
||||
@@ -420,6 +433,7 @@ fun SchedulesListSection(
|
||||
@Composable
|
||||
fun ScheduleCard(
|
||||
schedule: com.novayaplaneta.domain.model.Schedule,
|
||||
reward: com.novayaplaneta.domain.model.Reward?,
|
||||
screenHeightDp: Int,
|
||||
accentGreen: Color,
|
||||
dateCardColor: Color,
|
||||
@@ -478,6 +492,11 @@ fun ScheduleCard(
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (schedule.tasks.isNotEmpty()) {
|
||||
Text(
|
||||
text = "Задач: ${schedule.tasks.size}",
|
||||
@@ -485,18 +504,48 @@ fun ScheduleCard(
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
|
||||
// Отображение награды
|
||||
reward?.let {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(Color(0xFFFFD700).copy(alpha = 0.3f)) // Золотистый цвет для награды
|
||||
.padding(horizontal = 12.dp, vertical = 6.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Star,
|
||||
contentDescription = "Награда",
|
||||
tint = Color(0xFFFFB300), // Не сильно оранжевый, золотистый
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text(
|
||||
text = it.title,
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color(0xFFFF8C00) // Не сильно оранжевый
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CreateScheduleDialog(
|
||||
title: String,
|
||||
description: String,
|
||||
date: java.time.LocalDate?,
|
||||
selectedRewardId: String?,
|
||||
availableRewards: List<com.novayaplaneta.domain.model.Reward>,
|
||||
onTitleChange: (String) -> Unit,
|
||||
onDescriptionChange: (String) -> Unit,
|
||||
onDateChange: (java.time.LocalDate) -> Unit,
|
||||
onRewardSelected: (String?) -> Unit,
|
||||
onCreate: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
accentGreen: Color,
|
||||
@@ -548,6 +597,76 @@ fun CreateScheduleDialog(
|
||||
enabled = !isLoading
|
||||
)
|
||||
|
||||
// Выбор награды
|
||||
if (availableRewards.isNotEmpty()) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Награда (опционально)",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color.Black
|
||||
)
|
||||
|
||||
// Выпадающий список наград
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = !expanded },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = availableRewards.find { it.id == selectedRewardId }?.title ?: "Не выбрано",
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||
modifier = Modifier
|
||||
.menuAnchor()
|
||||
.fillMaxWidth(),
|
||||
enabled = !isLoading,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = accentGreen,
|
||||
focusedLabelColor = accentGreen
|
||||
)
|
||||
)
|
||||
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
// Опция "Не выбрано"
|
||||
DropdownMenuItem(
|
||||
text = { Text("Не выбрано") },
|
||||
onClick = {
|
||||
onRewardSelected(null)
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
|
||||
availableRewards.forEach { reward ->
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Column {
|
||||
Text(reward.title, fontWeight = FontWeight.Bold)
|
||||
reward.description?.let {
|
||||
Text(it, fontSize = 12.sp, color = Color.Gray)
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = {
|
||||
onRewardSelected(reward.id)
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Кнопки внизу
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
||||
@@ -2,11 +2,13 @@ package com.novayaplaneta.ui.screens.schedule
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.novayaplaneta.domain.model.Reward
|
||||
import com.novayaplaneta.domain.model.Schedule
|
||||
import com.novayaplaneta.domain.repository.AuthRepository
|
||||
import com.novayaplaneta.domain.usecase.CreateScheduleUseCase
|
||||
import com.novayaplaneta.domain.usecase.DeleteScheduleUseCase
|
||||
import com.novayaplaneta.domain.usecase.GenerateScheduleUseCase
|
||||
import com.novayaplaneta.domain.usecase.GetRewardsUseCase
|
||||
import com.novayaplaneta.domain.usecase.GetSchedulesUseCase
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -29,6 +31,7 @@ class ScheduleViewModel @Inject constructor(
|
||||
private val createScheduleUseCase: CreateScheduleUseCase,
|
||||
private val deleteScheduleUseCase: DeleteScheduleUseCase,
|
||||
private val generateScheduleUseCase: GenerateScheduleUseCase,
|
||||
private val getRewardsUseCase: GetRewardsUseCase,
|
||||
private val authRepository: AuthRepository
|
||||
) : ViewModel() {
|
||||
|
||||
@@ -37,6 +40,25 @@ class ScheduleViewModel @Inject constructor(
|
||||
|
||||
init {
|
||||
loadSchedules()
|
||||
loadRewards()
|
||||
}
|
||||
|
||||
private fun loadRewards() {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val user = authRepository.getCurrentUser().first()
|
||||
val userId = user?.id ?: return@launch
|
||||
|
||||
val loadResult = getRewardsUseCase.loadRewards()
|
||||
if (loadResult.isSuccess) {
|
||||
getRewardsUseCase(userId).collect { rewards ->
|
||||
_uiState.value = _uiState.value.copy(availableRewards = rewards)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Игнорируем ошибки загрузки наград
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadSchedules(scheduleDate: String? = null) {
|
||||
@@ -90,7 +112,8 @@ class ScheduleViewModel @Inject constructor(
|
||||
showAddDialog = true,
|
||||
newScheduleTitle = "",
|
||||
newScheduleDescription = "",
|
||||
newScheduleDate = LocalDate.now()
|
||||
newScheduleDate = LocalDate.now(),
|
||||
selectedRewardId = null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -99,10 +122,15 @@ class ScheduleViewModel @Inject constructor(
|
||||
showAddDialog = false,
|
||||
newScheduleTitle = "",
|
||||
newScheduleDescription = "",
|
||||
newScheduleDate = null
|
||||
newScheduleDate = null,
|
||||
selectedRewardId = null
|
||||
)
|
||||
}
|
||||
|
||||
fun updateSelectedReward(rewardId: String?) {
|
||||
_uiState.value = _uiState.value.copy(selectedRewardId = rewardId)
|
||||
}
|
||||
|
||||
fun updateNewScheduleTitle(title: String) {
|
||||
_uiState.value = _uiState.value.copy(newScheduleTitle = title)
|
||||
}
|
||||
@@ -121,12 +149,13 @@ class ScheduleViewModel @Inject constructor(
|
||||
createSchedule(
|
||||
title = state.newScheduleTitle,
|
||||
description = state.newScheduleDescription,
|
||||
date = state.newScheduleDate
|
||||
date = state.newScheduleDate,
|
||||
rewardId = state.selectedRewardId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun createSchedule(title: String, description: String, date: LocalDate) {
|
||||
fun createSchedule(title: String, description: String, date: LocalDate, rewardId: String? = null) {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
|
||||
|
||||
@@ -141,7 +170,8 @@ class ScheduleViewModel @Inject constructor(
|
||||
tasks = emptyList(),
|
||||
date = date.atStartOfDay(),
|
||||
createdAt = java.time.LocalDateTime.now(),
|
||||
userId = userId
|
||||
userId = userId,
|
||||
rewardId = rewardId
|
||||
)
|
||||
|
||||
val result = createScheduleUseCase(schedule)
|
||||
@@ -261,6 +291,8 @@ data class ScheduleUiState(
|
||||
val newScheduleTitle: String = "",
|
||||
val newScheduleDescription: String = "",
|
||||
val newScheduleDate: LocalDate? = null,
|
||||
val selectedRewardId: String? = null,
|
||||
val availableRewards: List<Reward> = emptyList(),
|
||||
val showGenerateDialog: Boolean = false,
|
||||
val generateChildAge: Int = 5,
|
||||
val generatePreferences: List<String> = emptyList(),
|
||||
|
||||
@@ -1,23 +1,58 @@
|
||||
package com.novayaplaneta.ui.screens.task
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material.icons.filled.Pause
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.NavController
|
||||
import com.novayaplaneta.ui.theme.LoginBackgroundTurquoise
|
||||
import com.novayaplaneta.ui.theme.LoginGreenAccent
|
||||
import com.novayaplaneta.ui.theme.LoginInputLightBlue
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun TaskScreen(
|
||||
scheduleId: String? = null,
|
||||
@@ -26,6 +61,13 @@ fun TaskScreen(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val configuration = LocalConfiguration.current
|
||||
val screenHeightDp = configuration.screenHeightDp
|
||||
|
||||
// Цвета из autism-friendly палитры
|
||||
val backgroundColor = LoginBackgroundTurquoise
|
||||
val accentGreen = LoginGreenAccent
|
||||
val dateCardColor = LoginInputLightBlue
|
||||
|
||||
// Загружаем задачи при открытии экрана
|
||||
LaunchedEffect(scheduleId) {
|
||||
@@ -34,65 +76,186 @@ fun TaskScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Задания") },
|
||||
actions = {
|
||||
if (scheduleId != null) {
|
||||
IconButton(onClick = { /* TODO: Show create task dialog */ }) {
|
||||
Icon(Icons.Default.Add, contentDescription = "Добавить задачу")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp)
|
||||
.background(color = backgroundColor)
|
||||
) {
|
||||
// Отображение ошибок
|
||||
uiState.errorMessage?.let { error ->
|
||||
Card(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||
)
|
||||
.fillMaxSize()
|
||||
.padding(24.dp)
|
||||
) {
|
||||
Text(
|
||||
text = error,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.onErrorContainer
|
||||
// Верхняя панель с кнопкой назад и кнопкой добавления
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Кнопка назад
|
||||
if (navController != null) {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(
|
||||
Icons.Filled.ArrowBack,
|
||||
contentDescription = "Назад",
|
||||
tint = accentGreen,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Spacer(modifier = Modifier.size(48.dp))
|
||||
}
|
||||
|
||||
// Заголовок
|
||||
Text(
|
||||
text = "Задания",
|
||||
fontSize = (screenHeightDp * 0.04f).toInt().coerceIn(32, 48).sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = accentGreen
|
||||
)
|
||||
|
||||
// Кнопка добавления
|
||||
if (scheduleId != null) {
|
||||
FloatingActionButton(
|
||||
onClick = { viewModel.showAddDialog() },
|
||||
modifier = Modifier.size(56.dp),
|
||||
containerColor = accentGreen,
|
||||
contentColor = Color.White
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Add,
|
||||
contentDescription = "Добавить задачу",
|
||||
modifier = Modifier.size(28.dp)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Spacer(modifier = Modifier.size(56.dp))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// Контент
|
||||
if (uiState.isLoading && uiState.tasks.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
CircularProgressIndicator(color = accentGreen)
|
||||
}
|
||||
} else if (uiState.tasks.isEmpty() && !uiState.isLoading) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text("Нет задач")
|
||||
Text(
|
||||
text = "Нет задач",
|
||||
fontSize = 24.sp,
|
||||
color = Color.Gray,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
items(uiState.tasks) { task ->
|
||||
// Получаем состояние таймера из Flow
|
||||
val timerStates by viewModel.timerStates.collectAsState()
|
||||
val timerState = timerStates[task.id]
|
||||
|
||||
TaskCard(
|
||||
task = task,
|
||||
timerState = timerState,
|
||||
accentGreen = accentGreen,
|
||||
dateCardColor = dateCardColor,
|
||||
screenHeightDp = screenHeightDp,
|
||||
onStartTimer = { duration ->
|
||||
viewModel.startTimer(task.id, duration)
|
||||
},
|
||||
onPauseTimer = {
|
||||
viewModel.pauseTimer(task.id)
|
||||
},
|
||||
onResumeTimer = {
|
||||
viewModel.resumeTimer(task.id)
|
||||
},
|
||||
onStopTimer = {
|
||||
viewModel.stopTimer(task.id)
|
||||
},
|
||||
onDelete = {
|
||||
viewModel.deleteTask(task.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Диалог создания задачи/награды
|
||||
if (uiState.showAddDialog) {
|
||||
CreateTaskOrRewardDialog(
|
||||
dialogMode = uiState.dialogMode,
|
||||
taskTitle = uiState.newTaskTitle,
|
||||
taskDescription = uiState.newTaskDescription,
|
||||
taskDuration = uiState.newTaskDuration,
|
||||
rewardTitle = uiState.newRewardTitle,
|
||||
rewardDescription = uiState.newRewardDescription,
|
||||
rewardPoints = uiState.newRewardPoints,
|
||||
onModeChange = { viewModel.setDialogMode(it) },
|
||||
onTaskTitleChange = { viewModel.updateNewTaskTitle(it) },
|
||||
onTaskDescriptionChange = { viewModel.updateNewTaskDescription(it) },
|
||||
onTaskDurationChange = { viewModel.updateNewTaskDuration(it) },
|
||||
onRewardTitleChange = { viewModel.updateNewRewardTitle(it) },
|
||||
onRewardDescriptionChange = { viewModel.updateNewRewardDescription(it) },
|
||||
onRewardPointsChange = { viewModel.updateNewRewardPoints(it) },
|
||||
onCreate = {
|
||||
when (uiState.dialogMode) {
|
||||
DialogMode.TASK -> {
|
||||
scheduleId?.let { id ->
|
||||
viewModel.createTask(id)
|
||||
}
|
||||
}
|
||||
|
||||
DialogMode.REWARD -> {
|
||||
viewModel.createReward()
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismiss = { viewModel.hideAddDialog() },
|
||||
accentGreen = accentGreen,
|
||||
screenHeightDp = screenHeightDp,
|
||||
isLoading = uiState.isLoading,
|
||||
scheduleId = scheduleId
|
||||
)
|
||||
}
|
||||
|
||||
// Snackbar для ошибок
|
||||
uiState.errorMessage?.let { error ->
|
||||
LaunchedEffect(error) {
|
||||
// Можно добавить SnackbarHost если нужно
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TaskCard(
|
||||
task: com.novayaplaneta.domain.model.Task,
|
||||
timerState: TimerState?,
|
||||
accentGreen: Color,
|
||||
dateCardColor: Color,
|
||||
screenHeightDp: Int,
|
||||
onStartTimer: (Int) -> Unit,
|
||||
onPauseTimer: () -> Unit,
|
||||
onResumeTimer: () -> Unit,
|
||||
onStopTimer: () -> Unit,
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = dateCardColor)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -101,45 +264,445 @@ fun TaskScreen(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = task.title,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.Black
|
||||
)
|
||||
|
||||
task.description?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
fontSize = 16.sp,
|
||||
color = Color.Black
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
task.duration?.let {
|
||||
Text(
|
||||
text = "Длительность: $it мин",
|
||||
style = MaterialTheme.typography.bodySmall
|
||||
fontSize = 14.sp,
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
|
||||
if (task.completed) {
|
||||
Text(
|
||||
text = "✓ Выполнено",
|
||||
fontSize = 14.sp,
|
||||
color = accentGreen,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(
|
||||
checked = task.completed,
|
||||
onCheckedChange = { completed ->
|
||||
viewModel.completeTask(task.id, completed)
|
||||
}
|
||||
)
|
||||
IconButton(onClick = { viewModel.deleteTask(task.id) }) {
|
||||
|
||||
// Таймер или кнопка удаления
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
if (task.completed) {
|
||||
// Показываем только иконку выполнения
|
||||
Icon(
|
||||
Icons.Default.Delete,
|
||||
Icons.Filled.PlayArrow,
|
||||
contentDescription = "Выполнено",
|
||||
tint = accentGreen,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
} else if (timerState != null) {
|
||||
// Показываем визуальный таймер
|
||||
TaskTimer(
|
||||
timerState = timerState,
|
||||
accentGreen = accentGreen,
|
||||
onPause = onPauseTimer,
|
||||
onResume = onResumeTimer,
|
||||
onStop = onStopTimer
|
||||
)
|
||||
} else {
|
||||
// Показываем кнопку запуска таймера
|
||||
IconButton(
|
||||
onClick = {
|
||||
task.duration?.let { onStartTimer(it) }
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.PlayArrow,
|
||||
contentDescription = "Запустить таймер",
|
||||
tint = accentGreen,
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(
|
||||
Icons.Filled.Delete,
|
||||
contentDescription = "Удалить",
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
tint = Color.Red,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TaskTimer(
|
||||
timerState: TimerState,
|
||||
accentGreen: Color,
|
||||
onPause: () -> Unit,
|
||||
onResume: () -> Unit,
|
||||
onStop: () -> Unit
|
||||
) {
|
||||
val totalSeconds = timerState.durationMinutes * 60
|
||||
val progress = if (totalSeconds > 0) {
|
||||
timerState.remainingSeconds.toFloat() / totalSeconds
|
||||
} else 0f
|
||||
|
||||
val timeText = formatTime(timerState.remainingSeconds)
|
||||
|
||||
Box(
|
||||
modifier = Modifier.size(100.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
val strokeWidth = 10.dp.toPx()
|
||||
val radius = size.minDimension / 2 - strokeWidth / 2
|
||||
|
||||
// Фоновый круг
|
||||
drawCircle(
|
||||
color = Color.Gray.copy(alpha = 0.2f),
|
||||
radius = radius,
|
||||
style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
|
||||
)
|
||||
|
||||
// Прогресс таймера (обратный - показываем оставшееся время)
|
||||
val sweepAngle = 360f * progress
|
||||
|
||||
drawArc(
|
||||
color = accentGreen,
|
||||
startAngle = -90f,
|
||||
sweepAngle = sweepAngle,
|
||||
useCenter = false,
|
||||
style = Stroke(width = strokeWidth, cap = StrokeCap.Round)
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.padding(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = timeText,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.Black
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
) {
|
||||
if (timerState.isRunning) {
|
||||
IconButton(
|
||||
onClick = onPause,
|
||||
modifier = Modifier.size(28.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.Pause,
|
||||
contentDescription = "Пауза",
|
||||
tint = accentGreen,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
IconButton(
|
||||
onClick = onResume,
|
||||
modifier = Modifier.size(28.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Filled.PlayArrow,
|
||||
contentDescription = "Продолжить",
|
||||
tint = accentGreen,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatTime(seconds: Int): String {
|
||||
val minutes = seconds / 60
|
||||
val remainingSeconds = seconds % 60
|
||||
return String.format("%02d:%02d", minutes, remainingSeconds)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CreateTaskOrRewardDialog(
|
||||
dialogMode: DialogMode,
|
||||
taskTitle: String,
|
||||
taskDescription: String,
|
||||
taskDuration: String,
|
||||
rewardTitle: String,
|
||||
rewardDescription: String,
|
||||
rewardPoints: Int,
|
||||
onModeChange: (DialogMode) -> Unit,
|
||||
onTaskTitleChange: (String) -> Unit,
|
||||
onTaskDescriptionChange: (String) -> Unit,
|
||||
onTaskDurationChange: (String) -> Unit,
|
||||
onRewardTitleChange: (String) -> Unit,
|
||||
onRewardDescriptionChange: (String) -> Unit,
|
||||
onRewardPointsChange: (Int) -> Unit,
|
||||
onCreate: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
accentGreen: Color,
|
||||
screenHeightDp: Int,
|
||||
isLoading: Boolean,
|
||||
scheduleId: String?
|
||||
) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(0.9f)
|
||||
.wrapContentHeight(),
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = Color.White
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Переключатель режимов
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// Кнопка "Задача"
|
||||
Button(
|
||||
onClick = { onModeChange(DialogMode.TASK) },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(48.dp),
|
||||
enabled = scheduleId != null && !isLoading,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (dialogMode == DialogMode.TASK) accentGreen else Color.LightGray,
|
||||
contentColor = if (dialogMode == DialogMode.TASK) Color.White else Color.Black,
|
||||
disabledContainerColor = Color.LightGray,
|
||||
disabledContentColor = Color.Gray
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Задача",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
|
||||
// Кнопка "Награда"
|
||||
Button(
|
||||
onClick = { onModeChange(DialogMode.REWARD) },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(48.dp),
|
||||
enabled = !isLoading,
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (dialogMode == DialogMode.REWARD) accentGreen else Color.LightGray,
|
||||
contentColor = if (dialogMode == DialogMode.REWARD) Color.White else Color.Black
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = "Награда",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Заголовок
|
||||
val titleSize = (screenHeightDp * 0.03f).toInt().coerceIn(24, 36).sp
|
||||
Text(
|
||||
text = if (dialogMode == DialogMode.TASK) "Создать задачу" else "Создать награду",
|
||||
fontSize = titleSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.Black
|
||||
)
|
||||
|
||||
when (dialogMode) {
|
||||
DialogMode.TASK -> {
|
||||
// Поле названия задачи
|
||||
OutlinedTextField(
|
||||
value = taskTitle,
|
||||
onValueChange = onTaskTitleChange,
|
||||
label = { Text("Название") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = !isLoading,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = accentGreen,
|
||||
focusedLabelColor = accentGreen
|
||||
)
|
||||
)
|
||||
|
||||
// Поле описания задачи
|
||||
OutlinedTextField(
|
||||
value = taskDescription,
|
||||
onValueChange = onTaskDescriptionChange,
|
||||
label = { Text("Описание") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
maxLines = 3,
|
||||
enabled = !isLoading,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = accentGreen,
|
||||
focusedLabelColor = accentGreen
|
||||
)
|
||||
)
|
||||
|
||||
// Поле длительности
|
||||
OutlinedTextField(
|
||||
value = taskDuration,
|
||||
onValueChange = { newValue ->
|
||||
if (newValue.all { it.isDigit() }) {
|
||||
onTaskDurationChange(newValue)
|
||||
}
|
||||
},
|
||||
label = { Text("Длительность (минуты)") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = !isLoading,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = accentGreen,
|
||||
focusedLabelColor = accentGreen
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
DialogMode.REWARD -> {
|
||||
// Поле названия награды
|
||||
OutlinedTextField(
|
||||
value = rewardTitle,
|
||||
onValueChange = onRewardTitleChange,
|
||||
label = { Text("Название") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = !isLoading,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = accentGreen,
|
||||
focusedLabelColor = accentGreen
|
||||
)
|
||||
)
|
||||
|
||||
// Поле описания награды
|
||||
OutlinedTextField(
|
||||
value = rewardDescription,
|
||||
onValueChange = onRewardDescriptionChange,
|
||||
label = { Text("Описание") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
maxLines = 3,
|
||||
enabled = !isLoading,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = accentGreen,
|
||||
focusedLabelColor = accentGreen
|
||||
)
|
||||
)
|
||||
|
||||
// Поле баллов
|
||||
OutlinedTextField(
|
||||
value = rewardPoints.toString(),
|
||||
onValueChange = { newValue ->
|
||||
if (newValue.all { it.isDigit() }) {
|
||||
onRewardPointsChange(newValue.toIntOrNull() ?: 10)
|
||||
}
|
||||
},
|
||||
label = { Text("Баллы") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = !isLoading,
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = accentGreen,
|
||||
focusedLabelColor = accentGreen
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Кнопки внизу
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Кнопка "Отмена"
|
||||
Button(
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color.LightGray,
|
||||
contentColor = Color.Black
|
||||
),
|
||||
enabled = !isLoading
|
||||
) {
|
||||
val buttonTextSize = (screenHeightDp * 0.022f).toInt().coerceIn(18, 26).sp
|
||||
Text(
|
||||
text = "Отмена",
|
||||
fontSize = buttonTextSize,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
|
||||
// Кнопка "Создать"
|
||||
Button(
|
||||
onClick = onCreate,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(56.dp),
|
||||
enabled = when (dialogMode) {
|
||||
DialogMode.TASK -> taskTitle.isNotBlank() && !isLoading
|
||||
DialogMode.REWARD -> rewardTitle.isNotBlank() && !isLoading
|
||||
},
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = accentGreen,
|
||||
contentColor = Color.White,
|
||||
disabledContainerColor = Color.LightGray,
|
||||
disabledContentColor = Color.Gray
|
||||
)
|
||||
) {
|
||||
if (isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = Color.White
|
||||
)
|
||||
} else {
|
||||
val buttonTextSize =
|
||||
(screenHeightDp * 0.022f).toInt().coerceIn(18, 26).sp
|
||||
Text(
|
||||
text = "Создать",
|
||||
fontSize = buttonTextSize,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,29 @@ package com.novayaplaneta.ui.screens.task
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.novayaplaneta.data.local.TimerStateManager
|
||||
import com.novayaplaneta.data.local.SavedTimerState
|
||||
import com.novayaplaneta.domain.model.Reward
|
||||
import com.novayaplaneta.domain.model.Task
|
||||
import com.novayaplaneta.domain.repository.AuthRepository
|
||||
import com.novayaplaneta.domain.usecase.ClaimRewardUseCase
|
||||
import com.novayaplaneta.domain.usecase.CompleteTaskUseCase
|
||||
import com.novayaplaneta.domain.usecase.CreateRewardUseCase
|
||||
import com.novayaplaneta.domain.usecase.CreateTaskUseCase
|
||||
import com.novayaplaneta.domain.usecase.DeleteTaskUseCase
|
||||
import com.novayaplaneta.domain.usecase.GetSchedulesUseCase
|
||||
import com.novayaplaneta.domain.usecase.GetTaskByIdUseCase
|
||||
import com.novayaplaneta.domain.usecase.GetTasksUseCase
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.LocalDateTime
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -21,14 +32,23 @@ class TaskViewModel @Inject constructor(
|
||||
private val getTasksUseCase: GetTasksUseCase,
|
||||
private val getTaskByIdUseCase: GetTaskByIdUseCase,
|
||||
private val createTaskUseCase: CreateTaskUseCase,
|
||||
private val createRewardUseCase: CreateRewardUseCase,
|
||||
private val deleteTaskUseCase: DeleteTaskUseCase,
|
||||
private val completeTaskUseCase: CompleteTaskUseCase
|
||||
private val completeTaskUseCase: CompleteTaskUseCase,
|
||||
private val claimRewardUseCase: ClaimRewardUseCase,
|
||||
private val getSchedulesUseCase: GetSchedulesUseCase,
|
||||
private val authRepository: AuthRepository,
|
||||
private val timerStateManager: TimerStateManager
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(TaskUiState())
|
||||
val uiState: StateFlow<TaskUiState> = _uiState.asStateFlow()
|
||||
|
||||
private val _timerStates = MutableStateFlow<Map<String, TimerState>>(emptyMap())
|
||||
val timerStates: StateFlow<Map<String, TimerState>> = _timerStates.asStateFlow()
|
||||
|
||||
private var currentScheduleId: String? = null
|
||||
private val timerJobs = mutableMapOf<String, Job>()
|
||||
|
||||
fun loadTasks(scheduleId: String) {
|
||||
currentScheduleId = scheduleId
|
||||
@@ -36,6 +56,24 @@ class TaskViewModel @Inject constructor(
|
||||
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
|
||||
|
||||
try {
|
||||
// Загружаем сохраненные состояния таймеров
|
||||
val savedStates = timerStateManager.getAllTimerStates()
|
||||
savedStates.forEach { (taskId, savedState) ->
|
||||
val timerState = TimerState(
|
||||
isRunning = savedState.isRunning,
|
||||
durationMinutes = savedState.durationMinutes,
|
||||
remainingSeconds = savedState.remainingSeconds
|
||||
)
|
||||
val currentStates = _timerStates.value.toMutableMap()
|
||||
currentStates[taskId] = timerState
|
||||
_timerStates.value = currentStates
|
||||
|
||||
// Перезапускаем таймер если он был запущен
|
||||
if (savedState.isRunning && savedState.remainingSeconds > 0) {
|
||||
resumeTimer(taskId)
|
||||
}
|
||||
}
|
||||
|
||||
val loadResult = getTasksUseCase.loadTasks(scheduleId)
|
||||
if (loadResult.isFailure) {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
@@ -82,15 +120,37 @@ class TaskViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun createTask(task: Task) {
|
||||
fun createTask(scheduleId: String) {
|
||||
val title = _uiState.value.newTaskTitle
|
||||
if (title.isBlank()) return
|
||||
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
|
||||
|
||||
val task = Task(
|
||||
id = UUID.randomUUID().toString(),
|
||||
title = title,
|
||||
description = _uiState.value.newTaskDescription.takeIf { it.isNotBlank() },
|
||||
imageUrl = null,
|
||||
completed = false,
|
||||
scheduledTime = null,
|
||||
duration = _uiState.value.newTaskDuration.toIntOrNull(),
|
||||
scheduleId = scheduleId,
|
||||
order = _uiState.value.tasks.size,
|
||||
category = null,
|
||||
createdAt = LocalDateTime.now(),
|
||||
updatedAt = LocalDateTime.now()
|
||||
)
|
||||
|
||||
val result = createTaskUseCase(task)
|
||||
result.onSuccess {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
errorMessage = null
|
||||
errorMessage = null,
|
||||
showAddDialog = false,
|
||||
newTaskTitle = "",
|
||||
newTaskDescription = "",
|
||||
newTaskDuration = ""
|
||||
)
|
||||
// Список задач обновится автоматически через Flow
|
||||
}.onFailure { exception ->
|
||||
@@ -102,6 +162,98 @@ class TaskViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun createReward() {
|
||||
val title = _uiState.value.newRewardTitle
|
||||
if (title.isBlank()) return
|
||||
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
|
||||
|
||||
val userId = authRepository.getCurrentUser().first()?.id
|
||||
if (userId == null) {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
errorMessage = "Пользователь не найден"
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val reward = Reward(
|
||||
id = UUID.randomUUID().toString(),
|
||||
title = title,
|
||||
description = _uiState.value.newRewardDescription.takeIf { it.isNotBlank() },
|
||||
imageUrl = null,
|
||||
pointsRequired = _uiState.value.newRewardPoints,
|
||||
isClaimed = false,
|
||||
earnedAt = null,
|
||||
userId = userId,
|
||||
createdAt = LocalDateTime.now(),
|
||||
updatedAt = LocalDateTime.now()
|
||||
)
|
||||
|
||||
val result = createRewardUseCase(reward)
|
||||
result.onSuccess {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
errorMessage = null,
|
||||
showAddDialog = false,
|
||||
newRewardTitle = "",
|
||||
newRewardDescription = "",
|
||||
newRewardPoints = 10
|
||||
)
|
||||
}.onFailure { exception ->
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
errorMessage = exception.message ?: "Ошибка создания награды"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showAddDialog() {
|
||||
_uiState.value = _uiState.value.copy(showAddDialog = true)
|
||||
}
|
||||
|
||||
fun hideAddDialog() {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
showAddDialog = false,
|
||||
newTaskTitle = "",
|
||||
newTaskDescription = "",
|
||||
newTaskDuration = "",
|
||||
newRewardTitle = "",
|
||||
newRewardDescription = "",
|
||||
newRewardPoints = 10
|
||||
)
|
||||
}
|
||||
|
||||
fun setDialogMode(mode: DialogMode) {
|
||||
_uiState.value = _uiState.value.copy(dialogMode = mode)
|
||||
}
|
||||
|
||||
fun updateNewTaskTitle(title: String) {
|
||||
_uiState.value = _uiState.value.copy(newTaskTitle = title)
|
||||
}
|
||||
|
||||
fun updateNewTaskDescription(description: String) {
|
||||
_uiState.value = _uiState.value.copy(newTaskDescription = description)
|
||||
}
|
||||
|
||||
fun updateNewTaskDuration(duration: String) {
|
||||
_uiState.value = _uiState.value.copy(newTaskDuration = duration)
|
||||
}
|
||||
|
||||
fun updateNewRewardTitle(title: String) {
|
||||
_uiState.value = _uiState.value.copy(newRewardTitle = title)
|
||||
}
|
||||
|
||||
fun updateNewRewardDescription(description: String) {
|
||||
_uiState.value = _uiState.value.copy(newRewardDescription = description)
|
||||
}
|
||||
|
||||
fun updateNewRewardPoints(points: Int) {
|
||||
_uiState.value = _uiState.value.copy(newRewardPoints = points)
|
||||
}
|
||||
|
||||
fun deleteTask(taskId: String) {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
|
||||
@@ -121,6 +273,11 @@ class TaskViewModel @Inject constructor(
|
||||
|
||||
fun completeTask(taskId: String, completed: Boolean) {
|
||||
viewModelScope.launch {
|
||||
// Останавливаем таймер если задача завершена
|
||||
if (completed) {
|
||||
stopTimer(taskId)
|
||||
}
|
||||
|
||||
// Оптимистичное обновление UI
|
||||
val currentTasks = _uiState.value.tasks.toMutableList()
|
||||
val taskIndex = currentTasks.indexOfFirst { it.id == taskId }
|
||||
@@ -131,7 +288,12 @@ class TaskViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
val result = completeTaskUseCase(taskId, completed)
|
||||
result.onFailure { exception ->
|
||||
result.onSuccess {
|
||||
// Проверяем, все ли задачи выполнены, и выдаем награду если нужно
|
||||
if (completed && currentScheduleId != null) {
|
||||
checkAndAwardReward(currentScheduleId!!)
|
||||
}
|
||||
}.onFailure { exception ->
|
||||
// Откатываем изменение в случае ошибки
|
||||
if (taskIndex >= 0) {
|
||||
val oldTask = currentTasks[taskIndex]
|
||||
@@ -145,15 +307,196 @@ class TaskViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun checkAndAwardReward(scheduleId: String) {
|
||||
try {
|
||||
val user = authRepository.getCurrentUser().first()
|
||||
val userId = user?.id ?: return
|
||||
|
||||
// Получаем расписание
|
||||
val schedules = getSchedulesUseCase(userId).first()
|
||||
val schedule = schedules.find { it.id == scheduleId } ?: return
|
||||
|
||||
// Проверяем, есть ли награда в расписании
|
||||
val rewardId = schedule.rewardId ?: return
|
||||
|
||||
// Проверяем, все ли задачи выполнены
|
||||
val allTasksCompleted = schedule.tasks.isNotEmpty() &&
|
||||
schedule.tasks.all { it.completed }
|
||||
|
||||
if (allTasksCompleted) {
|
||||
// Выдаем награду
|
||||
claimRewardUseCase(rewardId)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Игнорируем ошибки при проверке награды
|
||||
}
|
||||
}
|
||||
|
||||
fun startTimer(taskId: String, durationMinutes: Int) {
|
||||
// Останавливаем предыдущий таймер для этой задачи если есть
|
||||
stopTimer(taskId)
|
||||
|
||||
val newState = TimerState(
|
||||
isRunning = true,
|
||||
durationMinutes = durationMinutes,
|
||||
remainingSeconds = durationMinutes * 60
|
||||
)
|
||||
|
||||
val currentStates = _timerStates.value.toMutableMap()
|
||||
currentStates[taskId] = newState
|
||||
_timerStates.value = currentStates
|
||||
|
||||
timerJobs[taskId] = viewModelScope.launch {
|
||||
while (true) {
|
||||
delay(1000)
|
||||
val currentStates = _timerStates.value
|
||||
val state = currentStates[taskId] ?: break
|
||||
|
||||
if (!state.isRunning) continue
|
||||
|
||||
if (state.remainingSeconds <= 0) {
|
||||
// Таймер истек, завершаем задачу
|
||||
completeTask(taskId, true)
|
||||
val updatedStates = currentStates.toMutableMap()
|
||||
updatedStates.remove(taskId)
|
||||
_timerStates.value = updatedStates
|
||||
timerJobs.remove(taskId)
|
||||
timerStateManager.removeTimerState(taskId)
|
||||
break
|
||||
}
|
||||
|
||||
val newRemaining = state.remainingSeconds - 1
|
||||
val updatedState = state.copy(remainingSeconds = newRemaining)
|
||||
val updatedStates = currentStates.toMutableMap()
|
||||
updatedStates[taskId] = updatedState
|
||||
_timerStates.value = updatedStates
|
||||
|
||||
// Сохраняем состояние таймера
|
||||
timerStateManager.saveTimerState(
|
||||
taskId,
|
||||
SavedTimerState(
|
||||
isRunning = updatedState.isRunning,
|
||||
durationMinutes = updatedState.durationMinutes,
|
||||
remainingSeconds = updatedState.remainingSeconds
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun pauseTimer(taskId: String) {
|
||||
val currentStates = _timerStates.value.toMutableMap()
|
||||
currentStates[taskId]?.let { state ->
|
||||
val pausedState = state.copy(isRunning = false)
|
||||
currentStates[taskId] = pausedState
|
||||
_timerStates.value = currentStates
|
||||
|
||||
// Сохраняем состояние таймера
|
||||
viewModelScope.launch {
|
||||
timerStateManager.saveTimerState(
|
||||
taskId,
|
||||
SavedTimerState(
|
||||
isRunning = pausedState.isRunning,
|
||||
durationMinutes = pausedState.durationMinutes,
|
||||
remainingSeconds = pausedState.remainingSeconds
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun resumeTimer(taskId: String) {
|
||||
val currentStates = _timerStates.value.toMutableMap()
|
||||
currentStates[taskId]?.let { state ->
|
||||
if (state.remainingSeconds > 0) {
|
||||
currentStates[taskId] = state.copy(isRunning = true)
|
||||
_timerStates.value = currentStates
|
||||
|
||||
// Перезапускаем таймер
|
||||
timerJobs[taskId]?.cancel()
|
||||
timerJobs[taskId] = viewModelScope.launch {
|
||||
while (true) {
|
||||
delay(1000)
|
||||
val currentStates = _timerStates.value
|
||||
val currentState = currentStates[taskId] ?: break
|
||||
|
||||
if (!currentState.isRunning) continue
|
||||
|
||||
if (currentState.remainingSeconds <= 0) {
|
||||
completeTask(taskId, true)
|
||||
val updatedStates = currentStates.toMutableMap()
|
||||
updatedStates.remove(taskId)
|
||||
_timerStates.value = updatedStates
|
||||
timerJobs.remove(taskId)
|
||||
timerStateManager.removeTimerState(taskId)
|
||||
break
|
||||
}
|
||||
|
||||
val newRemaining = currentState.remainingSeconds - 1
|
||||
val updatedState = currentState.copy(remainingSeconds = newRemaining)
|
||||
val updatedStates = currentStates.toMutableMap()
|
||||
updatedStates[taskId] = updatedState
|
||||
_timerStates.value = updatedStates
|
||||
|
||||
// Сохраняем состояние таймера
|
||||
timerStateManager.saveTimerState(
|
||||
taskId,
|
||||
SavedTimerState(
|
||||
isRunning = updatedState.isRunning,
|
||||
durationMinutes = updatedState.durationMinutes,
|
||||
remainingSeconds = updatedState.remainingSeconds
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stopTimer(taskId: String) {
|
||||
timerJobs[taskId]?.cancel()
|
||||
timerJobs.remove(taskId)
|
||||
val currentStates = _timerStates.value.toMutableMap()
|
||||
currentStates.remove(taskId)
|
||||
_timerStates.value = currentStates
|
||||
|
||||
// Удаляем сохраненное состояние
|
||||
viewModelScope.launch {
|
||||
timerStateManager.removeTimerState(taskId)
|
||||
}
|
||||
}
|
||||
|
||||
fun getTimerState(taskId: String): TimerState? {
|
||||
return _timerStates.value[taskId]
|
||||
}
|
||||
|
||||
fun clearError() {
|
||||
_uiState.value = _uiState.value.copy(errorMessage = null)
|
||||
}
|
||||
}
|
||||
|
||||
enum class DialogMode {
|
||||
TASK, REWARD
|
||||
}
|
||||
|
||||
data class TaskUiState(
|
||||
val tasks: List<Task> = emptyList(),
|
||||
val selectedTask: Task? = null,
|
||||
val isLoading: Boolean = false,
|
||||
val errorMessage: String? = null
|
||||
val errorMessage: String? = null,
|
||||
val showAddDialog: Boolean = false,
|
||||
val dialogMode: DialogMode = DialogMode.TASK,
|
||||
val newTaskTitle: String = "",
|
||||
val newTaskDescription: String = "",
|
||||
val newTaskDuration: String = "",
|
||||
val newRewardTitle: String = "",
|
||||
val newRewardDescription: String = "",
|
||||
val newRewardPoints: Int = 10
|
||||
)
|
||||
|
||||
data class TimerState(
|
||||
val isRunning: Boolean = false,
|
||||
val durationMinutes: Int = 0,
|
||||
val remainingSeconds: Int = 0
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user