diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 89e0263..a6e91ef 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,6 +2,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) + id("com.google.gms.google-services") id("de.mannodermaus.android-junit5") kotlin("plugin.serialization") version "1.9.23" } @@ -45,11 +46,15 @@ android { dependencies { + //user token + implementation(libs.kotlinx.coroutines.play.services) + implementation(libs.firebase.messaging) + //tests testImplementation(libs.mockk) testImplementation(libs.jupiter.junit.jupiter) androidTestImplementation(libs.ui.test.junit4) debugImplementation(libs.ui.test.manifest) - implementation(libs.androidx.lifecycle.process) + implementation(libs.androidx.lifecycle.process)//Dispatchers.setMain() implementation(libs.kotlinx.serialization.json) testImplementation(libs.junit.jupiter) testRuntimeOnly(libs.junit.vintage.engine) @@ -82,6 +87,7 @@ dependencies { implementation(libs.androidx.ui) implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material) implementation(libs.androidx.material3) implementation(libs.androidx.lifecycle.runtime.compose.android) implementation(libs.androidx.compose.material3) diff --git a/app/google-services.json b/app/google-services.json new file mode 100644 index 0000000..3a31eeb --- /dev/null +++ b/app/google-services.json @@ -0,0 +1,30 @@ +{ + "project_info": { + "project_number": "780456329938", + "firebase_url": "https://smartcalendarru-default-rtdb.firebaseio.com", + "project_id": "smartcalendarru", + "storage_bucket": "smartcalendarru.firebasestorage.app" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:780456329938:android:14d945eb519314b326230b", + "android_client_info": { + "package_name": "org.hse.smartcalendar" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyBBo5NexFhO3bcrOUp9Kjv5Xhbmuu2TluY" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt b/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt index 09d5c37..717219a 100644 --- a/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt +++ b/app/src/androidTest/kotlin/org/hse/smartcalendar/ui/screens/AchievementsScreenTest.kt @@ -6,6 +6,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.datetime.LocalTime import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType +import org.hse.smartcalendar.data.SharedInfo import org.hse.smartcalendar.store.StatisticsStore import org.hse.smartcalendar.ui.screens.model.AchievementType import org.hse.smartcalendar.ui.theme.SmartCalendarTheme @@ -39,6 +40,7 @@ class AchievementsScreenTest { start = LocalTime.Companion.fromMinutesOfDay(0), end = LocalTime.Companion.fromMinutesOfDay(300), date = TimeUtils.Companion.getCurrentDateTime().date, + sharedInfo = SharedInfo() ) secondTask = DailyTask( title = "first", @@ -50,6 +52,7 @@ class AchievementsScreenTest { start = LocalTime.Companion.fromMinutesOfDay(300), end = LocalTime.Companion.fromMinutesOfDay(1440-1), date = TimeUtils.Companion.getCurrentDateTime().date, + sharedInfo = SharedInfo() ) } fun assertAchievementData(type: AchievementType, text: String){ diff --git a/app/src/main/java/org/hse/smartcalendar/activity/BaseApplication.kt b/app/src/main/java/org/hse/smartcalendar/activity/BaseApplication.kt index 0321161..6521c56 100644 --- a/app/src/main/java/org/hse/smartcalendar/activity/BaseApplication.kt +++ b/app/src/main/java/org/hse/smartcalendar/activity/BaseApplication.kt @@ -11,6 +11,7 @@ import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.ProcessLifecycleOwner import androidx.work.WorkManager +import com.google.firebase.FirebaseApp import org.hse.smartcalendar.R //@HiltAndroidApp @@ -19,6 +20,7 @@ class BaseApplication : Application(), LifecycleObserver { override fun onCreate() { super.onCreate() + FirebaseApp.initializeApp(this) ProcessLifecycleOwner.get().lifecycle.addObserver(this) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val name = getString(R.string.channel_name) diff --git a/app/src/main/java/org/hse/smartcalendar/data/DailySchedule.kt b/app/src/main/java/org/hse/smartcalendar/data/DailySchedule.kt index 1ef64ae..53f29b3 100644 --- a/app/src/main/java/org/hse/smartcalendar/data/DailySchedule.kt +++ b/app/src/main/java/org/hse/smartcalendar/data/DailySchedule.kt @@ -62,7 +62,7 @@ class DailySchedule (val date : LocalDate = Clock.System.now() return dailyTasksList } - class NestedTaskException(oldTask: DailyTask, newTask: DailyTask) : IllegalArgumentException( + class NestedTaskException(val oldTask: DailyTask, newTask: DailyTask) : IllegalArgumentException( "New task have conflict schedule with previous one:\n" + "Old task: start = " + oldTask.getDailyTaskStartTime() + "; end = " + oldTask.getDailyTaskEndTime() + "\n" + "New task: start = " + newTask.getDailyTaskStartTime() + "; end = " + newTask.getDailyTaskEndTime() diff --git a/app/src/main/java/org/hse/smartcalendar/data/DailyTask.kt b/app/src/main/java/org/hse/smartcalendar/data/DailyTask.kt index 60ab1b5..f118af6 100644 --- a/app/src/main/java/org/hse/smartcalendar/data/DailyTask.kt +++ b/app/src/main/java/org/hse/smartcalendar/data/DailyTask.kt @@ -10,7 +10,13 @@ import kotlinx.serialization.Serializable import org.hse.smartcalendar.utility.TimeUtils import org.hse.smartcalendar.utility.UUIDSerializer import java.util.UUID - +@Serializable +data class SharedInfo( + val isShared: Boolean = false, + val invitees: List = listOf(), + val participants: List = listOf(), + val organizerName: String = "" +) @Serializable data class DailyTask ( @Serializable(with = UUIDSerializer::class) @@ -24,8 +30,9 @@ data class DailyTask ( private var start : LocalTime,//hour, minute private var end: LocalTime,//hour, minute private var date: LocalDate,//year, month, day + private var sharedInfo: SharedInfo ) { - companion object {//затычка, можешь убрать + companion object { val defaultDate = TimeUtils.getCurrentDateTime().date fun fromTime(start: LocalTime, end: LocalTime, date: LocalDate): DailyTask{ return fromTimeAndType(start,end,date, DailyTaskType.COMMON) @@ -37,7 +44,20 @@ data class DailyTask ( end = end, start = start, date = date, - type = type + type = type, + sharedInfo = SharedInfo() + ) + } + fun example(title: String, type: DailyTaskType, description: String, start: LocalTime, end: LocalTime, isComplete: Boolean=false): DailyTask{ + return DailyTask( + isComplete = isComplete, + title = title, + type = type, + description = description, + start = start, + end = end, + date = defaultDate, + sharedInfo = SharedInfo(), ) } } @@ -64,6 +84,9 @@ data class DailyTask ( fun setCompletion(status: Boolean) { isComplete = status } + fun setDate(date: LocalDate) { + this.date = date + } fun getDailyTaskEndTime() : LocalTime { return end @@ -138,7 +161,9 @@ data class DailyTask ( fun getMinutesLength(): Int{ return (end.toSecondOfDay()-start.toSecondOfDay())/60 } - + fun getSharedInfo(): SharedInfo{ + return sharedInfo + } class TimeConflictException(start: LocalTime, end: LocalTime) : IllegalArgumentException( "Illegal start and end time: start = " + start.toString() + diff --git a/app/src/main/java/org/hse/smartcalendar/data/Invite.kt b/app/src/main/java/org/hse/smartcalendar/data/Invite.kt new file mode 100644 index 0000000..ac6cdd2 --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/data/Invite.kt @@ -0,0 +1,13 @@ +package org.hse.smartcalendar.data + +import kotlinx.serialization.Serializable +import org.hse.smartcalendar.utility.UUIDSerializer +import java.util.UUID + +@Serializable +data class Invite( + @Serializable(with = UUIDSerializer::class) + val id: UUID, + val inviterName: String, + val task: DailyTask, +) \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/data/InviteAction.kt b/app/src/main/java/org/hse/smartcalendar/data/InviteAction.kt new file mode 100644 index 0000000..72eeb10 --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/data/InviteAction.kt @@ -0,0 +1,25 @@ +package org.hse.smartcalendar.data + +import kotlinx.serialization.Serializable +import org.hse.smartcalendar.utility.UUIDSerializer +import java.util.UUID + +@Serializable +data class InviteAction( + val type: Type, + @Serializable(with = UUIDSerializer::class) + val eventId: UUID, + val loginOrEmail: String +) { + enum class Type { + ACCEPT, + INVITE, + REMOVE_INVITE; + + override fun toString(): String = name + } + + companion object { + const val jsonName = "invite_action_json" + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/data/store/InvitesStore.kt b/app/src/main/java/org/hse/smartcalendar/data/store/InvitesStore.kt new file mode 100644 index 0000000..fb7b1ca --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/data/store/InvitesStore.kt @@ -0,0 +1,60 @@ +package org.hse.smartcalendar.data.store + +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.hse.smartcalendar.data.Invite +import org.hse.smartcalendar.network.ApiClient +import org.hse.smartcalendar.network.NetworkResponse +import org.hse.smartcalendar.network.InviteResponse +import org.hse.smartcalendar.network.toInvite +import org.hse.smartcalendar.repository.InviteRepository + +object InvitesStore { + private val _invites = mutableStateListOf() + val invites: List get() = _invites + private val repository = InviteRepository(ApiClient.inviteApiService) + + private var lastFetchedInvites: List = emptyList() + private var _hasNewInvites = mutableStateOf(false) + val hasNewInvites: Boolean get() = _hasNewInvites.value + + suspend fun init(): NetworkResponse> = withContext(Dispatchers.IO) { + when (val resp = repository.getMyInvites()) { + is NetworkResponse.Success -> { + val invites = resp.data.map { it.toInvite() } + updateDifference(fetched = invites) + setInvites(invites) + NetworkResponse.Success(invites) + } + is NetworkResponse.Error -> NetworkResponse.Error(resp.message) + is NetworkResponse.NetworkError -> NetworkResponse.NetworkError(resp.exceptionMessage) + is NetworkResponse.Loading -> NetworkResponse.Loading + } + } + private fun updateDifference( fetched: List){ + val isDifferent = fetched.size != lastFetchedInvites.size || + fetched.map { it.id } != lastFetchedInvites.map { it.id } + if (isDifferent) { + _hasNewInvites.value = true + } + lastFetchedInvites = fetched + } + + private fun setInvites(newList: List) { + _invites.clear() + _invites.addAll(newList) + } + + fun removeInvite(invite: Invite) { + _invites.remove(invite) + } + + fun addInvite(invite: Invite) { + _invites.add(invite) + } + fun markInvitesSeen() { + _hasNewInvites.value = false + } +} diff --git a/app/src/main/java/org/hse/smartcalendar/network/ApiClient.kt b/app/src/main/java/org/hse/smartcalendar/network/ApiClient.kt index 083c964..51c04fe 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/ApiClient.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/ApiClient.kt @@ -50,6 +50,9 @@ object ApiClient { val audioApiService : AudioApiInterface by lazy { retrofit.create(AudioApiInterface::class.java) } + val inviteApiService: InviteApiInterface by lazy { + retrofit.create(InviteApiInterface::class.java) + } } class AuthInterceptor(private val tokenProvider: () -> String?) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { diff --git a/app/src/main/java/org/hse/smartcalendar/network/ApiInterface.kt b/app/src/main/java/org/hse/smartcalendar/network/ApiInterface.kt index 1179f75..5f86fff 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/ApiInterface.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/ApiInterface.kt @@ -84,3 +84,30 @@ interface AudioApiInterface { @Part file: MultipartBody.Part ): Response> } + +interface InviteApiInterface { + + @GET("api/users/me/invites") + suspend fun getMyInvites(): Response> + + @POST("api/users/events/{eventId}/accept-invite") + suspend fun acceptInvite(@Path("eventId") eventId: UUID): Response + + @POST("api/users/events/{eventId}/invite") + suspend fun inviteUser( + @Path("eventId") eventId: UUID, + @Body request: InviteRequest + ): Response + + @POST("api/users/events/{eventId}/remove-invite") + suspend fun removeInvite( + @Path("eventId") eventId: UUID, + @Body request: InviteRequest + ): Response + + @POST("api/users/events/{eventId}/remove-participant") + suspend fun removeParticipant( + @Path("eventId") eventId: UUID, + @Body request: InviteRequest + ): Response +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/network/DataResponse.kt b/app/src/main/java/org/hse/smartcalendar/network/DataResponse.kt index 5d3ad9a..c165de2 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/DataResponse.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/DataResponse.kt @@ -9,6 +9,7 @@ import org.hse.smartcalendar.data.DailyTaskType import org.hse.smartcalendar.utility.TimeUtils import java.util.UUID import androidx.compose.runtime.MutableState +import org.hse.smartcalendar.data.SharedInfo data class RegisterResponse ( val id: Long? = null, @@ -57,7 +58,8 @@ data class TaskResponse( date = LocalDate.parse(date), type = type, creationTime = creationTime, - isComplete = complete + isComplete = complete, + sharedInfo = SharedInfo()//TODO ) } } @@ -84,6 +86,7 @@ data class ChatTaskResponse( type = DailyTaskType.valueOf(type?.uppercase() ?: "COMMON"), creationTime = creationTime?: TimeUtils.getCurrentDateTime(), isComplete = complete == true, + sharedInfo = SharedInfo() ) return task } diff --git a/app/src/main/java/org/hse/smartcalendar/network/InvitesResponse.kt b/app/src/main/java/org/hse/smartcalendar/network/InvitesResponse.kt new file mode 100644 index 0000000..1f2d125 --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/network/InvitesResponse.kt @@ -0,0 +1,57 @@ +package org.hse.smartcalendar.network + +import kotlinx.datetime.LocalDateTime +import kotlinx.serialization.Serializable +import org.hse.smartcalendar.data.DailyTask +import org.hse.smartcalendar.data.DailyTaskType +import org.hse.smartcalendar.data.Invite +import org.hse.smartcalendar.data.SharedInfo +import java.util.UUID + +@Serializable +data class InviteResponse( + val id: String, + val title: String, + val description: String, + val start: LocalDateTime, + val end: LocalDateTime, + val location: String, + val type: String, + val creationTime: LocalDateTime, + val organizer: OrganizerResponse, + val completed: Boolean, + val invitees: List, + val participants: List, + val shared: Boolean +) + +@Serializable +data class OrganizerResponse( + val username: String, + val email: String +) + +fun InviteResponse.toInvite(): Invite { + val task = DailyTask( + id = UUID.fromString(id), + title = title, + description = description, + start = start.time, + end = end.time, + date = start.date, + type = DailyTaskType.valueOf(type.uppercase()), + creationTime = creationTime, + isComplete = completed, + sharedInfo = SharedInfo( + isShared = shared, + invitees = invitees, + participants = participants.map { it.email }, + organizerName = organizer.username + ) + ) + return Invite( + id = UUID.fromString(id), + inviterName = organizer.username, + task = task + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/network/RequestClasses.kt b/app/src/main/java/org/hse/smartcalendar/network/RequestClasses.kt index 37f2963..3ebced0 100644 --- a/app/src/main/java/org/hse/smartcalendar/network/RequestClasses.kt +++ b/app/src/main/java/org/hse/smartcalendar/network/RequestClasses.kt @@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType import org.hse.smartcalendar.utility.TimeUtils - +data class InviteRequest(val loginOrEmail: String) data class LoginRequest( val username: String, val password: String diff --git a/app/src/main/java/org/hse/smartcalendar/repository/AuthRepository.kt b/app/src/main/java/org/hse/smartcalendar/repository/AuthRepository.kt index ecd8711..8a065bd 100644 --- a/app/src/main/java/org/hse/smartcalendar/repository/AuthRepository.kt +++ b/app/src/main/java/org/hse/smartcalendar/repository/AuthRepository.kt @@ -1,5 +1,6 @@ package org.hse.smartcalendar.repository +import android.util.Log import org.hse.smartcalendar.data.User import org.hse.smartcalendar.network.ApiClient import org.hse.smartcalendar.network.AuthApiInterface @@ -10,13 +11,24 @@ import org.hse.smartcalendar.network.LoginResponse import org.hse.smartcalendar.network.NetworkResponse import org.hse.smartcalendar.network.RegisterRequest import org.hse.smartcalendar.network.UserInfoResponse +import com.google.firebase.messaging.FirebaseMessaging +import kotlinx.coroutines.tasks.await //Repository - логика работы с задачами, получает данные, // формирует и отправляет запрос, возвращает данные/exception @Suppress("LiftReturnOrAssignment") class AuthRepository(private val api: AuthApiInterface) { + suspend fun getToken(): String{ + return try { + FirebaseMessaging.getInstance().token.await() + } catch (e: Exception) { + Log.e("FCM", "Failed to get token", e) + "" + } + } suspend fun loginUser(loginRequest: LoginRequest): NetworkResponse { try { + val token = getToken() val responseLogin = api.loginUser(loginRequest) if (responseLogin.isSuccessful) { val token = responseLogin.body()?.string() diff --git a/app/src/main/java/org/hse/smartcalendar/repository/InviteRepository.kt b/app/src/main/java/org/hse/smartcalendar/repository/InviteRepository.kt new file mode 100644 index 0000000..2538cfb --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/repository/InviteRepository.kt @@ -0,0 +1,38 @@ +package org.hse.smartcalendar.repository + +import okhttp3.ResponseBody +import org.hse.smartcalendar.data.store.InvitesStore +import org.hse.smartcalendar.network.InviteApiInterface +import org.hse.smartcalendar.network.InviteRequest +import org.hse.smartcalendar.network.InviteResponse +import org.hse.smartcalendar.network.NetworkResponse +import java.util.UUID + +class InviteRepository(private val api: InviteApiInterface) : BaseRepository() { + + suspend fun getMyInvites(): NetworkResponse> { + return withSupplierRequest { api.getMyInvites() } + } + + suspend fun acceptInvite(eventId: UUID): NetworkResponse { + return withSupplierRequest { api.acceptInvite(eventId) } + } + + suspend fun inviteUser(eventId: UUID, loginOrEmail: String): NetworkResponse { + return withSupplierRequest { + api.inviteUser(eventId, InviteRequest(loginOrEmail)) + } + } + + suspend fun removeInvite(eventId: UUID, loginOrEmail: String): NetworkResponse { + return withSupplierRequest { + api.removeInvite(eventId, InviteRequest(loginOrEmail)) + } + } + + suspend fun removeParticipant(eventId: UUID, loginOrEmail: String): NetworkResponse { + return withSupplierRequest { + api.removeParticipant(eventId, InviteRequest(loginOrEmail)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/repository/TaskRepository.kt b/app/src/main/java/org/hse/smartcalendar/repository/TaskRepository.kt index 65ab6ed..22844f9 100644 --- a/app/src/main/java/org/hse/smartcalendar/repository/TaskRepository.kt +++ b/app/src/main/java/org/hse/smartcalendar/repository/TaskRepository.kt @@ -6,6 +6,7 @@ import org.hse.smartcalendar.data.DailySchedule import org.hse.smartcalendar.data.DailySchedule.NestedTaskException import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.User +import org.hse.smartcalendar.network.ApiClient import org.hse.smartcalendar.network.CompleteStatusRequest import org.hse.smartcalendar.network.EditTaskRequest import org.hse.smartcalendar.network.NetworkResponse @@ -13,6 +14,7 @@ import org.hse.smartcalendar.network.TaskApiInterface import org.hse.smartcalendar.network.TaskRequest class TaskRepository(private val api: TaskApiInterface): BaseRepository() { + private val inviteRepository = InviteRepository(ApiClient.inviteApiService) suspend fun deleteTask(task: DailyTask): NetworkResponse{ val response = withSupplierRequest{ ->api.deleteTask(task.getId()) @@ -34,6 +36,7 @@ class TaskRepository(private val api: TaskApiInterface): BaseRepository() { suspend fun addTask(task: DailyTask): NetworkResponse { val response = withIdRequest { id -> api.addTask(id, TaskRequest.fromTask(task))} + return when (response) { is NetworkResponse.Success -> { NetworkResponse.Success("Task created".toResponseBody(null)) @@ -50,6 +53,7 @@ class TaskRepository(private val api: TaskApiInterface): BaseRepository() { try { return when (val response = withIdRequest { id -> api.getDailyTasks(id) }) { is NetworkResponse.Success -> { + User.clearSchedule() val listTask: List = response.data.map { it.toTask() } val map = listTask.groupBy { it.getTaskDate() } .mapValues { (_, taskList) -> diff --git a/app/src/main/java/org/hse/smartcalendar/ui/elements/Icons.kt b/app/src/main/java/org/hse/smartcalendar/ui/elements/Icons.kt index 1c0d35e..6b3d666 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/elements/Icons.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/elements/Icons.kt @@ -9,6 +9,137 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.path import androidx.compose.ui.unit.dp +val People: ImageVector + get() { + if (_People != null) return _People!! + + _People = ImageVector.Builder( + name = "People", + defaultWidth = 16.dp, + defaultHeight = 16.dp, + viewportWidth = 16f, + viewportHeight = 16f + ).apply { + path( + fill = SolidColor(Color.Black) + ) { + moveTo(15f, 14f) + reflectiveCurveToRelative(1f, 0f, 1f, -1f) + reflectiveCurveToRelative(-1f, -4f, -5f, -4f) + reflectiveCurveToRelative(-5f, 3f, -5f, 4f) + reflectiveCurveToRelative(1f, 1f, 1f, 1f) + close() + moveToRelative(-7.978f, -1f) + lineTo(7f, 12.996f) + curveToRelative(0.001f, -0.264f, 0.167f, -1.03f, 0.76f, -1.72f) + curveTo(8.312f, 10.629f, 9.282f, 10f, 11f, 10f) + curveToRelative(1.717f, 0f, 2.687f, 0.63f, 3.24f, 1.276f) + curveToRelative(0.593f, 0.69f, 0.758f, 1.457f, 0.76f, 1.72f) + lineToRelative(-0.008f, 0.002f) + lineToRelative(-0.014f, 0.002f) + close() + moveTo(11f, 7f) + arcToRelative(2f, 2f, 0f, true, false, 0f, -4f) + arcToRelative(2f, 2f, 0f, false, false, 0f, 4f) + moveToRelative(3f, -2f) + arcToRelative(3f, 3f, 0f, true, true, -6f, 0f) + arcToRelative(3f, 3f, 0f, false, true, 6f, 0f) + moveTo(6.936f, 9.28f) + arcToRelative(6f, 6f, 0f, false, false, -1.23f, -0.247f) + arcTo(7f, 7f, 0f, false, false, 5f, 9f) + curveToRelative(-4f, 0f, -5f, 3f, -5f, 4f) + quadToRelative(0f, 1f, 1f, 1f) + horizontalLineToRelative(4.216f) + arcTo(2.24f, 2.24f, 0f, false, true, 5f, 13f) + curveToRelative(0f, -1.01f, 0.377f, -2.042f, 1.09f, -2.904f) + curveToRelative(0.243f, -0.294f, 0.526f, -0.569f, 0.846f, -0.816f) + moveTo(4.92f, 10f) + arcTo(5.5f, 5.5f, 0f, false, false, 4f, 13f) + horizontalLineTo(1f) + curveToRelative(0f, -0.26f, 0.164f, -1.03f, 0.76f, -1.724f) + curveToRelative(0.545f, -0.636f, 1.492f, -1.256f, 3.16f, -1.275f) + close() + moveTo(1.5f, 5.5f) + arcToRelative(3f, 3f, 0f, true, true, 6f, 0f) + arcToRelative(3f, 3f, 0f, false, true, -6f, 0f) + moveToRelative(3f, -2f) + arcToRelative(2f, 2f, 0f, true, false, 0f, 4f) + arcToRelative(2f, 2f, 0f, false, false, 0f, -4f) + } + }.build() + + return _People!! + } + +private var _People: ImageVector? = null + + +val Event_upcoming: ImageVector + get() { + if (_Event_upcoming != null) return _Event_upcoming!! + + _Event_upcoming = ImageVector.Builder( + name = "Event_upcoming", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 960f, + viewportHeight = 960f + ).apply { + path( + fill = SolidColor(Color(0xFF000000)) + ) { + moveTo(600f, 880f) + verticalLineToRelative(-80f) + horizontalLineToRelative(160f) + verticalLineToRelative(-400f) + horizontalLineTo(200f) + verticalLineToRelative(160f) + horizontalLineToRelative(-80f) + verticalLineToRelative(-320f) + quadToRelative(0f, -33f, 23.5f, -56.5f) + reflectiveQuadTo(200f, 160f) + horizontalLineToRelative(40f) + verticalLineToRelative(-80f) + horizontalLineToRelative(80f) + verticalLineToRelative(80f) + horizontalLineToRelative(320f) + verticalLineToRelative(-80f) + horizontalLineToRelative(80f) + verticalLineToRelative(80f) + horizontalLineToRelative(40f) + quadToRelative(33f, 0f, 56.5f, 23.5f) + reflectiveQuadTo(840f, 240f) + verticalLineToRelative(560f) + quadToRelative(0f, 33f, -23.5f, 56.5f) + reflectiveQuadTo(760f, 880f) + close() + moveTo(320f, 960f) + lineToRelative(-56f, -56f) + lineToRelative(103f, -104f) + horizontalLineTo(40f) + verticalLineToRelative(-80f) + horizontalLineToRelative(327f) + lineTo(264f, 616f) + lineToRelative(56f, -56f) + lineToRelative(200f, 200f) + close() + moveTo(200f, 320f) + horizontalLineToRelative(560f) + verticalLineToRelative(-80f) + horizontalLineTo(200f) + close() + moveToRelative(0f, 0f) + verticalLineToRelative(-80f) + close() + } + }.build() + + return _Event_upcoming!! + } + +private var _Event_upcoming: ImageVector? = null + + public val Person: ImageVector get() { diff --git a/app/src/main/java/org/hse/smartcalendar/ui/elements/InvitesItem.kt b/app/src/main/java/org/hse/smartcalendar/ui/elements/InvitesItem.kt new file mode 100644 index 0000000..6be3ecb --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/ui/elements/InvitesItem.kt @@ -0,0 +1,164 @@ +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.DismissDirection +import androidx.compose.material.DismissValue +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.SwipeToDismiss +import androidx.compose.material.rememberDismissState +import androidx.compose.material3.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.datetime.LocalTime +import org.hse.smartcalendar.data.DailyTask +import org.hse.smartcalendar.data.DailyTaskType +import org.hse.smartcalendar.data.Invite +import org.hse.smartcalendar.ui.task.DailyTaskCard +import org.hse.smartcalendar.ui.theme.SmartCalendarTheme +import java.util.UUID + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun InviteItem( + invite: Invite, + onAccept: () -> Boolean, + onDecline: () -> Unit +) { + var expanded by remember { mutableStateOf(false) } + val dismissState = rememberDismissState { dismissValue -> + when (dismissValue) { + DismissValue.DismissedToEnd -> { + onAccept() + } + DismissValue.DismissedToStart -> { + onDecline() + true + } + else -> false + } + } + + SwipeToDismiss( + state = dismissState, + directions = setOf(DismissDirection.StartToEnd, DismissDirection.EndToStart), + background = { + val color = when (dismissState.dismissDirection) { + DismissDirection.StartToEnd -> Color(0xFFAAF683) + DismissDirection.EndToStart -> Color(0xFFFF6B6B) + null -> Color.Transparent + } + Box( + modifier = Modifier + .fillMaxSize() + .background(color) + .padding(horizontal = 16.dp), + contentAlignment = when (dismissState.dismissDirection) { + DismissDirection.StartToEnd -> Alignment.CenterStart + DismissDirection.EndToStart -> Alignment.CenterEnd + else -> Alignment.Center + } + ) { + Icon( + imageVector = if (dismissState.dismissDirection == DismissDirection.StartToEnd) + Icons.Default.Check else Icons.Default.Close, + contentDescription = null, + tint = Color.White + ) + } + }, + dismissContent = { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + ) { + Card( + shape = RoundedCornerShape(8.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp), + elevation = CardDefaults.cardElevation(2.dp) + ) { + Row( + Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Text(invite.inviterName, style = MaterialTheme.typography.labelMedium) + Spacer(Modifier.height(4.dp)) + Text(invite.task.getDailyTaskTitle(), style = MaterialTheme.typography.bodyLarge) + } + Column(horizontalAlignment = Alignment.End) { + Text( + invite.task.getDailyTaskArrangementString(), + style = MaterialTheme.typography.bodySmall + ) + Spacer(Modifier.height(4.dp)) + Text( + "Date: ${invite.task.getTaskDate()}", + style = MaterialTheme.typography.bodySmall + ) + } + } + } + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.End + ) { + TextButton(onClick = { expanded = !expanded }) { + Text(if (expanded) "Hide Details" else "Show Details") + } + } + if (expanded) { + DailyTaskCard( + task = invite.task, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + onCompletionChange = {}, + onLongPressAction = {} + ) + } + } + } + ) +} +@Preview(showBackground = true) +@Composable +fun InviteItemPreview() { + SmartCalendarTheme { + val task = DailyTask.fromTimeAndType( + type = DailyTaskType.WORK, + start = LocalTime(4, 0), + end = LocalTime(5, 0), + date = DailyTask.defaultDate + ) + val invite = Invite( + id = UUID.randomUUID(), + inviterName = "Alice", + task = task + ) + + Surface(modifier = Modifier.padding(16.dp)) { + InviteItem( + invite = invite, + onAccept = {true}, + onDecline = {} + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt b/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt index 785255b..43687e4 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/navigation/App.kt @@ -6,6 +6,7 @@ import androidx.compose.material3.DrawerValue import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalNavigationDrawer import androidx.compose.material3.rememberDrawerState +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope @@ -25,6 +26,7 @@ import org.hse.smartcalendar.ui.screens.AchievementsScreen import org.hse.smartcalendar.ui.screens.ChangeLogin import org.hse.smartcalendar.ui.screens.ChangePassword import org.hse.smartcalendar.ui.screens.GreetingScreen +import org.hse.smartcalendar.ui.screens.InvitesScreen import org.hse.smartcalendar.ui.screens.LoadingScreen import org.hse.smartcalendar.ui.screens.SettingsScreen import org.hse.smartcalendar.ui.screens.StatisticsScreen @@ -33,6 +35,7 @@ import org.hse.smartcalendar.ui.task.TaskEditWindow import org.hse.smartcalendar.utility.Navigation import org.hse.smartcalendar.utility.Screens import org.hse.smartcalendar.utility.rememberNavigation +import org.hse.smartcalendar.view.model.InvitesViewModel import org.hse.smartcalendar.view.model.ListViewModel import org.hse.smartcalendar.view.model.StatisticsViewModel import org.hse.smartcalendar.view.model.TaskEditViewModel @@ -46,6 +49,7 @@ fun App( listVM: ListViewModel, taskEditVM: TaskEditViewModel ) { + val invitesModel: InvitesViewModel = viewModel() val authModel: AuthViewModel = viewModel() val navigation = rememberNavigation() val coroutineScope = rememberCoroutineScope() @@ -56,9 +60,15 @@ fun App( navBackStackEntry?.destination?.route ?: Screens.CALENDAR.route val drawerEnabled = currentRoute !in listOf(Screens.LOGIN.route, Screens.GREETING.route, Screens.REGISTER.route) val isExpandedScreen =false - val DrawerState = rememberDrawerState(isExpandedScreen) + val navigationDrawerState = rememberDrawerState(isExpandedScreen) + val notificationDrawerState = rememberModalBottomSheetState( + skipPartiallyExpanded = true, + confirmValueChange = { newValue -> + true + } + ) val openDrawer: ()-> Unit = { - coroutineScope.launch { DrawerState.open() } + coroutineScope.launch { navigationDrawerState.open() } } if (drawerEnabled) { ModalNavigationDrawer( @@ -66,20 +76,21 @@ fun App( AppDrawer( currentRoute = initialRoute, navigation, - closeDrawer = { coroutineScope.launch { DrawerState.close() } } + closeDrawer = { coroutineScope.launch { navigationDrawerState.close() } } ) }, - drawerState = DrawerState, + drawerState = navigationDrawerState, gesturesEnabled = !isExpandedScreen - ){NestedNavigator(navigation, authModel,openDrawer,statisticsVM, listVM, taskEditVM ) + ){NestedNavigator(navigation, authModel,openDrawer,statisticsVM, listVM, taskEditVM, invitesModel ) } } else { - NestedNavigator(navigation, authModel,openDrawer, statisticsVM, listVM, taskEditVM ) + NestedNavigator(navigation, authModel,openDrawer, statisticsVM, listVM, taskEditVM, invitesModel ) } } @Composable fun NestedNavigator(navigation: Navigation, authModel: AuthViewModel,openDrawer: ()-> Unit, - statisticsModel: StatisticsViewModel, listModel: ListViewModel, editModel: TaskEditViewModel){ + statisticsModel: StatisticsViewModel, listModel: ListViewModel, editModel: TaskEditViewModel, + invitesModel: InvitesViewModel){ val reminderModel: ReminderViewModel = viewModel(factory = ReminderViewModelFactory( LocalContext.current.applicationContext as Application )) @@ -102,7 +113,7 @@ fun NestedNavigator(navigation: Navigation, authModel: AuthViewModel,openDrawer: AuthScreen(navigation, authModel, AuthType.Login) } composable(Screens.LOADING.route) { - LoadingScreen(navigation, statisticsModel, listModel) + LoadingScreen(navigation, statisticsModel, listModel, invitesModel) } } @@ -138,6 +149,9 @@ fun NestedNavigator(navigation: Navigation, authModel: AuthViewModel,openDrawer: composable(route = Screens.ACHIEVEMENTS.route) { AchievementsScreen(navigation, openDrawer, statisticsModel) } + composable(route = Screens.SHARED_EVENTS.route) { + InvitesScreen(navigation, openDrawer, invitesModel, listModel) + } composable(Screens.EDIT_TASK.route) { TaskEditWindow( onSave = {task->reminderModel.scheduleReminder(task)}, diff --git a/app/src/main/java/org/hse/smartcalendar/ui/navigation/AppDriver.kt b/app/src/main/java/org/hse/smartcalendar/ui/navigation/AppDriver.kt index b698324..91f11eb 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/navigation/AppDriver.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/navigation/AppDriver.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.tooling.preview.Preview import org.hse.smartcalendar.ui.elements.Calendar_add_on import org.hse.smartcalendar.ui.elements.Calendar_today +import org.hse.smartcalendar.ui.elements.Event_upcoming import org.hse.smartcalendar.ui.elements.Finance import org.hse.smartcalendar.ui.elements.Follow_the_signs import org.hse.smartcalendar.ui.elements.Medal @@ -43,6 +44,8 @@ fun AppDrawer( currentRoute = currentRoute, navigation = navigation, closeDrawer = closeDrawer) AppNavigationDrawerItem(label = "My Calendars", icon = Calendar_add_on, destination = Screens.MY_CALENDARS, currentRoute = currentRoute, navigation = navigation, closeDrawer = closeDrawer) + AppNavigationDrawerItem(label = "Shared events", icon = Event_upcoming, destination = Screens.SHARED_EVENTS, + currentRoute = currentRoute, navigation = navigation, closeDrawer = closeDrawer) } } @Composable diff --git a/app/src/main/java/org/hse/smartcalendar/ui/navigation/TopButton.kt b/app/src/main/java/org/hse/smartcalendar/ui/navigation/TopButton.kt index 81344cf..fd39e36 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/navigation/TopButton.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/navigation/TopButton.kt @@ -3,7 +3,10 @@ package org.hse.smartcalendar.ui.navigation import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Notifications import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Badge +import androidx.compose.material3.BadgedBox import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -13,6 +16,7 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarColors import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview +import org.hse.smartcalendar.data.store.InvitesStore import org.hse.smartcalendar.utility.Navigation import org.hse.smartcalendar.utility.Screens import org.hse.smartcalendar.utility.rememberNavigation @@ -29,20 +33,26 @@ fun TopButton(openMenu: (()-> Unit)? = null, navigation: Navigation, text: Strin IconButton(onClick = { openMenu?.invoke() }) { Icon( imageVector = Icons.Filled.Menu, - contentDescription = null + contentDescription = "Menu" ) } }, actions = { IconButton(onClick = { navigation.navigateTo(Screens.SETTINGS.route)}) { - Icon(Icons.Filled.Settings, contentDescription = null) + Icon(Icons.Filled.Settings, contentDescription = "Settings") } - IconButton( - onClick = {}) { - Icon(imageVector = Icons.Filled.MoreVert, - contentDescription = null) + IconButton(onClick = {navigation.navigateTo(Screens.SHARED_EVENTS.route)}) { + if (InvitesStore.hasNewInvites) { + BadgedBox(badge = { + Badge { Text("${InvitesStore.invites.size}") } + }) { + Icon(Icons.Filled.Notifications, "Invites") + } + } else { + Icon(Icons.Filled.Notifications, "Invites") + } } }, colors = TopAppBarColors( diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/LoadingScreen.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/LoadingScreen.kt index 6bb77fa..7aa225b 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/screens/LoadingScreen.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/LoadingScreen.kt @@ -21,10 +21,16 @@ import org.hse.smartcalendar.network.NetworkResponse import org.hse.smartcalendar.utility.Navigation import org.hse.smartcalendar.utility.Screens import org.hse.smartcalendar.view.model.InitViewModel +import org.hse.smartcalendar.view.model.InvitesViewModel import org.hse.smartcalendar.view.model.StatisticsViewModel import org.hse.smartcalendar.view.model.ListViewModel @Composable -fun LoadingScreen(navigation: Navigation, statisticsVM: StatisticsViewModel, listModel: ListViewModel){ +fun LoadingScreen( + navigation: Navigation, + statisticsVM: StatisticsViewModel, + listModel: ListViewModel, + invitesModel: InvitesViewModel +){ val initVM: InitViewModel = viewModel()//гарантирует 1 модель на Composable val initState by initVM.initResult.observeAsState() val statisticsState by statisticsVM.initResult.observeAsState() @@ -35,6 +41,7 @@ fun LoadingScreen(navigation: Navigation, statisticsVM: StatisticsViewModel, lis LaunchedEffect(initState) { if (initState is NetworkResponse.Success) { statisticsVM.init() + invitesModel.startPollingInvites() } } Column( diff --git a/app/src/main/java/org/hse/smartcalendar/ui/screens/SharedEventsScreen.kt b/app/src/main/java/org/hse/smartcalendar/ui/screens/SharedEventsScreen.kt new file mode 100644 index 0000000..b1939dc --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/ui/screens/SharedEventsScreen.kt @@ -0,0 +1,150 @@ +package org.hse.smartcalendar.ui.screens + +import InviteItem +import androidx.compose.foundation.layout.fillMaxSize +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.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import kotlinx.coroutines.launch +import org.hse.smartcalendar.ui.navigation.TopButton +import org.hse.smartcalendar.utility.Navigation +import org.hse.smartcalendar.view.model.InvitesViewModel +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.datetime.LocalTime +import org.hse.smartcalendar.data.DailyTask +import org.hse.smartcalendar.data.DailyTaskType +import org.hse.smartcalendar.data.Invite +import org.hse.smartcalendar.data.store.InvitesStore +import org.hse.smartcalendar.ui.theme.SmartCalendarTheme +import org.hse.smartcalendar.utility.Screens +import org.hse.smartcalendar.utility.rememberNavigation +import org.hse.smartcalendar.view.model.ListViewModel +import java.util.UUID + +@Composable +fun InvitesScreen( + navigation: Navigation, + openMenu: (()-> Unit)? = null, + viewModel: InvitesViewModel, + listViewModel: ListViewModel +) { + LaunchedEffect(Unit) { + InvitesStore.markInvitesSeen() + } + val invites = viewModel.invites + val snackbarHostState = remember { SnackbarHostState() } + val scope = rememberCoroutineScope() + + Scaffold( + topBar = { + TopButton( + text = "Invitations", + navigation = navigation, + openMenu = openMenu + ) + }, + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + } + ) { padding -> + if (invites.isEmpty()) { + Box( + Modifier + .fillMaxSize() + .padding(padding), + contentAlignment = Alignment.Center + ) { + Text("No shared‑task invitations") + } + } else { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(padding) + ) { + items(invites, key = { it.id }) { invite -> + InviteItem( + invite = invite, + onAccept = { + val notNested = viewModel.tryAdd(invite); + if (notNested) { + listViewModel.loadDailyTasks() + scope.launch { + snackbarHostState.showSnackbar("Invitation accepted") + } + } else { + val conflict = viewModel.lastConflict!! + scope.launch { + val result = snackbarHostState.showSnackbar( + message = "Time conflict with existing task", + actionLabel = "Go", + duration = SnackbarDuration.Indefinite + ) + if (result == SnackbarResult.ActionPerformed) { + listViewModel.changeDailyTaskSchedule(conflict.getTaskDate()) + navigation.navigateTo(Screens.CALENDAR.route) + } + } + } + notNested + }, + onDecline = { + viewModel.decline(invite) + scope.launch { + snackbarHostState.showSnackbar("Invitation declined") + } + } + ) + } + } + } + } +} +@Preview(showBackground = true) +@Composable +fun InvitesScreenPreview() { + SmartCalendarTheme { + val firstInvite = Invite( + id = UUID.randomUUID(), + inviterName = "Alexander Khrabrov", + task = DailyTask.example( + title = "Analysis exam", + type = DailyTaskType.STUDIES, + description = "The math exam will take place on June 25 at Kantemirovskaya", + start = LocalTime(10, 0), + end = LocalTime(11, 0) + ) + ) + val secondInvite = Invite( + id = UUID.randomUUID(), + inviterName = "ITMO", + task = DailyTask.example( + title = "Transfer Test", + type = DailyTaskType.STUDIES, + description = "The certification test will be held on August 21 at 49 Kronverksky Prospekt in room 2137.", + start = LocalTime(10, 0), + end = LocalTime(11, 0) + ) + ) + + val invitesViewModel: InvitesViewModel = viewModel() + invitesViewModel.addInvite(firstInvite) + invitesViewModel.addInvite(secondInvite) + InvitesScreen( + navigation = rememberNavigation(), + viewModel = invitesViewModel, + openMenu = {}, + listViewModel = viewModel() + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskCard.kt b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskCard.kt index 7b3d2a6..d8929f3 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskCard.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskCard.kt @@ -8,6 +8,8 @@ import androidx.compose.foundation.layout.Spacer 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.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButtonColors @@ -29,12 +31,12 @@ import androidx.compose.ui.unit.dp import kotlinx.datetime.LocalTime import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType +import org.hse.smartcalendar.ui.elements.People import org.hse.smartcalendar.view.model.ListViewModel import org.hse.smartcalendar.view.model.StatisticsManager import org.hse.smartcalendar.view.model.StatisticsViewModel import org.hse.smartcalendar.view.model.TaskEditViewModel - @Composable fun DailyTaskCard( task: DailyTask, @@ -42,13 +44,30 @@ fun DailyTaskCard( onCompletionChange: () -> Unit = { }, onLongPressAction: () -> Unit = { }, taskEditViewModel: TaskEditViewModel +) { + DailyTaskCard( + task, + modifier, + onCompletionChange, + { + onLongPressAction(); + taskEditViewModel.setTask(task) + } + ) +} + +@Composable +fun DailyTaskCard( + task: DailyTask, + modifier: Modifier = Modifier, + onCompletionChange: () -> Unit = { }, + onLongPressAction: () -> Unit = { } ) { Column(modifier = Modifier .padding(5.dp) .pointerInput(Unit) { detectTapGestures( onLongPress = { - taskEditViewModel.setTask(task) onLongPressAction() } ) @@ -96,6 +115,14 @@ fun DailyTaskCard( .align(Alignment.Bottom), textAlign = TextAlign.End ) + if (task.getSharedInfo().isShared) { + Icon( + imageVector = People, + contentDescription = "Shared", + modifier = Modifier.size(20.dp).padding(start = 4.dp), + tint = MaterialTheme.colorScheme.primary + ) + } } } Surface( @@ -124,42 +151,38 @@ fun DailyTaskCard( fun DailyTaskCardPreview() { val statisticsManager = StatisticsManager(StatisticsViewModel()) val taskEditViewModel = TaskEditViewModel(listViewModel = ListViewModel(statisticsManager)) - val previewCommonTask = DailyTask( + val previewCommonTask = DailyTask.example( title = "Common title example", type = DailyTaskType.COMMON, description = "Common description Example", start = LocalTime(4, 0), - end = LocalTime(5, 0), - date = DailyTask.defaultDate + end = LocalTime(5, 0) ) - val previewFitnessTask = DailyTask( + val previewFitnessTask = DailyTask.example( title = "Fitness title example", type = DailyTaskType.FITNESS, description = "Fitness description Example", start = LocalTime(4, 0), end = LocalTime(5, 0), - date = DailyTask.defaultDate ) - val previewWorkTask = DailyTask( + val previewWorkTask = DailyTask.example( title = "Work title example", type = DailyTaskType.WORK, description = "Work description Example", start = LocalTime(4, 0), end = LocalTime(5, 0), - date = DailyTask.defaultDate ) - val previewStudiesTask = DailyTask( + val previewStudiesTask = DailyTask.example( isComplete = true, title = "Studies title example", type = DailyTaskType.STUDIES, description = "Studies description Example", start = LocalTime(4, 0), end = LocalTime(5, 0), - date = DailyTask.defaultDate ) Column { diff --git a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskCreationWindow.kt b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskCreationWindow.kt index fc1b949..3960c3b 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskCreationWindow.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskCreationWindow.kt @@ -24,11 +24,13 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -43,6 +45,7 @@ import kotlinx.datetime.LocalTime import org.hse.smartcalendar.data.DailySchedule import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType +import org.hse.smartcalendar.data.SharedInfo import org.hse.smartcalendar.network.ChatTaskResponse import org.hse.smartcalendar.network.NetworkResponse import org.hse.smartcalendar.ui.elements.AudioRecorderButton @@ -69,6 +72,10 @@ fun BottomSheet( viewModel: ListViewModel, addTask: (DailyTask) -> Unit, ) { + val isShared = rememberSaveable { mutableStateOf(false) } + val invitees = remember { mutableStateListOf("") } + + val expendedTypeSelection = rememberSaveable { mutableStateOf(false) } val isConflictInTimeField = rememberSaveable { mutableStateOf(false) } val isEmptyTitle = rememberSaveable { mutableStateOf(false) } @@ -181,6 +188,8 @@ fun BottomSheet( ) } } + SharedInviteSection(isShared=isShared, + invitees=invitees) Row( modifier = Modifier .fillMaxWidth() @@ -222,7 +231,9 @@ fun BottomSheet( isEmptyTitle = isEmptyTitle, isNestedTask = isNestedTask, fstFiledHasFormatError = fstFiledHasFormatError, - sndFiledHasFormatError = sndFiledHasFormatError + sndFiledHasFormatError = sndFiledHasFormatError, + isShared = isShared, + invitees = invitees ) }, ) { @@ -292,6 +303,8 @@ fun addNewTask( taskTitle: MutableState, taskType: MutableState, taskDescription: MutableState, + isShared: MutableState, + invitees: MutableList, startTime: MutableState, endTime: MutableState, viewModel: ListViewModel, @@ -312,14 +325,14 @@ fun addNewTask( if (isConflictInTimeField.value || isEmptyTitle.value) { return } - val newTask = DailyTask( title = taskTitle.value, type = taskType.value, description = taskDescription.value, start = LocalTime.fromMinutesOfDay(startTime.value), end = LocalTime.fromMinutesOfDay(endTime.value), - date = viewModel.getScreenDate() + date = viewModel.getScreenDate(), + sharedInfo = SharedInfo(isShared = isShared.value, invitees = invitees.toList()) ) try { @@ -454,7 +467,7 @@ fun BottomSheetPreview() { addTask = {}, taskType = taskType, viewModel = ListViewModel(StatisticsManager(StatisticsViewModel())), - onRecordStop = TODO(), + onRecordStop = {null}, audioFile = audioFile, ) } diff --git a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskInviteSection.kt b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskInviteSection.kt new file mode 100644 index 0000000..c603592 --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTaskInviteSection.kt @@ -0,0 +1,109 @@ +package org.hse.smartcalendar.ui.task + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.hse.smartcalendar.ui.theme.SmartCalendarTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SharedInviteSection( + isShared: MutableState, + invitees: SnapshotStateList +) { + Column(modifier = Modifier.fillMaxWidth()) { + Button( + onClick = { + isShared.value = !isShared.value + if (isShared.value && invitees.isEmpty()) { + invitees.add("") + } + if (!isShared.value) { + invitees.clear() + } + }, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = if (isShared.value) + "Joint event" + else + "Individual event" + ) + } + if (isShared.value) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Write logins: ", + style = MaterialTheme.typography.labelSmall + ) + invitees.forEachIndexed { index, _ -> + Spacer(modifier = Modifier.height(4.dp)) + TextField( + value = invitees[index], + onValueChange = { newValue -> + invitees[index] = newValue + }, + placeholder = { Text("Login ${index + 1}") }, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 56.dp) + ) + } + Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + IconButton( + onClick = { + invitees.add("") + } + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = "Add another login" + ) + } + } + } + } +} +@Preview(showBackground = true) +@Composable +fun SharedInvitePreview() { + SmartCalendarTheme { + + val isShared = remember { mutableStateOf(false) } + val invitees = remember { mutableStateListOf("") } + Column(modifier = Modifier.padding(16.dp)) { + SharedInviteSection( + isShared = isShared, + invitees = invitees + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTasksList.kt b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTasksList.kt index c37474d..4d7d77a 100644 --- a/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTasksList.kt +++ b/app/src/main/java/org/hse/smartcalendar/ui/task/DailyTasksList.kt @@ -58,6 +58,7 @@ import kotlinx.datetime.format.MonthNames import kotlinx.datetime.format.char import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType +import org.hse.smartcalendar.data.SharedInfo import org.hse.smartcalendar.network.NetworkResponse import org.hse.smartcalendar.view.model.ReminderViewModel import org.hse.smartcalendar.view.model.ReminderViewModelFactory @@ -276,7 +277,8 @@ fun DailyTaskListPreview() { description = "sss", start = LocalTime(0, 0), end = LocalTime(1, 0), - date = LocalDate(2022, 5, 4) + date = LocalDate(2022, 5, 4), + sharedInfo = SharedInfo() ) ) val taskEditViewModel = TaskEditViewModel( diff --git a/app/src/main/java/org/hse/smartcalendar/utility/Navigation.kt b/app/src/main/java/org/hse/smartcalendar/utility/Navigation.kt index c1a1778..5d16f9f 100644 --- a/app/src/main/java/org/hse/smartcalendar/utility/Navigation.kt +++ b/app/src/main/java/org/hse/smartcalendar/utility/Navigation.kt @@ -19,6 +19,7 @@ enum class Screens(val route: String) { ACHIEVEMENTS("achievements"), EDIT_TASK("editTask"), MY_CALENDARS("myCalendars"), + SHARED_EVENTS("sharedIvents"), RATING("rating"), AI_ASSISTANT("aiAssistant"); enum class Subgraph(val route: String) { diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/InitViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/InitViewModel.kt index 7164424..c4a5d69 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/InitViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/InitViewModel.kt @@ -9,11 +9,13 @@ import org.hse.smartcalendar.network.ApiClient import org.hse.smartcalendar.network.NetworkResponse import org.hse.smartcalendar.network.UserInfoResponse import org.hse.smartcalendar.repository.AuthRepository +import org.hse.smartcalendar.repository.InviteRepository import org.hse.smartcalendar.repository.TaskRepository class InitViewModel:ViewModel() { private val authRepository: AuthRepository = AuthRepository(ApiClient.authApiService) private val taskRepository: TaskRepository = TaskRepository(ApiClient.taskApiService) + private val invitesRepository: InviteRepository = InviteRepository(ApiClient.inviteApiService) var _userInfoResult = MutableLiveData>() val userInfoResult = _userInfoResult var _initResult = MutableLiveData>() diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/InvitesViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/InvitesViewModel.kt new file mode 100644 index 0000000..20d6f95 --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/view/model/InvitesViewModel.kt @@ -0,0 +1,95 @@ +package org.hse.smartcalendar.view.model + +import androidx.compose.runtime.mutableStateListOf +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.workDataOf +import kotlinx.coroutines.launch +import org.hse.smartcalendar.data.DailySchedule +import org.hse.smartcalendar.data.DailyTask +import org.hse.smartcalendar.data.Invite +import org.hse.smartcalendar.data.InviteAction +import org.hse.smartcalendar.data.MainSchedule +import org.hse.smartcalendar.data.User +import org.hse.smartcalendar.data.store.InvitesStore +import org.hse.smartcalendar.network.ApiClient +import org.hse.smartcalendar.network.NetworkResponse +import org.hse.smartcalendar.repository.InviteRepository +import org.hse.smartcalendar.work.InviteApiWorker +import kotlinx.serialization.json.Json +import org.hse.smartcalendar.data.WorkManagerHolder +import androidx.work.ExistingWorkPolicy + +class InvitesViewModel : ViewModel() { + var _initResult = MutableLiveData>>() + val initResult:LiveData>> = _initResult + private val invitesRepository: InviteRepository = InviteRepository(ApiClient.inviteApiService) + val invites = mutableStateListOf() + private val mainSchedule = User.getSchedule() + private var _lastConflict: DailyTask? = null + val lastConflict: DailyTask? get() = _lastConflict + + fun startPollingInvites(){ + viewModelScope.launch { + while (true) { + _initResult.value = NetworkResponse.Loading + val response = InvitesStore.init() + if (response is NetworkResponse.Success) { + updateUI() + } + _initResult.value = response + kotlinx.coroutines.delay(10000) + } + } + } + fun updateUI(){ + invites.clear() + invites.addAll(InvitesStore.invites) + } + fun tryAdd(invite: Invite): Boolean{ + val task = invite.task + try { + mainSchedule.getOrCreateDailySchedule(task.getTaskDate()).addDailyTask(task) + } catch (e: DailySchedule.NestedTaskException){ + _lastConflict = e.oldTask + return false + } + accept(invite) + return true + } + + private fun accept(invite: Invite) { + scheduleInviteWork(invite, InviteAction.Type.ACCEPT) + invites.remove(invite) + } + + fun decline(invite: Invite) { + scheduleInviteWork(invite, InviteAction.Type.REMOVE_INVITE) + invites.remove(invite) + } + fun addInvite(invite: Invite){//preview + invites.add(invite) + } + private fun scheduleInviteWork(invite: Invite, actionType: InviteAction.Type) { + val action = InviteAction( + type = actionType, + eventId = invite.id, + loginOrEmail = User.name + ) + val actionJson = Json.encodeToString(InviteAction.serializer(), action) + + val workRequest = OneTimeWorkRequestBuilder() + .setInputData(workDataOf(InviteAction.jsonName to actionJson)) + .build() + WorkManagerHolder.getInstance() + .enqueueUniqueWork( + "invite_${action.eventId}_${action.loginOrEmail}_${action.type}", + ExistingWorkPolicy.APPEND, + workRequest + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt index f1b9831..80d446f 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/ListViewModel.kt @@ -9,9 +9,9 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest import androidx.work.OneTimeWorkRequestBuilder import androidx.work.workDataOf -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlinx.datetime.DatePeriod @@ -24,12 +24,15 @@ import kotlinx.serialization.json.Json import org.hse.smartcalendar.data.DailySchedule import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskAction +import org.hse.smartcalendar.data.InviteAction import org.hse.smartcalendar.data.User import org.hse.smartcalendar.data.WorkManagerHolder import org.hse.smartcalendar.network.ApiClient import org.hse.smartcalendar.network.ChatTaskResponse import org.hse.smartcalendar.network.NetworkResponse import org.hse.smartcalendar.repository.AudioRepository +import org.hse.smartcalendar.repository.InviteRepository +import org.hse.smartcalendar.work.InviteApiWorker import org.hse.smartcalendar.work.TaskApiWorker import java.io.File @@ -46,11 +49,9 @@ open class AbstractListViewModel(val statisticsManager: StatisticsManager) : Vie ) val dailyTaskList: SnapshotStateList = mutableStateListOf() protected val user: User = User - init { - loadDailyTasks() - } fun loadDailyTasks(){ dailyTaskSchedule = user.getSchedule().getOrCreateDailySchedule(dailyScheduleDate.value) + dailyTaskList.clear() dailyTaskList.addAll(dailyTaskSchedule.getDailyTaskList()) } open fun scheduleTaskRequest(task: DailyTask, action: DailyTaskAction.Type) { @@ -145,17 +146,42 @@ open class AbstractListViewModel(val statisticsManager: StatisticsManager) : Vie class ListViewModel(statisticsManager: StatisticsManager) : AbstractListViewModel(statisticsManager) { private val workManager = WorkManagerHolder.getInstance() private val audioRepo = AudioRepository(ApiClient.audioApiService) + fun getInviteesRequestList(task: DailyTask): List{ + return task.getSharedInfo().invitees.map { loginOrEmail -> + val inviteJson = Json.encodeToString( + InviteAction.serializer(), + InviteAction( + type = InviteAction.Type.INVITE, + eventId = task.getId(), + loginOrEmail = loginOrEmail + ) + ) + OneTimeWorkRequestBuilder() + .setInputData(workDataOf(InviteAction.jsonName to inviteJson)) + .build() + } + } override fun scheduleTaskRequest(task: DailyTask, action: DailyTaskAction.Type) { val taskJson = Json.encodeToString(DailyTaskAction.serializer(), DailyTaskAction(task = task, type = action)) val workRequest = OneTimeWorkRequestBuilder() .setInputData(workDataOf(DailyTaskAction.jsonName to taskJson)) .build() - workManager.enqueueUniqueWork( - "task_${task.getId()}", - ExistingWorkPolicy.APPEND, - workRequest - ) + if (action== DailyTaskAction.Type.ADD && task.getSharedInfo().isShared){ + workManager.beginUniqueWork( + "task_${task.getId()}", + ExistingWorkPolicy.APPEND, + workRequest + ) + .then(getInviteesRequestList(task)) + .enqueue() + } else { + workManager.enqueueUniqueWork( + "task_${task.getId()}", + ExistingWorkPolicy.APPEND, + workRequest + ) + } } fun sendAudio( audioFile: MutableState, diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/TaskEditViewModel.kt b/app/src/main/java/org/hse/smartcalendar/view/model/TaskEditViewModel.kt index 2b90f0b..85881f6 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/TaskEditViewModel.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/TaskEditViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel import kotlinx.datetime.LocalTime import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskAction +import org.hse.smartcalendar.data.DailyTaskType import org.hse.smartcalendar.data.WorkManagerHolder import org.hse.smartcalendar.utility.editHandler @@ -12,12 +13,12 @@ class TaskEditViewModel( val listViewModel: ListViewModel ) : ViewModel() { private val workManager = WorkManagerHolder.getInstance() - private var task: DailyTask = DailyTask( + private var task: DailyTask = DailyTask.example( title = "Preview title", description = "Preview description", start = LocalTime(0, 0), end = LocalTime(23, 59), - date = DailyTask.defaultDate + type = DailyTaskType.COMMON ) val changes = task @@ -35,6 +36,7 @@ class TaskEditViewModel( isNestedTask: MutableState, reminderViewModel: ReminderViewModel ): Boolean { + changes.setDate(listViewModel.getScheduleDate()) return editHandler( oldTask = task, newTask = changes, diff --git a/app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsState.kt b/app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsState.kt index d3dbae4..a2ea2fe 100644 --- a/app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsState.kt +++ b/app/src/main/java/org/hse/smartcalendar/view/model/state/StatisticsState.kt @@ -8,11 +8,11 @@ import org.hse.smartcalendar.network.TodayTime import org.hse.smartcalendar.utility.TimeUtils data class StatisticsUiState( - val total: TotalTimeTaskTypes = TotalTimeTaskTypes(0,0,0,0), - val week: WeekTime = WeekTime(0), - val averageDay: AverageDayTimeVars = AverageDayTimeVars(firstDay = LocalDate(1970,1,1), totalWorkMinutes = 0), - val today: TodayTimeVars = TodayTimeVars(0,0), - val calculable: StatisticsCalculableData = StatisticsCalculableData(0, 0, 0) + val total: TotalTimeTaskTypes = TotalTimeTaskTypes(320, 180, 60, 90), + val week: WeekTime = WeekTime(650), + val averageDay: AverageDayTimeVars = AverageDayTimeVars(firstDay = LocalDate(2025, 6, 1), totalWorkMinutes = 870), + val today: TodayTimeVars = TodayTimeVars(100, 60), + val calculable: StatisticsCalculableData = StatisticsCalculableData(3, 4, 5) ) class TodayTimeVars(planned: Long, completed: Long){ diff --git a/app/src/main/java/org/hse/smartcalendar/work/InviteApiWorker.kt b/app/src/main/java/org/hse/smartcalendar/work/InviteApiWorker.kt new file mode 100644 index 0000000..58a78db --- /dev/null +++ b/app/src/main/java/org/hse/smartcalendar/work/InviteApiWorker.kt @@ -0,0 +1,36 @@ +package org.hse.smartcalendar.work + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import kotlinx.serialization.json.Json +import org.hse.smartcalendar.data.InviteAction +import org.hse.smartcalendar.network.ApiClient +import org.hse.smartcalendar.network.NetworkResponse +import org.hse.smartcalendar.repository.InviteRepository + +class InviteApiWorker( + context: Context, + workerParams: WorkerParameters +) : CoroutineWorker(context, workerParams) { + + private val repo = InviteRepository(ApiClient.inviteApiService) + + override suspend fun doWork(): Result { + val json = inputData.getString(InviteAction.jsonName) + ?: return Result.failure() + val action = Json.decodeFromString(InviteAction.serializer(), json) + + val success = when (action.type) { + InviteAction.Type.ACCEPT -> repo.acceptInvite(action.eventId) + InviteAction.Type.INVITE -> repo.inviteUser(action.eventId, action.loginOrEmail) + InviteAction.Type.REMOVE_INVITE -> repo.removeInvite( + action.eventId, + action.loginOrEmail + ) + }.let { + it is NetworkResponse.Success + } + return if (success) Result.success() else Result.retry() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/hse/smartcalendar/work/TaskApiWorker.kt b/app/src/main/java/org/hse/smartcalendar/work/TaskApiWorker.kt index 57ccc82..1847071 100644 --- a/app/src/main/java/org/hse/smartcalendar/work/TaskApiWorker.kt +++ b/app/src/main/java/org/hse/smartcalendar/work/TaskApiWorker.kt @@ -5,6 +5,7 @@ import androidx.work.WorkerParameters import kotlinx.serialization.json.Json import org.hse.smartcalendar.data.DailyTaskAction import org.hse.smartcalendar.network.ApiClient +import org.hse.smartcalendar.repository.InviteRepository import org.hse.smartcalendar.repository.TaskRepository class TaskApiWorker( diff --git a/app/src/test/kotlin/org/hse/smartcalendar/view/model/TaskProvider.kt b/app/src/test/kotlin/org/hse/smartcalendar/view/model/TaskProvider.kt index 5cd2ade..4ea77d1 100644 --- a/app/src/test/kotlin/org/hse/smartcalendar/view/model/TaskProvider.kt +++ b/app/src/test/kotlin/org/hse/smartcalendar/view/model/TaskProvider.kt @@ -4,6 +4,7 @@ import org.hse.smartcalendar.data.DailyTask import org.hse.smartcalendar.data.DailyTaskType import org.hse.smartcalendar.utility.TimeUtils import kotlinx.datetime.LocalTime +import org.hse.smartcalendar.data.SharedInfo import org.hse.smartcalendar.utility.fromMinutesOfDay import java.util.UUID @@ -23,7 +24,8 @@ enum class TaskProvider(val provide:()-> DailyTask) { description = "", start = LocalTime.fromMinutesOfDay(10), end = LocalTime.fromMinutesOfDay(30), - date = TimeUtils.getCurrentDateTime().date + date = TimeUtils.getCurrentDateTime().date, + sharedInfo = SharedInfo() ) }), diff --git a/build.gradle.kts b/build.gradle.kts index c205465..b24291a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,5 +7,6 @@ plugins { buildscript { dependencies { classpath(libs.android.junit5) + classpath(libs.google.services) } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c088385..650a371 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,6 +5,8 @@ accompanistVersion = "0.34.0" agp = "8.8.1" androidJunit5 = "1.8.2.1" converterGson = "2.9.0" +firebaseMessaging = "24.1.1" +googleServices = "4.4.2" hiltAndroid = "2.52" hiltAndroidVersion = "2.48.1" hiltCompiler = "2.48.1" @@ -19,6 +21,7 @@ junitVersion = "1.2.1" espressoCore = "3.6.1" kotlinTest = "1.9.23" kotlinTestJunit5 = "1.9.23" +kotlinxCoroutinesPlayServices = "1.7.3" kotlinxCoroutinesTest = "1.7.3" kotlinxDatetime = "0.6.2" kotlinxSerializationJson = "1.6.3" @@ -29,6 +32,7 @@ composeBom = "2024.04.01" lifecycleRuntimeComposeAndroid = "2.8.7" loggingInterceptor = "4.12.0" loggingInterseptor = "4.7.2" +material = "1.8.2" mockk = "1.13.10" navigationCompose = "2.8.8" composeMaterial3 = "1.0.0-alpha33" @@ -53,6 +57,7 @@ android-junit5 = { module = "de.mannodermaus.gradle.plugins:android-junit5", ver androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } accompanist-permissions-v0340 = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanistVersion" } androidx-lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycleProcessVersion" } +androidx-material = { module = "androidx.compose.material:material", version.ref = "material" } androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-navigation-compose-v240beta02 = { module = "androidx.navigation:navigation-compose", version.ref = "navigationComposeVersion" } @@ -60,6 +65,8 @@ androidx-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedat androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" } converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" } converter-gson-v2110 = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofitVersion" } +firebase-messaging = { module = "com.google.firebase:firebase-messaging", version.ref = "firebaseMessaging" } +google-services = { module = "com.google.gms:google-services", version.ref = "googleServices" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroid" } hilt-android-v2481 = { module = "com.google.dagger:hilt-android", version.ref = "hiltAndroidVersion" } hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hiltCompiler" } @@ -82,6 +89,7 @@ junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", vers junit-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "junitVintageEngine" } kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlinTest" } kotlin-test-junit5 = { module = "org.jetbrains.kotlin:kotlin-test-junit5", version.ref = "kotlinTestJunit5" } +kotlinx-coroutines-play-services = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "kotlinxCoroutinesPlayServices" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } androidx-compose-material3 = { group = "androidx.wear.compose", name = "compose-material3", version.ref = "composeMaterial3" }