diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e5866be..76abb63 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: run: chmod +x gradlew - name: Build with Gradle - run: ./gradlew build + run: ./gradlew assembleRelease - name: Publish Release uses: softprops/action-gh-release@v1 diff --git a/app/src/main/kotlin/com/zjutjh/ijh/ui/component/CampusCardInfoCard.kt b/app/src/main/kotlin/com/zjutjh/ijh/ui/component/CampusCardInfoCard.kt index c29dc91..f03073a 100644 --- a/app/src/main/kotlin/com/zjutjh/ijh/ui/component/CampusCardInfoCard.kt +++ b/app/src/main/kotlin/com/zjutjh/ijh/ui/component/CampusCardInfoCard.kt @@ -21,18 +21,19 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.zjutjh.ijh.R import com.zjutjh.ijh.ui.theme.IJhTheme +import com.zjutjh.ijh.util.LoadResult import com.zjutjh.ijh.util.toLocalizedString import java.time.Duration @Composable fun CampusCardInfoCard( modifier: Modifier = Modifier, - balance: String?, - lastSyncDuration: Duration? + balance: LoadResult, + lastSync: LoadResult, ) { val context = LocalContext.current - val subtitle = remember(lastSyncDuration) { - prompt(context, lastSyncDuration) + val subtitle = remember(lastSync) { + prompt(context, lastSync) } GlanceCard( @@ -50,7 +51,7 @@ fun CampusCardInfoCard( label = "Loading", ) { when (it) { - null -> Column( + is LoadResult.Loading -> Column( horizontalAlignment = Alignment.CenterHorizontally ) { CircularProgressIndicator() @@ -60,11 +61,17 @@ fun CampusCardInfoCard( ) } - else -> { + is LoadResult.Ready -> { + val text = if (it.data == null) { + "N/A" + } else { + "¥${it.data}" + } + Text( modifier = Modifier.padding(vertical = 8.dp), color = MaterialTheme.colorScheme.primary, - text = "¥$it", + text = text, style = MaterialTheme.typography.displaySmall, textAlign = TextAlign.Center, maxLines = 1, @@ -75,15 +82,21 @@ fun CampusCardInfoCard( } } -private fun prompt(context: Context, lastSyncDuration: Duration?) = +private fun prompt(context: Context, lastSyncDuration: LoadResult) = buildString { val separator = " • " append(context.getString(R.string.balance)) append(separator) - if (lastSyncDuration != null) { - append(lastSyncDuration.toLocalizedString(context)) - } else { - append(context.getString(R.string.never)) + when (lastSyncDuration) { + is LoadResult.Loading -> append(context.getString(R.string.unknown)) + is LoadResult.Ready -> { + val duration = lastSyncDuration.data + if (duration != null) { + append(duration.toLocalizedString(context)) + } else { + append(context.getString(R.string.never)) + } + } } } @@ -92,7 +105,10 @@ private fun prompt(context: Context, lastSyncDuration: Duration?) = @Composable private fun CampusCardInfoCardPreview() { IJhTheme { - CampusCardInfoCard(balance = "123", lastSyncDuration = Duration.ofDays(1)) + CampusCardInfoCard( + balance = LoadResult.Ready("123"), + lastSync = LoadResult.Ready(Duration.ofSeconds(10)) + ) } } @@ -100,6 +116,6 @@ private fun CampusCardInfoCardPreview() { @Composable private fun CampusCardInfoCardPreviewEmpty() { IJhTheme { - CampusCardInfoCard(balance = null, lastSyncDuration = null) + CampusCardInfoCard(balance = LoadResult.Loading, lastSync = LoadResult.Loading) } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/zjutjh/ijh/ui/component/ScheduleCard.kt b/app/src/main/kotlin/com/zjutjh/ijh/ui/component/ScheduleCard.kt index 979b55b..8d3dd4e 100644 --- a/app/src/main/kotlin/com/zjutjh/ijh/ui/component/ScheduleCard.kt +++ b/app/src/main/kotlin/com/zjutjh/ijh/ui/component/ScheduleCard.kt @@ -50,13 +50,13 @@ fun ScheduleCard( modifier: Modifier = Modifier, courses: List?, termDay: TermDayState?, - lastSyncDuration: Duration?, + lastSync: Duration?, onButtonClick: () -> Unit, ) { val context = LocalContext.current - val subtitle = remember(termDay, lastSyncDuration) { - prompt(context, termDay, lastSyncDuration) + val subtitle = remember(termDay, lastSync) { + prompt(context, termDay, lastSync) } GlanceCard( @@ -226,7 +226,7 @@ private fun ScheduleSurfacePreview() { courses = CourseRepositoryMock.getCourses(), onButtonClick = {}, termDay = termDay, - lastSyncDuration = Duration.ofDays(2), + lastSync = Duration.ofDays(2), ) } } @@ -242,7 +242,7 @@ private fun ScheduleSurfaceEmptyPreview() { courses = emptyList(), onButtonClick = {}, termDay = null, - lastSyncDuration = Duration.ofDays(1), + lastSync = Duration.ofDays(1), ) } } @@ -258,7 +258,7 @@ private fun ScheduleSurfaceLoadingPreview() { courses = null, onButtonClick = {}, termDay = null, - lastSyncDuration = Duration.ofDays(1), + lastSync = Duration.ofDays(1), ) } } diff --git a/app/src/main/kotlin/com/zjutjh/ijh/ui/screen/HomeScreen.kt b/app/src/main/kotlin/com/zjutjh/ijh/ui/screen/HomeScreen.kt index af99ac9..5d662db 100644 --- a/app/src/main/kotlin/com/zjutjh/ijh/ui/screen/HomeScreen.kt +++ b/app/src/main/kotlin/com/zjutjh/ijh/ui/screen/HomeScreen.kt @@ -78,6 +78,7 @@ fun HomeRoute( val termDayState by viewModel.termDayState.collectAsStateWithLifecycle() val coursesLastSyncState by viewModel.courseLastSyncState.collectAsStateWithLifecycle() val cardBalanceState by viewModel.cardBalanceState.collectAsStateWithLifecycle() + val cardBalanceLastSyncState by viewModel.cardBalanceLastSyncState.collectAsStateWithLifecycle() val isLoggedIn = when (loginState) { null -> false @@ -99,8 +100,9 @@ fun HomeRoute( isLoggedIn = isLoggedIn, courses = courses, termDay = termDay, - coursesLastSyncDuration = coursesLastSyncState, + coursesLastSync = coursesLastSyncState, cardBalance = cardBalanceState, + cardBalanceLastSync = cardBalanceLastSyncState, onRefresh = viewModel::refreshAll, onNavigateToLogin = onNavigateToLogin, onNavigateToProfile = onNavigateToProfile, @@ -115,8 +117,9 @@ private fun HomeScreen( isLoggedIn: Boolean?, courses: List?, termDay: TermDayState?, - coursesLastSyncDuration: Duration?, - cardBalance: Pair?, + coursesLastSync: Duration?, + cardBalance: LoadResult, + cardBalanceLastSync: LoadResult, onRefresh: () -> Unit, onNavigateToLogin: () -> Unit, onNavigateToProfile: () -> Unit, @@ -151,12 +154,12 @@ private fun HomeScreen( courses = courses, termDay = termDay, onButtonClick = onNavigateToCalendar, - lastSyncDuration = coursesLastSyncDuration, + lastSync = coursesLastSync, ) CampusCardInfoCard( modifier = modifier, - balance = cardBalance?.first, - lastSyncDuration = cardBalance?.second + balance = cardBalance, + lastSync = cardBalanceLastSync, ) } @@ -350,8 +353,9 @@ private fun HomeScreenPreview() { isLoggedIn = true, courses = courses, termDay = termDay, - coursesLastSyncDuration = Duration.ofDays(1), - cardBalance = "123" to Duration.ofDays(2), + coursesLastSync = Duration.ofDays(1), + cardBalance = LoadResult.Ready("123"), + cardBalanceLastSync = LoadResult.Ready(Duration.ofDays(2)), onRefresh = ::emptyFun, onNavigateToAbout = ::emptyFun, onNavigateToCalendar = ::emptyFun, diff --git a/app/src/main/kotlin/com/zjutjh/ijh/ui/viewmodel/HomeViewModel.kt b/app/src/main/kotlin/com/zjutjh/ijh/ui/viewmodel/HomeViewModel.kt index bf4f753..7876db3 100644 --- a/app/src/main/kotlin/com/zjutjh/ijh/ui/viewmodel/HomeViewModel.kt +++ b/app/src/main/kotlin/com/zjutjh/ijh/ui/viewmodel/HomeViewModel.kt @@ -42,9 +42,7 @@ class HomeViewModel @Inject constructor( .distinctUntilChanged() .combine(timerFlow) { t1, _ -> t1 } .map { - if (it == null) null else { - Duration.between(it, ZonedDateTime.now()) - } + it?.let { Duration.between(it, ZonedDateTime.now()) } } .flowOn(Dispatchers.Default) .stateIn( @@ -73,7 +71,6 @@ class HomeViewModel @Inject constructor( .mapLatest { it?.toTermDayState() } - .flowOn(Dispatchers.Default) .asLoadResultStateFlow( scope = viewModelScope, started = SharingStarted.WhileSubscribed(), @@ -91,27 +88,30 @@ class HomeViewModel @Inject constructor( } else flowOf(emptyList()) } else flowOf(emptyList()) } - .flowOn(Dispatchers.Default) .asLoadResultStateFlow( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000) ) - val cardBalanceState: StateFlow?> = cardInfoRepository.balanceStream + val cardBalanceState: StateFlow> = cardInfoRepository.balanceStream .distinctUntilChanged() - .combine(timerFlow) { t, _ -> t } - .map { - if (it == null) null else { - Pair(it.first, Duration.between(it.second, ZonedDateTime.now())) - } - } - .flowOn(Dispatchers.Default) - .stateIn( + .asLoadResultStateFlow( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), - initialValue = null ) + val cardBalanceLastSyncState: StateFlow> = + cardInfoRepository.lastSyncTimeStream + .distinctUntilChanged() + .combine(timerFlow) { t1, _ -> t1 } + .map { + it?.let { Duration.between(it, ZonedDateTime.now()) } + } + .asLoadResultStateFlow( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000) + ) + init { viewModelScope.launch(Dispatchers.Default) { // Check session state, renew if needed. TODO: move to application scope @@ -131,7 +131,7 @@ class HomeViewModel @Inject constructor( } } } - // Subscribe latest login state, and trigger refresh. + //Subscribe latest login state, and trigger refresh. loginState.collectLatest { refreshAll(this) } @@ -155,19 +155,25 @@ class HomeViewModel @Inject constructor( _refreshState.update { true } val timer = scope.async { delay(300L) } - val term = refreshTerm() - val isLoggedIn = loginState.value - if (isLoggedIn != null) { - if (term != null) { - refreshCourse(term.first, term.second) + val tasks = if (isLoggedIn != null) { + val task1 = scope.async { + val term = refreshTerm() + if (term != null) { + refreshCourse(term.first, term.second) + } } - refreshCard() + val task2 = scope.async { + refreshCard() + } + mutableListOf(task1, task2) + } else { + mutableListOf() } + tasks.add(timer) + awaitAll(deferreds = tasks.toTypedArray()) Log.i("Home", "Synchronization complete.") - - timer.await() _refreshState.update { false } } diff --git a/app/src/main/kotlin/com/zjutjh/ijh/util/LoadResult.kt b/app/src/main/kotlin/com/zjutjh/ijh/util/LoadResult.kt index fe6fb48..7735ed8 100644 --- a/app/src/main/kotlin/com/zjutjh/ijh/util/LoadResult.kt +++ b/app/src/main/kotlin/com/zjutjh/ijh/util/LoadResult.kt @@ -1,15 +1,20 @@ package com.zjutjh.ijh.util +import androidx.compose.runtime.Stable import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn +import kotlin.coroutines.CoroutineContext +@Stable sealed interface LoadResult { data object Loading : LoadResult class Ready(val data: T) : LoadResult @@ -23,6 +28,12 @@ sealed interface LoadResult { if (this is Ready && v is Ready) { areEquivalent(this.data, v.data) } else this is Loading && v is Loading + + fun map(transform: (T) -> R): LoadResult = + when (this) { + is Loading -> Loading + is Ready -> Ready(transform(data)) + } } fun LoadResult.isLoading(): Boolean = this is LoadResult.Loading @@ -36,6 +47,7 @@ fun Flow.asLoadResultFlow(): Flow> = this .onStart { emit(LoadResult.Loading) } fun Flow.asLoadResultSharedFlow( + context: CoroutineContext = Dispatchers.Default, scope: CoroutineScope, started: SharingStarted ): SharedFlow> = this @@ -43,6 +55,7 @@ fun Flow.asLoadResultSharedFlow( LoadResult.Ready(it) } .onStart { emit(LoadResult.Loading) } + .flowOn(context) .shareIn( scope = scope, started = started, @@ -51,12 +64,14 @@ fun Flow.asLoadResultSharedFlow( fun Flow.asLoadResultStateFlow( + context: CoroutineContext = Dispatchers.Default, scope: CoroutineScope, started: SharingStarted ): StateFlow> = this .map> { LoadResult.Ready(it) } + .flowOn(context) .stateIn( scope = scope, started = started, diff --git a/core/data/src/main/kotlin/com/zjutjh/ijh/data/CardInfoRepository.kt b/core/data/src/main/kotlin/com/zjutjh/ijh/data/CardInfoRepository.kt index 8aaf26d..2323420 100644 --- a/core/data/src/main/kotlin/com/zjutjh/ijh/data/CardInfoRepository.kt +++ b/core/data/src/main/kotlin/com/zjutjh/ijh/data/CardInfoRepository.kt @@ -8,9 +8,11 @@ import java.util.Date interface CardInfoRepository { /** - * [Pair]: balance (Unit: CNY) in string, last sync time + * balance (Unit: CNY) in string */ - val balanceStream: Flow?> + val balanceStream: Flow + + val lastSyncTimeStream: Flow suspend fun sync() diff --git a/core/data/src/main/kotlin/com/zjutjh/ijh/data/impl/CardInfoRepositoryImpl.kt b/core/data/src/main/kotlin/com/zjutjh/ijh/data/impl/CardInfoRepositoryImpl.kt index 8d599b7..492e618 100644 --- a/core/data/src/main/kotlin/com/zjutjh/ijh/data/impl/CardInfoRepositoryImpl.kt +++ b/core/data/src/main/kotlin/com/zjutjh/ijh/data/impl/CardInfoRepositoryImpl.kt @@ -19,10 +19,12 @@ class CardInfoRepositoryImpl @Inject constructor( private val local: WeJhPreferenceDataSource ) : CardInfoRepository { - override val balanceStream: Flow?> = local.data.map { - it.cardOrNull?.let { card -> - Pair(card.balance, card.lastSyncTime.toZonedDateTime()) - } + override val balanceStream: Flow = local.data.map { + it.cardOrNull?.balance + } + + override val lastSyncTimeStream: Flow = local.data.map { + it.cardOrNull?.lastSyncTime?.toZonedDateTime() } override suspend fun sync() {