Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package com.example.hangsha_android.data.local

import android.content.Context
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.example.hangsha_android.data.network.model.TimetableListResponse
import com.example.hangsha_android.data.network.model.TimetableResponse
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.IOException
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map

private val Context.guestTimetablesDataStore by preferencesDataStore(name = "guest_timetables")

@Singleton
class GuestTimetableLocalDataSource @Inject constructor(
@param:ApplicationContext private val context: Context
) {
private val gson = Gson()

val timetables: Flow<List<StoredGuestTimetable>> =
context.guestTimetablesDataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences -> parseTimetables(preferences[GUEST_TIMETABLES_JSON]) }

suspend fun getTimetables(
year: Int,
semester: String
): TimetableListResponse {
return TimetableListResponse(
items = timetables.first()
.filter { timetable -> timetable.year == year && timetable.semester == semester }
.map { timetable -> timetable.toTimetableResponse() }
)
}

suspend fun createTimetable(
name: String,
year: Int,
semester: String
): TimetableResponse {
val currentTimetables = timetables.first()
val nextId = (currentTimetables.minOfOrNull { timetable -> timetable.id } ?: 0L) - 1L
val timetable = StoredGuestTimetable(
id = nextId,
name = name.trim(),
year = year,
semester = semester
)
replaceAll(currentTimetables + timetable)
return timetable.toTimetableResponse()
}

suspend fun updateTimetableName(
timetableId: Long,
name: String
): TimetableResponse {
val currentTimetables = timetables.first()
val target = currentTimetables.firstOrNull { timetable -> timetable.id == timetableId }
?: throw NoSuchElementException("Timetable was not found.")
val updatedTimetable = target.copy(name = name.trim())
replaceAll(currentTimetables.map { timetable ->
if (timetable.id == timetableId) updatedTimetable else timetable
})
return updatedTimetable.toTimetableResponse()
}
suspend fun deleteTimetable(timetableId: Long) {
replaceAll(timetables.first().filterNot { timetable -> timetable.id == timetableId })
}
private suspend fun replaceAll(items: List<StoredGuestTimetable>) {
context.guestTimetablesDataStore.edit { preferences ->
preferences[GUEST_TIMETABLES_JSON] = gson.toJson(
items
.filter { timetable -> timetable.name.isNotBlank() }
.distinctBy { timetable -> timetable.id }
.sortedWith(compareBy({ timetable -> timetable.year }, { timetable -> timetable.semester }, { timetable -> timetable.id }))
)
}
}

private fun parseTimetables(json: String?): List<StoredGuestTimetable> {
if (json.isNullOrBlank()) {
return emptyList()
}

return runCatching {
gson.fromJson<List<StoredGuestTimetable>>(json, storedTimetableListType)
}.getOrDefault(emptyList())
}

companion object {
private val GUEST_TIMETABLES_JSON = stringPreferencesKey("guest_timetables_json")
private val storedTimetableListType = object : TypeToken<List<StoredGuestTimetable>>() {}.type
}
}

data class StoredGuestTimetable(
val id: Long,
val name: String,
val year: Int,
val semester: String
)

private fun StoredGuestTimetable.toTimetableResponse(): TimetableResponse {
return TimetableResponse(
id = id,
name = name,
year = year,
semester = semester
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.example.hangsha_android.data.network.api

import com.example.hangsha_android.data.network.model.CreateTimetableRequest
import com.example.hangsha_android.data.network.model.TimetableListResponse
import com.example.hangsha_android.data.network.model.TimetableResponse
import com.example.hangsha_android.data.network.model.UpdateTimetableRequest
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.PATCH
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query

interface TimetableApi {
@GET("api/v1/timetables")
suspend fun getTimetables(
@Query("year") year: Int,
@Query("semester") semester: String
): Response<TimetableListResponse>

@PATCH("api/v1/timetables/{timetableId}")
suspend fun updateTimetable(
@Path("timetableId") timetableId: Long,
@Body request: UpdateTimetableRequest
): Response<TimetableResponse>

@DELETE("api/v1/timetables/{timetableId}")
suspend fun deleteTimetable(
@Path("timetableId") timetableId: Long
): Response<Unit>

@POST("api/v1/timetables")
suspend fun createTimetable(
@Body request: CreateTimetableRequest
): Response<TimetableResponse>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.hangsha_android.data.network.model

import com.google.gson.annotations.SerializedName

data class CreateTimetableRequest(
@SerializedName("name")
val name: String,
@SerializedName("year")
val year: Int,
@SerializedName("semester")
val semester: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example.hangsha_android.data.network.model

import com.google.gson.annotations.SerializedName

data class TimetableListResponse(
@SerializedName("items")
val items: List<TimetableResponse>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.hangsha_android.data.network.model

import com.google.gson.annotations.SerializedName

data class TimetableResponse(
@SerializedName("id")
val id: Long,
@SerializedName("name")
val name: String,
@SerializedName("year")
val year: Int,
@SerializedName("semester")
val semester: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example.hangsha_android.data.network.model

import com.google.gson.annotations.SerializedName

data class UpdateTimetableRequest(
@SerializedName("name")
val name: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.example.hangsha_android.data.repository

import com.example.hangsha_android.data.local.AuthTokenStorage
import com.example.hangsha_android.data.local.GuestTimetableLocalDataSource
import com.example.hangsha_android.data.network.api.TimetableApi
import com.example.hangsha_android.data.network.model.CreateTimetableRequest
import com.example.hangsha_android.data.network.model.TimetableListResponse
import com.example.hangsha_android.data.network.model.TimetableResponse
import com.example.hangsha_android.data.network.model.UpdateTimetableRequest
import javax.inject.Inject
import javax.inject.Singleton
import retrofit2.Response

@Singleton
class TimetableRepository @Inject constructor(
private val timetableApi: TimetableApi,
private val localDataSource: GuestTimetableLocalDataSource,
private val authTokenStorage: AuthTokenStorage
) {
suspend fun getTimetables(
year: Int,
semester: String
): Response<TimetableListResponse> {
if (!isLoggedIn()) {
return Response.success(
localDataSource.getTimetables(
year = year,
semester = semester
)
)
}

return timetableApi.getTimetables(
year = year,
semester = semester
)
}

suspend fun createTimetable(
name: String,
year: Int,
semester: String
): Response<TimetableResponse> {
if (!isLoggedIn()) {
return Response.success(
localDataSource.createTimetable(
name = name,
year = year,
semester = semester
)
)
}

return timetableApi.createTimetable(
request = CreateTimetableRequest(
name = name,
year = year,
semester = semester
)
)
}

suspend fun updateTimetableName(
timetableId: Long,
name: String
): Response<TimetableResponse> {
val normalizedName = name.trim()
if (!isLoggedIn()) {
return Response.success(
localDataSource.updateTimetableName(
timetableId = timetableId,
name = normalizedName
)
)
}

return timetableApi.updateTimetable(
timetableId = timetableId,
request = UpdateTimetableRequest(name = normalizedName)
)
}
suspend fun deleteTimetable(timetableId: Long): Response<Unit> {
if (!isLoggedIn()) {
localDataSource.deleteTimetable(timetableId)
return Response.success(Unit)
}

return timetableApi.deleteTimetable(timetableId = timetableId)
}
private fun isLoggedIn(): Boolean {
return authTokenStorage.hasAccessToken()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.example.hangsha_android.data.network.api.EventApi
import com.example.hangsha_android.data.network.api.ExcludedKeywordsApi
import com.example.hangsha_android.data.network.api.MemoApi
import com.example.hangsha_android.data.network.api.ServerHealthApi
import com.example.hangsha_android.data.network.api.TimetableApi
import com.example.hangsha_android.data.network.api.UserApi
import dagger.Module
import dagger.Provides
Expand Down Expand Up @@ -176,6 +177,14 @@ object NetworkModule {
return retrofit.create(CategoryApi::class.java)
}

@Provides
@Singleton
fun provideTimetableApi(
@Named("app") retrofit: Retrofit
): TimetableApi {
return retrofit.create(TimetableApi::class.java)
}

private val AUTH_PATH_PREFIXES = listOf(
"/api/v1/mobile/auth/",
"/api/v1/auth/"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import com.example.hangsha_android.ui.view.onboarding.OnboardingViewModel
import com.example.hangsha_android.ui.view.onboarding.OnboardingWelcomeScreen
import com.example.hangsha_android.ui.view.signup.SignUpScreen
import com.example.hangsha_android.ui.view.signup.SignUpViewModel
import com.example.hangsha_android.ui.view.timetable.TimetableScreen
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInOptions
import com.google.android.gms.common.api.ApiException
Expand Down Expand Up @@ -528,7 +529,7 @@ fun NavGraphBuilder.mainGraph(navController: NavHostController) {
)
}
composable(BottomTab.Timetable.route) {
SimplePageText("timetable")
TimetableScreen()
}
composable(BottomTab.Bookmarks.route) {
val authStateViewModel: AuthStateViewModel = hiltViewModel()
Expand Down
Loading
Loading