From a5f8083d688a8dc0bd7d0e39fc7b7d600fa5dc4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BA=BF=20T=C3=B9ng?= Date: Sun, 3 Mar 2024 00:30:14 +0700 Subject: [PATCH 01/21] Update project - Move all DUT account session to new class for easier manage. --- .../dutschedule/activity/MainActivity.kt | 14 +- .../dutschedule/model/DUTAccountSession.kt | 262 +++++++++++++++--- .../dutschedule/model/account/AccountAuth.kt | 4 + .../model/account/AccountSession.kt | 4 + .../repository/DutRequestRepository.kt | 2 +- .../ui/view/account/AccountInformation.kt | 19 +- .../dutschedule/ui/view/account/MainView.kt | 76 +++-- .../dutschedule/ui/view/account/SubjectFee.kt | 29 +- .../ui/view/account/SubjectInformation.kt | 29 +- .../ui/view/account/TrainingResult.kt | 27 +- .../ui/view/account/TrainingSubjectResult.kt | 30 +- .../ui/view/main/MainViewDashboard.kt | 10 +- .../dutschedule/viewmodel/MainViewModel.kt | 239 ++-------------- 13 files changed, 335 insertions(+), 410 deletions(-) diff --git a/app/src/main/java/io/zoemeow/dutschedule/activity/MainActivity.kt b/app/src/main/java/io/zoemeow/dutschedule/activity/MainActivity.kt index c891788..f9ca699 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/activity/MainActivity.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/activity/MainActivity.kt @@ -69,7 +69,7 @@ class MainActivity : BaseActivity() { contentColor = contentColor, notificationList = getMainViewModel().notificationHistory, notificationClicked = { - // TODO: Notification list requested + // Notification list requested notificationSheetScope.launch { if (!isNotificationOpened.value) { isNotificationOpened.value = true @@ -100,11 +100,11 @@ class MainActivity : BaseActivity() { ) LessonTodaySummaryItem( padding = PaddingValues(bottom = 10.dp, start = 15.dp, end = 15.dp), - hasLoggedIn = getMainViewModel().accountSession.value.processState == ProcessState.Successful, - isLoading = getMainViewModel().accountSession.value.processState == ProcessState.Running || getMainViewModel().subjectSchedule.processState.value == ProcessState.Running, + hasLoggedIn = getMainViewModel().accountSession.accountSession.processState.value == ProcessState.Successful, + isLoading = getMainViewModel().accountSession.accountSession.processState.value == ProcessState.Running || getMainViewModel().accountSession.subjectSchedule.processState.value == ProcessState.Running, clicked = { - getMainViewModel().accountLogin( - after = { + getMainViewModel().accountSession.reLogin( + onCompleted = { if (it) { val intent = Intent(context, AccountActivity::class.java) intent.action = "subject_schedule" @@ -113,12 +113,12 @@ class MainActivity : BaseActivity() { } ) }, - affectedList = getMainViewModel().subjectSchedule.data.value?.filter { subSch -> + affectedList = getMainViewModel().accountSession.subjectSchedule.data.filter { subSch -> subSch.subjectStudy.scheduleList.any { schItem -> schItem.dayOfWeek + 1 == CustomDateUtil.getCurrentDayOfWeek() } && subSch.subjectStudy.scheduleList.any { schItem -> schItem.lesson.end >= CustomClock.getCurrent().toDUTLesson2().lesson } - }?.toList() ?: listOf(), + }.toList(), opacity = getControlBackgroundAlpha() ) // AffectedLessonsSummaryItem( diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/DUTAccountSession.kt b/app/src/main/java/io/zoemeow/dutschedule/model/DUTAccountSession.kt index 31d25d5..8777c89 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/DUTAccountSession.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/DUTAccountSession.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.model +import android.util.Log import androidx.compose.runtime.MutableLongState import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableLongStateOf @@ -14,14 +15,33 @@ import io.zoemeow.dutschedule.model.account.AccountAuth import io.zoemeow.dutschedule.model.account.AccountSession import io.zoemeow.dutschedule.model.account.SchoolYearItem import io.zoemeow.dutschedule.repository.DutRequestRepository +import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class DUTAccountSession { +/** + * @param onEventSent Event when done: + * 1: Login/Logout + * 2: Subject schedule + * 3: Subject fee + * 4: Account information + * 5: Account training status + */ +class DUTAccountSession( + private val dutRequestRepository: DutRequestRepository, + private val onEventSent: ((Int) -> Unit)? = null +) { + val accountSession: VariableState = VariableState(data = mutableStateOf(null)) + val schoolYear: MutableState = mutableStateOf(null) + val subjectSchedule: VariableListState = VariableListState() + val subjectFee: VariableListState = VariableListState() + val accountInformation: VariableState = VariableState(data = mutableStateOf(null)) + val accountTrainingStatus: VariableState = VariableState(data = mutableStateOf(null)) + data class VariableState( - val data: MutableState, + val data: MutableState, val lastRequest: MutableLongState = mutableLongStateOf(0), val processState: MutableState = mutableStateOf(ProcessState.NotRunYet) ) { @@ -35,6 +55,14 @@ class DUTAccountSession { else -> true } } + + fun resetValue() { + if (processState.value != ProcessState.Running) { + data.value = null + lastRequest.longValue = 0 + processState.value = ProcessState.NotRunYet + } + } } data class VariableListState( @@ -52,33 +80,25 @@ class DUTAccountSession { else -> true } } - } - val accountSession: MutableState = mutableStateOf(null) - val subjectSchedule: VariableListState = VariableListState() - val subjectFee: VariableListState = VariableListState() - val accountInformation: VariableState = VariableState(data = mutableStateOf(null)) - val accountTrainingStatus: VariableState = VariableState(data = mutableStateOf(null)) - - val dutRequestRepository: DutRequestRepository - val schoolYear: MutableState - - constructor( - accountSession: AccountSession?, - dutRequestRepository: DutRequestRepository, - schoolYear: SchoolYearItem - // TODO: Trigger event here! - ) { - this.accountSession.value = accountSession - this.dutRequestRepository = dutRequestRepository - this.schoolYear = mutableStateOf(schoolYear) + fun resetValue() { + if (processState.value != ProcessState.Running) { + data.clear() + lastRequest.longValue = 0 + processState.value = ProcessState.NotRunYet + } + } } private fun launchOnScope( script: () -> Unit, onCompleted: ((Throwable?) -> Unit)? = null ) { - CoroutineScope(Dispatchers.Main).launch { + val handler = CoroutineExceptionHandler { _, throwable -> + onCompleted?.let { it(throwable) } + } + + CoroutineScope(Dispatchers.Main).launch(handler) { withContext(Dispatchers.IO) { script() } @@ -87,20 +107,183 @@ class DUTAccountSession { } } - fun login(accountAuth: AccountAuth? = null) { - // If accountSession is exist, let's re-login. + private fun checkVariable(): Boolean { + return when { + this.accountSession.data.value == null -> false + this.schoolYear.value == null -> false + else -> true + } + } + + fun getAccountSession(): AccountSession? { + return this.accountSession.data.value + } + + fun setAccountSession(accountSession: AccountSession) { + if (this.accountSession.processState.value == ProcessState.Running) { + return + } + + this.accountSession.data.value = accountSession.clone() + } + + fun getSubjectScheduleCache(): List { + return this.subjectSchedule.data.toList() + } + + fun setSchoolYear(schoolYearItem: SchoolYearItem) { + this.schoolYear.value = schoolYearItem + } + + /** + * Login account to this application. + * @param accountAuth Your login. + * @param onCompleted Return a bool value which this request has done correctly. + */ + fun login( + accountAuth: AccountAuth? = null, + force: Boolean = true, + onCompleted: ((Boolean) -> Unit)? = null + ) { + if (accountSession.processState.value == ProcessState.Running) { + return + } + accountSession.processState.value = ProcessState.Running + + launchOnScope( + script = { + // If accountAuth isn't null, just login with new account + if (accountAuth != null) { + Log.d("login", "new account") + accountSession.data.value = AccountSession( + accountAuth = accountAuth.clone() + ) + dutRequestRepository.login( + accountSession = accountSession.data.value!!, + forceLogin = false, + onSessionChanged = { sId, dateUnix -> + if (dateUnix == null || dateUnix == 0L || sId == null) { + // TODO: Account session isn't valid! + throw Exception() + } else { + accountSession.data.value = accountSession.data.value!!.clone( + accountAuth = accountSession.data.value!!.accountAuth, + sessionId = sId, + sessionLastRequest = dateUnix + ) + } + } + ) + } + // If accountSession is exist, let's re-login. + else if (accountSession.data.value != null) { + Log.d("login", "have account") + // Check if logged in + // If so, return to accountSession + if (!accountSession.data.value!!.isValidLogin()) { + throw Exception() + } + dutRequestRepository.login( + accountSession = accountSession.data.value!!, + forceLogin = force, + onSessionChanged = { sId, dateUnix -> + if (dateUnix == null || dateUnix == 0L || sId == null) { + // TODO: Account session isn't valid! + throw Exception() + } else { + accountSession.data.value = accountSession.data.value!!.clone( + accountAuth = accountSession.data.value!!.accountAuth, + sessionId = sId, + sessionLastRequest = dateUnix + ) + } + } + ) + } + // Otherwise, throw exception here + else { + // TODO: Account auth isn't valid! + Log.d("login", "no accounts") + throw Exception() + } + }, + onCompleted = { + // TODO: Throwable here + Log.d("login", "done login") + it?.printStackTrace() + accountSession.processState.value = when { + it == null -> ProcessState.Successful + accountSession.data.value != null -> when { + accountSession.data.value!!.accountAuth.isValidLogin() -> ProcessState.Failed + else -> ProcessState.NotRunYet + } + else -> ProcessState.NotRunYet + } + onCompleted?.let { it2 -> it2(it == null) } + onEventSent?.let { it(1) } + } + ) + } + + /** + * Re-login your account on sv.dut.udn.vn + */ + fun reLogin( + force: Boolean = false, + onCompleted: ((Boolean) -> Unit)? = null + ) { + if (accountSession.processState.value == ProcessState.Running) { + return + } - // Otherwise, use AccountAuth variable. + login( + force = force, + onCompleted = { + if (it && accountSession.processState.value == ProcessState.Successful) { + fetchAccountInformation() + fetchSubjectSchedule() + } + onCompleted?.let { it2 -> it2(it) } + } + ) } - fun logout() { + /** + * Logout account in this application from sv.dut.udn.vn server. + */ + fun logout( + onCompleted: ((Boolean) -> Unit)? = null + ) { + if (accountSession.processState.value == ProcessState.Running) { + return + } + accountSession.processState.value = ProcessState.Running + + launchOnScope( + script = { + // TODO: Fully logout from server + accountSession.resetValue() + subjectSchedule.resetValue() + subjectFee.resetValue() + accountInformation.resetValue() + accountTrainingStatus.resetValue() + }, + onCompleted = { throwable -> + accountSession.processState.value = ProcessState.NotRunYet + onCompleted?.let { it(throwable != null) } + onEventSent?.let { it(1) } + } + ) } fun fetchSubjectSchedule(force: Boolean = false) { if (!subjectSchedule.isSuccessfulRequestExpired() && !force) { return } + if (!checkVariable()) { + return + } if (subjectSchedule.processState.value == ProcessState.Running) { return } @@ -108,14 +291,14 @@ class DUTAccountSession { launchOnScope( script = { - if (accountSession.value == null) { + if (accountSession.data.value == null) { // TODO: AccountSession null throw Exception("") } val data = dutRequestRepository.getSubjectSchedule( - accountSession.value!!, - schoolYear.value + accountSession.data.value!!, + schoolYear.value!! ) if (data == null) { @@ -131,6 +314,7 @@ class DUTAccountSession { (it != null) -> ProcessState.Failed else -> ProcessState.Successful } + onEventSent?.let { it(2) } } ) } @@ -139,6 +323,9 @@ class DUTAccountSession { if (!subjectFee.isSuccessfulRequestExpired() && !force) { return } + if (!checkVariable()) { + return + } if (subjectFee.processState.value == ProcessState.Running) { return } @@ -146,14 +333,14 @@ class DUTAccountSession { launchOnScope( script = { - if (accountSession.value == null) { + if (accountSession.data.value == null) { // TODO: AccountSession null throw Exception("") } val data = dutRequestRepository.getSubjectFee( - accountSession.value!!, - schoolYear.value + accountSession.data.value!!, + schoolYear.value!! ) if (data == null) { @@ -169,6 +356,7 @@ class DUTAccountSession { (it != null) -> ProcessState.Failed else -> ProcessState.Successful } + onEventSent?.let { it(3) } } ) } @@ -184,12 +372,12 @@ class DUTAccountSession { launchOnScope( script = { - if (accountSession.value == null) { + if (accountSession.data.value == null) { // TODO: AccountSession null throw Exception("") } - val data = dutRequestRepository.getAccountInformation(accountSession.value!!) + val data = dutRequestRepository.getAccountInformation(accountSession.data.value!!) if (data == null) { // TODO: Exception when no data returned here! @@ -203,6 +391,7 @@ class DUTAccountSession { (it != null) -> ProcessState.Failed else -> ProcessState.Successful } + onEventSent?.let { it(4) } } ) } @@ -218,12 +407,12 @@ class DUTAccountSession { launchOnScope( script = { - if (accountSession.value == null) { + if (accountSession.data.value == null) { // TODO: AccountSession null throw Exception("") } - val data = dutRequestRepository.getAccountTrainingStatus(accountSession.value!!) + val data = dutRequestRepository.getAccountTrainingStatus(accountSession.data.value!!) if (data == null) { // TODO: Exception when no data returned here! @@ -237,6 +426,7 @@ class DUTAccountSession { (it != null) -> ProcessState.Failed else -> ProcessState.Successful } + onEventSent?.let { it(5) } } ) } diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/account/AccountAuth.kt b/app/src/main/java/io/zoemeow/dutschedule/model/account/AccountAuth.kt index 4f4b554..28d13de 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/account/AccountAuth.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/account/AccountAuth.kt @@ -24,4 +24,8 @@ data class AccountAuth( rememberLogin = rememberLogin ?: this.rememberLogin, ) } + + fun isValidLogin(): Boolean { + return this.username != null && this.password != null + } } \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/account/AccountSession.kt b/app/src/main/java/io/zoemeow/dutschedule/model/account/AccountSession.kt index a808fe7..4059f5a 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/account/AccountSession.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/account/AccountSession.kt @@ -24,4 +24,8 @@ data class AccountSession( sessionLastRequest = sessionLastRequest ?: this.sessionLastRequest ) } + + fun isValidLogin(): Boolean { + return accountAuth.isValidLogin() + } } \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/repository/DutRequestRepository.kt b/app/src/main/java/io/zoemeow/dutschedule/repository/DutRequestRepository.kt index d27d0d2..52bbd83 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/repository/DutRequestRepository.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/repository/DutRequestRepository.kt @@ -98,7 +98,7 @@ class DutRequestRepository { ): Boolean { return when { (accountSession.sessionId != null && System.currentTimeMillis() - accountSession.sessionLastRequest >= (1000 * 60 * 5) && Account.isLoggedIn(accountSession.sessionId) && !forceLogin) -> true - (accountSession.accountAuth.username != null && accountSession.accountAuth.password != null) -> { + (accountSession.accountAuth.isValidLogin()) -> { val sessionId = generateNewSessionId() val timestamp = System.currentTimeMillis() diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/AccountInformation.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/AccountInformation.kt index 3994310..ede7bc7 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/AccountInformation.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/AccountInformation.kt @@ -33,9 +33,6 @@ import androidx.compose.ui.unit.dp import io.zoemeow.dutschedule.activity.AccountActivity import io.zoemeow.dutschedule.model.ProcessState import io.zoemeow.dutschedule.ui.component.base.OutlinedTextBox -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -76,18 +73,10 @@ fun AccountActivity.AccountInformation( ) }, floatingActionButton = { - if (getMainViewModel().accountInformation.processState.value != ProcessState.Running) { + if (getMainViewModel().accountSession.accountInformation.processState.value != ProcessState.Running) { FloatingActionButton( onClick = { - CoroutineScope(Dispatchers.IO).launch { - getMainViewModel().accountLogin( - after = { - if (it) { - getMainViewModel().accountInformation.refreshData(force = true) - } - } - ) - } + getMainViewModel().accountSession.fetchAccountInformation(force = true) }, content = { Icon(Icons.Default.Refresh, "Refresh") @@ -101,7 +90,7 @@ fun AccountActivity.AccountInformation( .fillMaxSize() .padding(padding), content = { - if (getMainViewModel().accountInformation.processState.value == ProcessState.Running) { + if (getMainViewModel().accountSession.accountInformation.processState.value == ProcessState.Running) { LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) } Column( @@ -112,7 +101,7 @@ fun AccountActivity.AccountInformation( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, content = { - getMainViewModel().accountInformation.data.value?.let { data -> + getMainViewModel().accountSession.accountInformation.data.value?.let { data -> val mapPersonalInfo = mapOf( "Name" to (data.name ?: "(unknown)"), "Date of birth" to (data.dateOfBirth ?: "(unknown)"), diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt index 991605b..89dc21f 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt @@ -97,7 +97,7 @@ fun AccountActivity.MainView( .padding(it) .verticalScroll(rememberScrollState()), content = { - when (getMainViewModel().accountSession.value.processState) { + when (getMainViewModel().accountSession.accountSession.processState.value) { ProcessState.NotRunYet -> { LoginBannerNotLoggedIn( opacity = getControlBackgroundAlpha(), @@ -117,7 +117,7 @@ fun AccountActivity.MainView( horizontalArrangement = Arrangement.Start, opacity = getControlBackgroundAlpha(), clicked = { - getMainViewModel().accountReLogin() + getMainViewModel().accountSession.reLogin() } ) ButtonBase( @@ -145,7 +145,7 @@ fun AccountActivity.MainView( ) } ProcessState.Successful -> { - getMainViewModel().accountInformation.let { accInfo -> + getMainViewModel().accountSession.accountInformation.let { accInfo -> AccountInfoBanner( opacity = getControlBackgroundAlpha(), padding = PaddingValues(10.dp), @@ -235,41 +235,39 @@ fun AccountActivity.MainView( loginClicked = { username, password, rememberLogin -> run { CoroutineScope(Dispatchers.IO).launch { - getMainViewModel().accountLogin( - data = AccountAuth( - username = username, - password = password, - rememberLogin = rememberLogin - ), - before = { - loginDialogEnabled.value = false - showSnackBar( - text = "Logging you in...", - clearPrevious = true, - ) - }, - after = { - when (it) { - true -> { - loginDialogEnabled.value = true - loginDialogVisible.value = false - showSnackBar( - text = "Successfully logged in!", - clearPrevious = true, - ) - getMainViewModel().accountInformation.refreshData(force = true) - } - false -> { - loginDialogEnabled.value = true - showSnackBar( - text = "Login failed! Please check your login information and try again.", - clearPrevious = true, - ) - } - } - } + loginDialogEnabled.value = false + showSnackBar( + text = "Logging you in...", + clearPrevious = true, ) } + getMainViewModel().accountSession.login( + accountAuth = AccountAuth( + username = username, + password = password, + rememberLogin = rememberLogin + ), + onCompleted = { + when (it) { + true -> { + loginDialogEnabled.value = true + loginDialogVisible.value = false + getMainViewModel().accountSession.reLogin() + showSnackBar( + text = "Successfully logged in!", + clearPrevious = true, + ) + } + false -> { + loginDialogEnabled.value = true + showSnackBar( + text = "Login failed! Please check your login information and try again.", + clearPrevious = true, + ) + } + } + } + ) } }, cancelRequested = { @@ -287,9 +285,9 @@ fun AccountActivity.MainView( canDismiss = true, logoutClicked = { run { - getMainViewModel().accountLogout( - after = { - logoutDialogVisible.value = false + loginDialogVisible.value = false + getMainViewModel().accountSession.logout( + onCompleted = { showSnackBar( text = "Successfully logout!", clearPrevious = true, diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectFee.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectFee.kt index 23717c5..29425bc 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectFee.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectFee.kt @@ -34,9 +34,6 @@ import androidx.compose.ui.unit.dp import io.zoemeow.dutschedule.activity.AccountActivity import io.zoemeow.dutschedule.model.ProcessState import io.zoemeow.dutschedule.ui.component.account.AccountSubjectFeeInformation -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -77,18 +74,10 @@ fun AccountActivity.SubjectFee( ) }, floatingActionButton = { - if (getMainViewModel().subjectFee.processState.value != ProcessState.Running) { + if (getMainViewModel().accountSession.subjectFee.processState.value != ProcessState.Running) { FloatingActionButton( onClick = { - CoroutineScope(Dispatchers.IO).launch { - getMainViewModel().accountLogin( - after = { - if (it) { - getMainViewModel().subjectFee.refreshData(force = true) - } - } - ) - } + getMainViewModel().accountSession.fetchSubjectFee(force = true) }, content = { Icon(Icons.Default.Refresh, "Refresh") @@ -102,7 +91,7 @@ fun AccountActivity.SubjectFee( .fillMaxSize() .padding(padding), content = { - if (getMainViewModel().subjectFee.processState.value == ProcessState.Running) { + if (getMainViewModel().accountSession.subjectFee.processState.value == ProcessState.Running) { LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) } Column( @@ -124,7 +113,7 @@ fun AccountActivity.SubjectFee( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, content = { - getMainViewModel().subjectFee.data.value?.forEach { item -> + getMainViewModel().accountSession.subjectFee.data.forEach { item -> AccountSubjectFeeInformation( modifier = Modifier.padding(bottom = 10.dp), item = item, @@ -142,15 +131,7 @@ fun AccountActivity.SubjectFee( val hasRun = remember { mutableStateOf(false) } run { if (!hasRun.value) { - CoroutineScope(Dispatchers.IO).launch { - getMainViewModel().accountLogin( - after = { - if (it) { - getMainViewModel().subjectFee.refreshData() - } - } - ) - } + getMainViewModel().accountSession.fetchSubjectFee() hasRun.value = true } } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt index 6dacae0..f4d0b74 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt @@ -37,9 +37,6 @@ import io.zoemeow.dutschedule.activity.AccountActivity import io.zoemeow.dutschedule.model.ProcessState import io.zoemeow.dutschedule.ui.component.account.AccountSubjectMoreInformation import io.zoemeow.dutschedule.ui.component.account.SubjectInformation -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -82,18 +79,10 @@ fun AccountActivity.SubjectInformation( ) }, floatingActionButton = { - if (getMainViewModel().subjectSchedule.processState.value != ProcessState.Running) { + if (getMainViewModel().accountSession.subjectSchedule.processState.value != ProcessState.Running) { FloatingActionButton( onClick = { - CoroutineScope(Dispatchers.IO).launch { - getMainViewModel().accountLogin( - after = { - if (it) { - getMainViewModel().subjectSchedule.refreshData(force = true) - } - } - ) - } + getMainViewModel().accountSession.fetchSubjectSchedule(force = true) }, content = { Icon(Icons.Default.Refresh, "Refresh") @@ -107,7 +96,7 @@ fun AccountActivity.SubjectInformation( .fillMaxSize() .padding(padding), content = { - if (getMainViewModel().subjectSchedule.processState.value == ProcessState.Running) { + if (getMainViewModel().accountSession.subjectSchedule.processState.value == ProcessState.Running) { LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) } Column( @@ -129,7 +118,7 @@ fun AccountActivity.SubjectInformation( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, content = { - getMainViewModel().subjectSchedule.data.value?.forEach { item -> + getMainViewModel().accountSession.subjectSchedule.data.forEach { item -> SubjectInformation( modifier = Modifier.padding(bottom = 7.dp), item = item, @@ -176,15 +165,7 @@ fun AccountActivity.SubjectInformation( val hasRun = remember { mutableStateOf(false) } run { if (!hasRun.value) { - CoroutineScope(Dispatchers.IO).launch { - getMainViewModel().accountLogin( - after = { - if (it) { - getMainViewModel().subjectSchedule.refreshData() - } - } - ) - } + getMainViewModel().accountSession.fetchSubjectSchedule() hasRun.value = true } } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt index b227ee9..6c07a7d 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt @@ -40,9 +40,6 @@ import io.zoemeow.dutschedule.model.ProcessState import io.zoemeow.dutschedule.ui.component.base.ButtonBase import io.zoemeow.dutschedule.ui.component.base.OutlinedTextBox import io.zoemeow.dutschedule.ui.component.base.SimpleCardItem -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -84,16 +81,10 @@ fun AccountActivity.TrainingResult( ) }, floatingActionButton = { - if (getMainViewModel().accountTrainingStatus.processState.value != ProcessState.Running) { + if (getMainViewModel().accountSession.accountTrainingStatus.processState.value != ProcessState.Running) { FloatingActionButton( onClick = { - CoroutineScope(Dispatchers.IO).launch { - getMainViewModel().accountLogin( - after = { - if (it) { getMainViewModel().accountTrainingStatus.refreshData(force = true) } - } - ) - } + getMainViewModel().accountSession.fetchAccountTrainingStatus(force = true) }, content = { Icon(Icons.Default.Refresh, "Refresh") @@ -107,7 +98,7 @@ fun AccountActivity.TrainingResult( .fillMaxSize() .padding(padding), content = { - if (getMainViewModel().accountTrainingStatus.processState.value == ProcessState.Running) { + if (getMainViewModel().accountSession.accountTrainingStatus.processState.value == ProcessState.Running) { LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) } Column( @@ -117,7 +108,7 @@ fun AccountActivity.TrainingResult( verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.Start, content = { - getMainViewModel().accountTrainingStatus.data.value?.let { + getMainViewModel().accountSession.accountTrainingStatus.data.value?.let { fun graduateStatus(): String { val owned = ArrayList() val missing = ArrayList() @@ -259,15 +250,7 @@ fun AccountActivity.TrainingResult( val hasRun = remember { mutableStateOf(false) } run { if (!hasRun.value) { - CoroutineScope(Dispatchers.IO).launch { - getMainViewModel().accountLogin( - after = { - if (it) { - getMainViewModel().accountTrainingStatus.refreshData() - } - } - ) - } + getMainViewModel().accountSession.fetchAccountTrainingStatus() hasRun.value = true } } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingSubjectResult.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingSubjectResult.kt index 03a40ea..fe6db53 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingSubjectResult.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingSubjectResult.kt @@ -178,22 +178,16 @@ fun AccountActivity.TrainingSubjectResult( Icon(Icons.Default.Search, "Search") } ) - } else null + } } ) }, floatingActionButton = { - if (getMainViewModel().accountTrainingStatus.processState.value != ProcessState.Running) { + if (getMainViewModel().accountSession.accountTrainingStatus.processState.value != ProcessState.Running) { FloatingActionButton( onClick = { clearAllFocusAndHideKeyboard() - CoroutineScope(Dispatchers.IO).launch { - getMainViewModel().accountLogin( - after = { - if (it) { getMainViewModel().accountTrainingStatus.refreshData(force = true) } - } - ) - } + getMainViewModel().accountSession.fetchAccountTrainingStatus(force = true) }, content = { Icon(Icons.Default.Refresh, "Refresh") @@ -209,7 +203,7 @@ fun AccountActivity.TrainingSubjectResult( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, content = { - if (getMainViewModel().accountTrainingStatus.processState.value == ProcessState.Running) { + if (getMainViewModel().accountSession.accountTrainingStatus.processState.value == ProcessState.Running) { LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) } Column( @@ -256,7 +250,7 @@ fun AccountActivity.TrainingSubjectResult( schYearOption.value = false } ) - (getMainViewModel().accountTrainingStatus.data.value?.subjectResultList?.map { it.schoolYear }?.toList()?.distinct()?.reversed() ?: listOf()).forEach { + (getMainViewModel().accountSession.accountTrainingStatus.data.value?.subjectResultList?.map { it.schoolYear }?.toList()?.distinct()?.reversed() ?: listOf()).forEach { DropdownMenuItem( modifier = Modifier.background( color = when (schYearOptionText.value == it) { @@ -313,7 +307,7 @@ fun AccountActivity.TrainingSubjectResult( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, content = { - getMainViewModel().accountTrainingStatus.data.value?.subjectResultList?.filter { + getMainViewModel().accountSession.accountTrainingStatus.data.value?.subjectResultList?.filter { p -> (schYearOptionText.value == "All school year items" || p.schoolYear == schYearOptionText.value) && (searchQuery.value.isEmpty() @@ -339,7 +333,7 @@ fun AccountActivity.TrainingSubjectResult( TableCell( modifier = Modifier.fillMaxHeight(), backgroundColor = MaterialTheme.colorScheme.background.copy(alpha = getControlBackgroundAlpha()), - text = "${subjectItem.name}", + text = subjectItem.name, contentAlign = Alignment.CenterStart, textAlign = TextAlign.Start, weight = 0.6f @@ -410,15 +404,7 @@ fun AccountActivity.TrainingSubjectResult( val hasRun = remember { mutableStateOf(false) } run { if (!hasRun.value) { - CoroutineScope(Dispatchers.IO).launch { - getMainViewModel().accountLogin( - after = { - if (it) { - getMainViewModel().accountTrainingStatus.refreshData() - } - } - ) - } + getMainViewModel().accountSession.fetchAccountTrainingStatus() hasRun.value = true } } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt index e597841..8ea6933 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt @@ -155,9 +155,9 @@ fun MainActivity.MainViewDashboard( "Account", style = MaterialTheme.typography.titleSmall ) - getMainViewModel().accountSession.value.let { + getMainViewModel().accountSession.accountSession.processState.value.let { Text( - when (it.processState) { + when (it) { ProcessState.NotRunYet -> "Not logged in" ProcessState.Running -> "Fetching..." // ProcessState.Failed -> when (it.data.accountAuth.username == null) { @@ -167,7 +167,7 @@ fun MainActivity.MainViewDashboard( // it.data.accountAuth.username // ) // } - else -> it.data.accountAuth.username ?: "unknown" + else -> getMainViewModel().accountSession.accountSession.data.value?.accountAuth?.username ?: "unknown" }, style = MaterialTheme.typography.bodySmall ) @@ -178,12 +178,12 @@ fun MainActivity.MainViewDashboard( icon = { BadgedBox( badge = { - if (getMainViewModel().accountSession.value.processState == ProcessState.Failed) { + if (getMainViewModel().accountSession.accountSession.processState.value == ProcessState.Failed) { Badge { Text("!") } } }, content = { - when (getMainViewModel().accountSession.value.processState) { + when (getMainViewModel().accountSession.accountSession.processState.value) { ProcessState.Running -> CircularProgressIndicator( modifier = Modifier.size(26.dp), strokeWidth = 3.dp diff --git a/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt b/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt index 6cdb700..0a7ab2e 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.viewmodel +import android.util.Log import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf @@ -8,20 +9,13 @@ import com.google.gson.Gson import com.google.gson.reflect.TypeToken import dagger.hilt.android.lifecycle.HiltViewModel import io.dutwrapper.dutwrapper.Utils -import io.dutwrapper.dutwrapper.model.accounts.AccountInformation -import io.dutwrapper.dutwrapper.model.accounts.SubjectFeeItem -import io.dutwrapper.dutwrapper.model.accounts.SubjectScheduleItem -import io.dutwrapper.dutwrapper.model.accounts.trainingresult.AccountTrainingStatus import io.dutwrapper.dutwrapper.model.news.NewsGlobalItem import io.dutwrapper.dutwrapper.model.news.NewsSubjectItem import io.dutwrapper.dutwrapper.model.utils.DutSchoolYearItem +import io.zoemeow.dutschedule.model.DUTAccountSession import io.zoemeow.dutschedule.model.NotificationHistory -import io.zoemeow.dutschedule.model.ProcessState import io.zoemeow.dutschedule.model.ProcessVariable -import io.zoemeow.dutschedule.model.VariableTimestamp -import io.zoemeow.dutschedule.model.account.AccountAuth import io.zoemeow.dutschedule.model.account.AccountSession -import io.zoemeow.dutschedule.model.account.SchoolYearItem import io.zoemeow.dutschedule.model.news.NewsCache import io.zoemeow.dutschedule.model.news.NewsFetchType import io.zoemeow.dutschedule.model.news.NewsGroupByDate @@ -40,142 +34,22 @@ class MainViewModel @Inject constructor( private val dutRequestRepository: DutRequestRepository, ) : ViewModel() { val appSettings: MutableState = mutableStateOf(AppSettings()) - val accountSession: MutableState> = mutableStateOf( - VariableTimestamp(data = AccountSession()) - ) - - /** - * Re-login your account on sv.dut.udn.vn - */ - fun accountReLogin() { - launchOnScope( - script = { - accountLogin(after = { result -> - if (result) { - subjectSchedule.refreshData() - accountInformation.refreshData() - } - }) - } - ) - } - - /** - * Login account to this application. - * @param data: Login information. - */ - fun accountLogin( - data: AccountAuth? = null, - before: (() -> Unit)? = null, - after: ((Boolean) -> Unit)? = null - ) { - // If current process is running, ignore this run. - if (accountSession.value.processState == ProcessState.Running) - return - - // Before run - before?.let { it() } - - try { - // If ProcessState.Successful and last run doesn't last 5 minutes, ignore. - // Otherwise will continue - if (!accountSession.value.isSuccessfulRequestExpired()) { - // After run - after?.let { it(accountSession.value.processState == ProcessState.Successful) } - - return - } - // If data exist, merge it to accountSession - data?.let { - accountSession.value = accountSession.value.clone( - data = accountSession.value.data.clone( - accountAuth = AccountAuth( - username = it.username, - password = it.password - ) - ) - ) - } - - accountSession.value = accountSession.value.clone( - processState = ProcessState.Running - ) - val response = dutRequestRepository.login( - accountSession.value.data, - forceLogin = true, - onSessionChanged = { sessionId, timestamp -> - accountSession.value = accountSession.value.clone( - data = accountSession.value.data.clone( - sessionId = sessionId, - sessionLastRequest = timestamp - ), - lastRequest = timestamp - ) + val accountSession: DUTAccountSession = DUTAccountSession( + dutRequestRepository = dutRequestRepository, + onEventSent = { eventId -> + when (eventId) { + 1 -> { + Log.d("app", "triggered saved login") + saveSettings() } - ) - when (response) { - true -> { - accountSession.value = accountSession.value.clone( - processState = ProcessState.Successful - ) - } - - false -> { - accountSession.value = accountSession.value.clone( - processState = ProcessState.NotRunYet - ) + 2, 3, 4, 5 -> { + // TODO: Save account cache here! + // saveSettings() } } - - // After run - after?.let { it(accountSession.value.processState == ProcessState.Successful) } - } catch (_: Exception) { - accountSession.value = accountSession.value.clone( - processState = ProcessState.Failed - ) - - // After run with thrown - after?.let { it(false) } - } - - // Save settings - saveSettings() - } - - /** - * Logout account in this application from server. - */ - fun accountLogout( - after: ((Boolean) -> Unit)? = null, - ) { - // If current process is running, ignore this run. - if (accountSession.value.processState == ProcessState.Running) - return - - try { - // Delete all account sessions. This will always be true. - accountSession.value = accountSession.value.clone( - lastRequest = 0, - processState = ProcessState.NotRunYet, - data = AccountSession(), - ) - - // Clear all user cache data - accountInformation.resetToDefault() - subjectSchedule.data.value = null - subjectFee.data.value = null - accountTrainingStatus.data.value = null - - // After run - after?.let { it(true) } - } catch (_: Exception) { - after?.let { it(false) } } - - // Save settings - saveSettings() - } + ) /** * Refresh or clear news global. @@ -184,7 +58,7 @@ class MainViewModel @Inject constructor( */ val newsGlobal = ProcessVariable>( onRefresh = { baseData, arg -> - val newsBase = baseData ?: NewsCache() + val newsBase = baseData ?: NewsCache() val fetchType = NewsFetchType.fromValue(Integer.parseInt(arg?.get("newsfetchtype") ?: "1")) // Get news from internet @@ -288,7 +162,7 @@ class MainViewModel @Inject constructor( */ val newsSubject = ProcessVariable>( onRefresh = { baseData, arg -> - val newsBase = baseData ?: NewsCache() + val newsBase = baseData ?: NewsCache() val fetchType = NewsFetchType.fromValue(Integer.parseInt(arg?.get("newsfetchtype") ?: "1")) // Get news from internet @@ -384,64 +258,6 @@ class MainViewModel @Inject constructor( } ) - /** - * Subject schedule cache for current logged in account. - */ - val subjectSchedule = ProcessVariable>( - onRefresh = { _, _ -> - // TODO: Remember change year and semester here! - return@ProcessVariable dutRequestRepository.getSubjectSchedule( - accountSession.value.data, - SchoolYearItem( - year = appSettings.value.currentSchoolYear.year, - semester = appSettings.value.currentSchoolYear.semester - ) - ) - }, - onAfterRefresh = { saveSettings() } - ) - - /** - * Subject fee cache for current logged in account. - */ - val subjectFee = ProcessVariable>( - onRefresh = { _, _ -> - // TODO: Remember change year and semester here! - return@ProcessVariable dutRequestRepository.getSubjectFee( - accountSession.value.data, - SchoolYearItem( - year = appSettings.value.currentSchoolYear.year, - semester = appSettings.value.currentSchoolYear.semester - ) - ) - }, - onAfterRefresh = { saveSettings() } - ) - - /** - * Account information cache for current logged in account. - */ - val accountInformation = ProcessVariable( - onRefresh = { _, _ -> - return@ProcessVariable dutRequestRepository.getAccountInformation( - accountSession.value.data - ) - }, - onAfterRefresh = { saveSettings() } - ) - - /** - * Account training status cache for current logged in account. - */ - val accountTrainingStatus = ProcessVariable( - onRefresh = { _, _ -> - return@ProcessVariable dutRequestRepository.getAccountTrainingStatus( - accountSession.value.data - ) - }, - onAfterRefresh = { saveSettings() } - ) - /** * Get current school week if possible. */ @@ -474,8 +290,8 @@ class MainViewModel @Inject constructor( launchOnScope( script = { fileModuleRepository.saveAppSettings(appSettings.value) - fileModuleRepository.saveAccountSession(accountSession.value.data) - fileModuleRepository.saveAccountSubjectScheduleCache(subjectSchedule.data.value ?: arrayListOf()) + fileModuleRepository.saveAccountSession(accountSession.getAccountSession() ?: AccountSession()) + fileModuleRepository.saveAccountSubjectScheduleCache(ArrayList(accountSession.getSubjectScheduleCache())) } ) } @@ -516,10 +332,10 @@ class MainViewModel @Inject constructor( } } - // Get account subject schedule - fileModuleRepository.getAccountSubjectScheduleCache().also { - subjectSchedule.data.value = it - } + // TODO: Get account subject schedule from cache +// fileModuleRepository.getAccountSubjectScheduleCache().also { +// subjectSchedule.data.value = it +// } } ) } @@ -545,10 +361,8 @@ class MainViewModel @Inject constructor( runOnStartupEnabled.value = false appSettings.value = fileModuleRepository.getAppSettings() - accountSession.value = VariableTimestamp( - lastRequest = 0, - data = fileModuleRepository.getAccountSession() - ) + accountSession.setAccountSession(fileModuleRepository.getAccountSession()) + accountSession.setSchoolYear(schoolYearItem = appSettings.value.currentSchoolYear) invokeOnCompleted?.let { it() } } @@ -559,6 +373,7 @@ class MainViewModel @Inject constructor( loadCache() currentSchoolWeek.refreshData(force = true) reloadNotification() + accountSession.reLogin(force = true) launchOnScope(script = { newsGlobal.refreshData( force = true, @@ -568,12 +383,6 @@ class MainViewModel @Inject constructor( force = true, args = mapOf("newsfetchtype" to NewsFetchType.FirstPage.value.toString()) ) - accountLogin(after = { - if (it) { - subjectSchedule.refreshData() - accountInformation.refreshData() - } - }) }) } ) From 301c2646d70b01b5b0abd6584c69664abd4d7e43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BA=BF=20T=C3=B9ng?= Date: Mon, 4 Mar 2024 01:20:47 +0700 Subject: [PATCH 02/21] Update project [New] News Notification Settings: - This will merge subject filter, parse news subject, and others into one. --- .../io/zoemeow/dutschedule/GlobalVariables.kt | 2 + .../dutschedule/activity/SettingsActivity.kt | 10 + .../dutschedule/model/settings/AppSettings.kt | 10 + .../ui/component/base/CheckboxOption.kt | 58 +++ .../ui/component/base/OptionItem.kt | 3 +- .../ui/component/base/RadioButtonOption.kt | 64 +++ .../ui/component/settings/ContentRegion.kt | 4 +- .../newsfilter/NewsFilterAddInNewsSubject.kt | 3 + .../newsfilter/NewsFilterAddManually.kt | 2 + .../settings/newsfilter/NewsFilterClearAll.kt | 2 + .../newsfilter/NewsFilterCurrentFilter.kt | 2 + .../ui/view/settings/ExperimentSettings.kt | 15 +- .../view/settings/NewsNotificationSettings.kt | 394 ++++++++++++++++++ 13 files changed, 564 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/io/zoemeow/dutschedule/GlobalVariables.kt create mode 100644 app/src/main/java/io/zoemeow/dutschedule/ui/component/base/CheckboxOption.kt create mode 100644 app/src/main/java/io/zoemeow/dutschedule/ui/component/base/RadioButtonOption.kt create mode 100644 app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt diff --git a/app/src/main/java/io/zoemeow/dutschedule/GlobalVariables.kt b/app/src/main/java/io/zoemeow/dutschedule/GlobalVariables.kt new file mode 100644 index 0000000..82388d5 --- /dev/null +++ b/app/src/main/java/io/zoemeow/dutschedule/GlobalVariables.kt @@ -0,0 +1,2 @@ +package io.zoemeow.dutschedule + diff --git a/app/src/main/java/io/zoemeow/dutschedule/activity/SettingsActivity.kt b/app/src/main/java/io/zoemeow/dutschedule/activity/SettingsActivity.kt index 05a586c..fd76c3a 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/activity/SettingsActivity.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/activity/SettingsActivity.kt @@ -13,6 +13,7 @@ import io.zoemeow.dutschedule.ui.view.settings.ExperimentSettings import io.zoemeow.dutschedule.ui.view.settings.LanguageSettings import io.zoemeow.dutschedule.ui.view.settings.MainView import io.zoemeow.dutschedule.ui.view.settings.NewsFilterSettings +import io.zoemeow.dutschedule.ui.view.settings.NewsNotificationSettings import io.zoemeow.dutschedule.ui.view.settings.ParseNewsSubjectNotification import io.zoemeow.dutschedule.utils.BackgroundImageUtil @@ -80,6 +81,15 @@ class SettingsActivity : BaseActivity() { ) } + "settings_newsnotificaitonsettings" -> { + NewsNotificationSettings( + context = context, + snackBarHostState = snackBarHostState, + containerColor = containerColor, + contentColor = contentColor + ) + } + else -> { MainView( context = context, diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/settings/AppSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/model/settings/AppSettings.kt index 226f49f..7e6901e 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/settings/AppSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/settings/AppSettings.kt @@ -32,6 +32,12 @@ data class AppSettings( @SerializedName("appsettings.newsbackground.duration") val newsBackgroundDuration: Int = 0, + @SerializedName("appsettings.newsbackground.newsglobal.enabled") + val newsBackgroundGlobalEnabled: Boolean = false, + + @SerializedName("appsettings.newsbackground.newssubject.enabled") + val newsBackgroundSubjectEnabled: Int = -1, + @SerializedName("appsettings.newsbackground.parsenewssubject") val newsBackgroundParseNewsSubject: Boolean = false, @@ -47,6 +53,8 @@ data class AppSettings( newsFilterList: ArrayList? = null, backgroundImageOpacity: Float? = null, fetchNewsBackgroundDuration: Int? = null, + newsBackgroundGlobalEnabled: Boolean? = null, + newsBackgroundSubjectEnabled: Int? = null, newsBackgroundParseNewsSubject: Boolean? = null, currentSchoolYear: SchoolYearItem? = null ): AppSettings { @@ -63,6 +71,8 @@ data class AppSettings( 0 -> 0 else -> if (fetchNewsBackgroundDuration >= 5) fetchNewsBackgroundDuration else 5 }, + newsBackgroundGlobalEnabled = newsBackgroundGlobalEnabled ?: this.newsBackgroundGlobalEnabled, + newsBackgroundSubjectEnabled = newsBackgroundSubjectEnabled ?: this.newsBackgroundSubjectEnabled, newsBackgroundParseNewsSubject = newsBackgroundParseNewsSubject ?: this.newsBackgroundParseNewsSubject, currentSchoolYear = currentSchoolYear ?: this.currentSchoolYear ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/CheckboxOption.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/CheckboxOption.kt new file mode 100644 index 0000000..8f3d0c9 --- /dev/null +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/CheckboxOption.kt @@ -0,0 +1,58 @@ +package io.zoemeow.dutschedule.ui.component.base + +import androidx.compose.material3.Checkbox +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview + +@Composable +fun CheckboxOption( + modifier: Modifier = Modifier, + modifierInside: Modifier = Modifier, + title: String, + description: String? = null, + onClick: () -> Unit, + isChecked: Boolean = false, + isEnabled: Boolean = true, + isVisible: Boolean = true +) { + OptionItem( + modifier = modifier, + modifierInside = modifierInside, + title = title, + description = description, + leadingIcon = { + Checkbox( + checked = isChecked, + onCheckedChange = { onClick() }, + enabled = isEnabled, + ) + }, + onClick = { + if (isEnabled) onClick() + }, + isEnabled = isEnabled, + isVisible = isVisible + ) +} + +@Preview +@Composable +private fun CheckboxOptionPreview() { + CheckboxOption( + title = "This title", + description = "This description", + onClick = { }, + isChecked = true + ) +} + +@Preview +@Composable +private fun CheckboxWithoutDescriptionPreview() { + CheckboxOption( + title = "This title", + onClick = { }, + isChecked = true + ) +} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/OptionItem.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/OptionItem.kt index 2951b1d..415267b 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/OptionItem.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/OptionItem.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.unit.dp @Composable fun OptionItem( modifier: Modifier = Modifier, + modifierInside: Modifier = Modifier.padding(vertical = 15.dp), title: String, description: String? = null, leadingIcon: (@Composable () -> Unit)? = null, @@ -46,7 +47,7 @@ fun OptionItem( color = Color.Transparent, content = { Row( - modifier = Modifier.padding(vertical = 15.dp), + modifier = modifierInside, verticalAlignment = Alignment.CenterVertically, content = { leadingIcon?.let { diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/RadioButtonOption.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/RadioButtonOption.kt new file mode 100644 index 0000000..3bb7626 --- /dev/null +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/RadioButtonOption.kt @@ -0,0 +1,64 @@ +package io.zoemeow.dutschedule.ui.component.base + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.RadioButton +import androidx.compose.material3.RadioButtonColors +import androidx.compose.material3.RadioButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun RadioButtonOption( + modifier: Modifier = Modifier, + modifierInside: Modifier = Modifier.padding(vertical = 15.dp), + title: String, + description: String? = null, + onClick: () -> Unit, + radioButtonColors: RadioButtonColors = RadioButtonDefaults.colors(), + isChecked: Boolean = false, + isEnabled: Boolean = true, + isVisible: Boolean = true +) { + OptionItem( + modifier = modifier, + modifierInside = modifierInside, + title = title, + description = description, + leadingIcon = { + RadioButton( + selected = isChecked, + onClick = onClick, + enabled = isEnabled, + colors = radioButtonColors + ) + }, + onClick = { + if (isEnabled) onClick() + }, + isEnabled = isEnabled, + isVisible = isVisible + ) +} + +@Preview +@Composable +private fun RadioButtonOptionPreview() { + RadioButtonOption( + title = "This title", + description = "This description", + onClick = { }, + isChecked = true + ) +} + +@Preview +@Composable +private fun RadioButtonOptionWithoutDescriptionPreview() { + RadioButtonOption( + title = "This title", + onClick = { }, + isChecked = true + ) +} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/ContentRegion.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/ContentRegion.kt index 49e3f47..dad854b 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/ContentRegion.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/ContentRegion.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier @Composable fun ContentRegion( modifier: Modifier = Modifier, + textModifier: Modifier = Modifier, text: String, content: @Composable () -> Unit ) { @@ -26,7 +27,8 @@ fun ContentRegion( Text( text = text, style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.primary + color = MaterialTheme.colorScheme.primary, + modifier = textModifier ) content() } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterAddInNewsSubject.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterAddInNewsSubject.kt index 3750c74..8df7837 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterAddInNewsSubject.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterAddInNewsSubject.kt @@ -2,16 +2,19 @@ package io.zoemeow.dutschedule.ui.component.settings.newsfilter import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import io.zoemeow.dutschedule.ui.component.base.ExpandableContent import io.zoemeow.dutschedule.ui.component.base.ExpandableContentDefaultTitle @Composable fun NewsFilterAddInNewsSubject( + modifier: Modifier = Modifier, expanded: Boolean = false, onExpanded: (() -> Unit)? = null, opacity: Float = 1.0f ) { ExpandableContent( + modifier = modifier, opacity = opacity, title = { ExpandableContentDefaultTitle(title = "Add filter via news subject or subject schedule") diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterAddManually.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterAddManually.kt index 9046eea..5aa4523 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterAddManually.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterAddManually.kt @@ -22,6 +22,7 @@ import io.zoemeow.dutschedule.ui.component.base.ExpandableContentDefaultTitle @Composable fun NewsFilterAddManually( + modifier: Modifier = Modifier, expanded: Boolean = false, onExpanded: (() -> Unit)? = null, onSubmit: ((String, String, String) -> Unit)? = null, @@ -32,6 +33,7 @@ fun NewsFilterAddManually( val subjectName = remember { mutableStateOf("") } ExpandableContent( + modifier = modifier, opacity = opacity, title = { ExpandableContentDefaultTitle(title = "Add filter manually") diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterClearAll.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterClearAll.kt index 704e368..07bd4b4 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterClearAll.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterClearAll.kt @@ -16,12 +16,14 @@ import io.zoemeow.dutschedule.ui.component.base.ExpandableContentDefaultTitle @Composable fun NewsFilterClearAll( + modifier: Modifier = Modifier, expanded: Boolean = false, onExpanded: (() -> Unit)? = null, onSubmit: (() -> Unit)? = null, opacity: Float = 1.0f ) { ExpandableContent( + modifier = modifier, opacity = opacity, title = { ExpandableContentDefaultTitle(title = "Clear all filters") diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterCurrentFilter.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterCurrentFilter.kt index 071cfa8..ddce7a6 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterCurrentFilter.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterCurrentFilter.kt @@ -18,11 +18,13 @@ import io.zoemeow.dutschedule.ui.component.base.ExpandableContentDefaultTitle @OptIn(ExperimentalLayoutApi::class) @Composable fun NewsFilterCurrentFilter( + modifier: Modifier = Modifier, selectedSubjects: List? = null, onRemoveRequested: ((SubjectCode) -> Unit)? = null, opacity: Float = 1.0f ) { ExpandableContent( + modifier = modifier, opacity = opacity, title = { ExpandableContentDefaultTitle(title = "Your current filter") diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt index cf1d7ec..2f47ee9 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt @@ -120,9 +120,18 @@ fun SettingsActivity.ExperimentSettings( false -> "Disabled (regular notification for news subject)" }, onClick = { - val intent = Intent(context, SettingsActivity::class.java) - intent.action = "settings_newssubjectnewparse" - context.startActivity(intent) + Intent(context, SettingsActivity::class.java).apply { + action = "settings_newssubjectnewparse" + }.also { intent -> context.startActivity(intent) } + } + ) + OptionItem( + title = "News notifications in background", + description = "Configure your settings", + onClick = { + Intent(context, SettingsActivity::class.java).apply { + action = "settings_newsnotificaitonsettings" + }.also { intent -> context.startActivity(intent) } } ) } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt new file mode 100644 index 0000000..135b64f --- /dev/null +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt @@ -0,0 +1,394 @@ +package io.zoemeow.dutschedule.ui.view.settings + +import android.content.Context +import android.content.Intent +import androidx.activity.ComponentActivity +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +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 io.zoemeow.dutschedule.activity.PermissionRequestActivity +import io.zoemeow.dutschedule.activity.SettingsActivity +import io.zoemeow.dutschedule.model.settings.SubjectCode +import io.zoemeow.dutschedule.ui.component.base.CheckboxOption +import io.zoemeow.dutschedule.ui.component.base.DividerItem +import io.zoemeow.dutschedule.ui.component.base.OptionItem +import io.zoemeow.dutschedule.ui.component.base.RadioButtonOption +import io.zoemeow.dutschedule.ui.component.base.SimpleCardItem +import io.zoemeow.dutschedule.ui.component.base.SwitchWithTextInSurface +import io.zoemeow.dutschedule.ui.component.settings.ContentRegion + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsActivity.NewsNotificationSettings( + context: Context, + snackBarHostState: SnackbarHostState?, + containerColor: Color, + contentColor: Color +) { + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + + Scaffold( + modifier = Modifier.fillMaxSize(), + snackbarHost = { + snackBarHostState?.let { + SnackbarHost(hostState = it) + } + }, + containerColor = containerColor, + contentColor = contentColor, + topBar = { + LargeTopAppBar( + title = { Text("News Notification Settings") }, + colors = TopAppBarDefaults.largeTopAppBarColors(containerColor = Color.Transparent, scrolledContainerColor = Color.Transparent), + navigationIcon = { + IconButton( + onClick = { + setResult(ComponentActivity.RESULT_OK) + finish() + }, + content = { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + "Back", + modifier = Modifier.size(25.dp) + ) + } + ) + }, + scrollBehavior = scrollBehavior + ) + }, + ) { + MainView( + padding = it, + fetchNewsInBackgroundDuration = getMainViewModel().appSettings.value.newsBackgroundDuration, + onFetchNewsStateChanged = { enabled -> + if (enabled) { + if (PermissionRequestActivity.checkPermissionScheduleExactAlarm(context).isGranted) { + // TODO: Fetch news in background onClick + val dataTemp = getMainViewModel().appSettings.value.clone( + fetchNewsBackgroundDuration = 30 + ) + getMainViewModel().appSettings.value = dataTemp + getMainViewModel().saveSettings() + } else { + showSnackBar( + text = "You need to enable Alarms & Reminders in Android app settings to use this feature.", + clearPrevious = true, + actionText = "Open", + action = { + Intent(context, PermissionRequestActivity::class.java).also { intent -> + context.startActivity(intent) + } + } + ) + } + } else { + val dataTemp = getMainViewModel().appSettings.value.clone( + fetchNewsBackgroundDuration = 0 + ) + getMainViewModel().appSettings.value = dataTemp + getMainViewModel().saveSettings() + } + }, + onFetchNewsDurationClicked = { + if (PermissionRequestActivity.checkPermissionScheduleExactAlarm(context).isGranted) { + // TODO: Fetch news in background onClick + } else { + showSnackBar( + text = "You need to enable Alarms & Reminders in Android app settings to use this feature.", + clearPrevious = true, + actionText = "Open", + action = { + Intent(context, PermissionRequestActivity::class.java).also { intent -> + context.startActivity(intent) + } + } + ) + } + }, + isNewsGlobalEnabled = getMainViewModel().appSettings.value.newsBackgroundGlobalEnabled, + onNewsGlobalStateChanged = { enabled -> + val dataTemp = getMainViewModel().appSettings.value.clone( + newsBackgroundGlobalEnabled = enabled + ) + getMainViewModel().appSettings.value = dataTemp + getMainViewModel().saveSettings() + }, + isNewsSubjectEnabled = getMainViewModel().appSettings.value.newsBackgroundSubjectEnabled, + onNewsSubjectStateChanged = { code -> + val dataTemp = getMainViewModel().appSettings.value.clone( + newsBackgroundSubjectEnabled = code + ) + getMainViewModel().appSettings.value = dataTemp + getMainViewModel().saveSettings() + }, + subjectFilterList = getMainViewModel().appSettings.value.newsBackgroundFilterList, + onSubjectFilterAdd = { + + }, + onSubjectFilterDelete = { code -> + + }, + onSubjectFilterClear = { + + }, + opacity = getControlBackgroundAlpha() + ) + } +} + +@Composable +private fun MainView( + padding: PaddingValues = PaddingValues(0.dp), + fetchNewsInBackgroundDuration: Int = 0, + onFetchNewsStateChanged: (Boolean) -> Unit, + onFetchNewsDurationClicked: () -> Unit, + isNewsGlobalEnabled: Boolean = false, + onNewsGlobalStateChanged: ((Boolean) -> Unit)? = null, + isNewsSubjectEnabled: Int = -1, + onNewsSubjectStateChanged: ((Int) -> Unit)? = null, + subjectFilterList: ArrayList = arrayListOf(), + onSubjectFilterAdd: (() -> Unit)? = null, + onSubjectFilterDelete: ((SubjectCode) -> Unit)? = null, + onSubjectFilterClear: (() -> Unit)? = null, + opacity: Float = 1f +) { + // isNewsSubjectEnabled: + // - -1: Off + // - 0: All + // - 1: Your subject schedule list + // - 2: Custom list + + Column( + modifier = Modifier + .padding(padding) + .verticalScroll(rememberScrollState()) + ) { + SwitchWithTextInSurface( + text = "Refresh news in background", + enabled = true, + checked = fetchNewsInBackgroundDuration > 0, + onCheckedChange = { + // TODO: Refresh news state changed, default is 30 minutes + onFetchNewsStateChanged(!(fetchNewsInBackgroundDuration > 0)) + } + ) + ContentRegion( + modifier = Modifier.padding(top = 10.dp), + textModifier = Modifier.padding(horizontal = 20.dp), + text = "Notification settings" + ) { + OptionItem( + modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), + title = "Fetch news duration", + description = when { + (fetchNewsInBackgroundDuration > 0) -> + String.format( + "Every %d minute%s", + fetchNewsInBackgroundDuration, + if (fetchNewsInBackgroundDuration != 1) "s" else "" + ) + else -> "Disabled" + }, + onClick = { + if (fetchNewsInBackgroundDuration > 0) { + onFetchNewsDurationClicked() + } + } + ) + } + DividerItem(padding = PaddingValues(top = 5.dp, bottom = 15.dp)) + ContentRegion( + textModifier = Modifier.padding(horizontal = 20.dp), + text = "Global news notification" + ) { + CheckboxOption( + title = "Enable global news notification", + modifierInside = Modifier.padding(horizontal = 6.5.dp), + isEnabled = fetchNewsInBackgroundDuration > 0, + isChecked = isNewsGlobalEnabled, + onClick = { + // TODO: Refresh news state changed + onNewsGlobalStateChanged?.let { it(!isNewsGlobalEnabled) } + } + ) + } + ContentRegion( + modifier = Modifier.padding(top = 10.dp), + textModifier = Modifier.padding(horizontal = 20.dp), + text = "Subject news notification" + ) { + RadioButtonOption( + modifierInside = Modifier.padding(horizontal = 6.5.dp), + title = "Off", + isEnabled = fetchNewsInBackgroundDuration > 0, + isChecked = isNewsSubjectEnabled == -1, + onClick = { + // TODO: Subject news notification off - onClick + onNewsSubjectStateChanged?.let { it(-1) } + } + ) + RadioButtonOption( + modifierInside = Modifier.padding(horizontal = 6.5.dp), + title = "All subject news notifications", + isEnabled = fetchNewsInBackgroundDuration > 0, + isChecked = isNewsSubjectEnabled == 0, + onClick = { + // TODO: Subject news notification all - onClick + onNewsSubjectStateChanged?.let { it(0) } + } + ) + RadioButtonOption( + modifierInside = Modifier.padding(horizontal = 6.5.dp), + title = "Match your subject schedule", + isEnabled = fetchNewsInBackgroundDuration > 0, + isChecked = isNewsSubjectEnabled == 1, + onClick = { + // TODO: Subject news notification your subject schedule - onClick + onNewsSubjectStateChanged?.let { it(1) } + } + ) + RadioButtonOption( + modifierInside = Modifier.padding(horizontal = 6.5.dp), + title = "Follow custom list", + isEnabled = fetchNewsInBackgroundDuration > 0, + isChecked = isNewsSubjectEnabled == 2, + onClick = { + // TODO: Subject news notification custom list - onClick + onNewsSubjectStateChanged?.let { it(2) } + } + ) + } + DividerItem(padding = PaddingValues(top = 5.dp, bottom = 15.dp)) + ContentRegion( + textModifier = Modifier.padding(horizontal = 20.dp), + text = "News subject filter" + ) { + if (isNewsSubjectEnabled != 2) { + SimpleCardItem( + padding = PaddingValues(horizontal = 20.4.dp, vertical = 7.dp), + title = "News subject filter list is disabled", + content = { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(horizontal = 15.dp) + .padding(bottom = 15.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text("To manage your subject news filter, please check \"Follow custom list\" option first.") + } + }, + clicked = { }, + opacity = opacity + ) + } + SimpleCardItem( + padding = PaddingValues(horizontal = 20.4.dp, vertical = 5.dp), + title = "Your current filter list", + clicked = { }, + opacity = opacity, + content = { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(horizontal = 15.dp) + .padding(bottom = 15.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + if (subjectFilterList.size == 0) { + Text("Your added subject filter will shown here.") + } + subjectFilterList.forEach { code -> + OptionItem( + modifier = Modifier.padding(vertical = 3.dp), + modifierInside = Modifier, + title = "${code.subjectName} - ${code.subjectName}.Nh${code.classId}", + onClick = { }, + trailingIcon = { + IconButton( + onClick = { + if (fetchNewsInBackgroundDuration > 0) { + onSubjectFilterDelete?.let { it(code) } + } + }, + content = { + Icon(Icons.Default.Delete, "Delete") + } + ) + } + ) + } + } + } + ) + OptionItem( + modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), + title = "Add a subject news filter", + leadingIcon = { Icon(Icons.Default.Add, "Add a subject news filter") }, + isEnabled = isNewsSubjectEnabled == 2, + onClick = { + // TODO: Add a subject news filter + onSubjectFilterAdd?.let { it() } + } + ) + OptionItem( + modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), + title = "Clear all subject news filter list", + leadingIcon = { Icon(Icons.Default.Delete, "Clear all subject news filter") }, + isEnabled = isNewsSubjectEnabled == 2, + onClick = { + // TODO: Clear all subject news filter list + onSubjectFilterClear?.let { it() } + } + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun MainViewPreview() { + MainView( + fetchNewsInBackgroundDuration = 0, + onFetchNewsStateChanged = { }, + onFetchNewsDurationClicked = { }, + isNewsGlobalEnabled = true, + subjectFilterList = arrayListOf( + SubjectCode("19", "12", "Nhập môn ngành"), + SubjectCode("19", "12", "PBL3") + ), + isNewsSubjectEnabled = 2 + ) +} \ No newline at end of file From 89aaf66ed7376edc622368b3b89d950ddaca7464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BA=BF=20T=C3=B9ng?= Date: Mon, 4 Mar 2024 22:28:40 +0700 Subject: [PATCH 03/21] Implement News Background Settings --- .../dutschedule/activity/MainActivity.kt | 1 + .../dutschedule/model/settings/AppSettings.kt | 13 +- .../dutschedule/model/settings/ThemeMode.kt | 11 + .../service/NewsBackgroundUpdateService.kt | 17 +- .../NotificationHistoryBottomSheet.kt | 5 +- .../ui/view/settings/ExperimentSettings.kt | 14 +- .../view/settings/NewsNotificationSettings.kt | 215 ++++++++++++++---- 7 files changed, 208 insertions(+), 68 deletions(-) diff --git a/app/src/main/java/io/zoemeow/dutschedule/activity/MainActivity.kt b/app/src/main/java/io/zoemeow/dutschedule/activity/MainActivity.kt index f9ca699..8d62b44 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/activity/MainActivity.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/activity/MainActivity.kt @@ -265,6 +265,7 @@ class MainActivity : BaseActivity() { } ) NotificationHistoryBottomSheet( + itemList = getMainViewModel().notificationHistory, visible = isNotificationOpened.value, sheetState = notificationModalBottomSheetState, onDismiss = { isNotificationOpened.value = false }, diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/settings/AppSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/model/settings/AppSettings.kt index 7e6901e..b21ad3d 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/settings/AppSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/settings/AppSettings.kt @@ -33,10 +33,19 @@ data class AppSettings( val newsBackgroundDuration: Int = 0, @SerializedName("appsettings.newsbackground.newsglobal.enabled") - val newsBackgroundGlobalEnabled: Boolean = false, + val newsBackgroundGlobalEnabled: Boolean = true, + /** + * Is subject news notify you? + * + * -1: Off; + * 0: All; + * 1: Your subject schedule list; + * 2: Custom list (follow "newsBackgroundFilterList") + * @since v2.0-draft17 + */ @SerializedName("appsettings.newsbackground.newssubject.enabled") - val newsBackgroundSubjectEnabled: Int = -1, + val newsBackgroundSubjectEnabled: Int = 0, @SerializedName("appsettings.newsbackground.parsenewssubject") val newsBackgroundParseNewsSubject: Boolean = false, diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/settings/ThemeMode.kt b/app/src/main/java/io/zoemeow/dutschedule/model/settings/ThemeMode.kt index 67350d5..f58d66a 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/settings/ThemeMode.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/settings/ThemeMode.kt @@ -3,7 +3,18 @@ package io.zoemeow.dutschedule.model.settings import com.google.gson.annotations.SerializedName enum class ThemeMode(val value: Int) { + /** + * Follow your device theme + */ @SerializedName("0") FollowDeviceTheme(0), + + /** + * Dark mode + */ @SerializedName("1") DarkMode(1), + + /** + * Light mode + */ @SerializedName("2") LightMode(2) } \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/service/NewsBackgroundUpdateService.kt b/app/src/main/java/io/zoemeow/dutschedule/service/NewsBackgroundUpdateService.kt index b0877d0..cd30791 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/service/NewsBackgroundUpdateService.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/service/NewsBackgroundUpdateService.kt @@ -234,6 +234,11 @@ class NewsBackgroundUpdateService : BaseService( return } + // If user denied, no extra action needed + if (!settings.newsBackgroundGlobalEnabled) { + return + } + // TODO: Notify by notify variable... // Processing news global notifications for notify here! @@ -350,6 +355,11 @@ class NewsBackgroundUpdateService : BaseService( return } + // If user denied, no extra action needed + if (settings.newsBackgroundSubjectEnabled == -1) { + return + } + // TODO: Notify by notify variable... // TODO: Processing news subject notifications for notify here! @@ -359,11 +369,12 @@ class NewsBackgroundUpdateService : BaseService( var notifyRequired = false // If enabled news filter, do following. - // If filter was empty -> Not set -> All news -> Enable notify. - if (settings.newsBackgroundFilterList.isEmpty()) { + // settings.newsBackgroundSubjectEnabled == 0 -> All news enabled + if (settings.newsBackgroundSubjectEnabled == 0) { notifyRequired = true } - // If a news in filter list -> Enable notify. + // TODO: settings.newsBackgroundSubjectEnabled == 1 action + // settings.newsBackgroundSubjectEnabled == 2 else if (settings.newsBackgroundFilterList.any { source -> newsItem.affectedClass.any { targetGroup -> targetGroup.codeList.any { target -> diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationHistoryBottomSheet.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationHistoryBottomSheet.kt index 5336edd..f25f9c8 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationHistoryBottomSheet.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationHistoryBottomSheet.kt @@ -37,6 +37,7 @@ import io.zoemeow.dutschedule.utils.getRandomString fun MainActivity.NotificationHistoryBottomSheet( visible: Boolean = false, sheetState: SheetState, + itemList: List = listOf(), onDismiss: () -> Unit, onClearItem: ((NotificationHistory) -> Unit)? = null, onClearAll: (() -> Unit)? = null, @@ -45,10 +46,10 @@ fun MainActivity.NotificationHistoryBottomSheet( if (visible) { ModalBottomSheet( onDismissRequest = onDismiss, - sheetState = sheetState + sheetState = sheetState, ) { MainView( - itemList = listOf(), + itemList = itemList, opacity = opacity, onClearItem = onClearItem, clearAllRequested = onClearAll diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt index 2f47ee9..3ac70b3 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt @@ -114,19 +114,7 @@ fun SettingsActivity.ExperimentSettings( text = "Notifications", content = { OptionItem( - title = "New parse method on notification", - description = when (getMainViewModel().appSettings.value.newsBackgroundParseNewsSubject) { - true -> "Enabled (special notification for news subject)" - false -> "Disabled (regular notification for news subject)" - }, - onClick = { - Intent(context, SettingsActivity::class.java).apply { - action = "settings_newssubjectnewparse" - }.also { intent -> context.startActivity(intent) } - } - ) - OptionItem( - title = "News notifications in background", + title = "News notifications in background settings", description = "Configure your settings", onClick = { Intent(context, SettingsActivity::class.java).apply { diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt index 135b64f..0992411 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt @@ -5,7 +5,11 @@ import android.content.Intent import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -16,17 +20,25 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.ElevatedButton import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SuggestionChip import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -51,7 +63,7 @@ fun SettingsActivity.NewsNotificationSettings( containerColor: Color, contentColor: Color ) { - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + // val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() Scaffold( modifier = Modifier.fillMaxSize(), @@ -63,9 +75,10 @@ fun SettingsActivity.NewsNotificationSettings( containerColor = containerColor, contentColor = contentColor, topBar = { - LargeTopAppBar( + TopAppBar( title = { Text("News Notification Settings") }, - colors = TopAppBarDefaults.largeTopAppBarColors(containerColor = Color.Transparent, scrolledContainerColor = Color.Transparent), + // colors = TopAppBarDefaults.largeTopAppBarColors(containerColor = Color.Transparent, scrolledContainerColor = Color.Transparent), + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent, scrolledContainerColor = Color.Transparent), navigationIcon = { IconButton( onClick = { @@ -80,23 +93,27 @@ fun SettingsActivity.NewsNotificationSettings( ) } ) - }, - scrollBehavior = scrollBehavior + } + // scrollBehavior = scrollBehavior ) }, ) { MainView( padding = it, fetchNewsInBackgroundDuration = getMainViewModel().appSettings.value.newsBackgroundDuration, - onFetchNewsStateChanged = { enabled -> - if (enabled) { + onFetchNewsStateChanged = { duration -> + if (duration > 0) { if (PermissionRequestActivity.checkPermissionScheduleExactAlarm(context).isGranted) { // TODO: Fetch news in background onClick val dataTemp = getMainViewModel().appSettings.value.clone( - fetchNewsBackgroundDuration = 30 + fetchNewsBackgroundDuration = duration ) getMainViewModel().appSettings.value = dataTemp getMainViewModel().saveSettings() + showSnackBar( + text = "Successfully enabled fetch news in background! News will refresh every $duration minute(s).", + clearPrevious = true + ) } else { showSnackBar( text = "You need to enable Alarms & Reminders in Android app settings to use this feature.", @@ -115,24 +132,18 @@ fun SettingsActivity.NewsNotificationSettings( ) getMainViewModel().appSettings.value = dataTemp getMainViewModel().saveSettings() - } - }, - onFetchNewsDurationClicked = { - if (PermissionRequestActivity.checkPermissionScheduleExactAlarm(context).isGranted) { - // TODO: Fetch news in background onClick - } else { showSnackBar( - text = "You need to enable Alarms & Reminders in Android app settings to use this feature.", - clearPrevious = true, - actionText = "Open", - action = { - Intent(context, PermissionRequestActivity::class.java).also { intent -> - context.startActivity(intent) - } - } + text = "Successfully disabled fetch news in background!", + clearPrevious = true ) } }, + isNewSubjectNotificationParseEnabled = getMainViewModel().appSettings.value.newsBackgroundParseNewsSubject, + onNewSubjectNotificationParseStateChanged = { + Intent(context, SettingsActivity::class.java).apply { + action = "settings_newssubjectnewparse" + }.also { intent -> context.startActivity(intent) } + }, isNewsGlobalEnabled = getMainViewModel().appSettings.value.newsBackgroundGlobalEnabled, onNewsGlobalStateChanged = { enabled -> val dataTemp = getMainViewModel().appSettings.value.clone( @@ -140,14 +151,40 @@ fun SettingsActivity.NewsNotificationSettings( ) getMainViewModel().appSettings.value = dataTemp getMainViewModel().saveSettings() + showSnackBar( + text = "Successfully ${ + if (enabled) "enabled" else "disabled" + } global news notification!", + clearPrevious = true + ) }, isNewsSubjectEnabled = getMainViewModel().appSettings.value.newsBackgroundSubjectEnabled, - onNewsSubjectStateChanged = { code -> + onNewsSubjectStateChanged = f@ { code -> + if (code == 1) { + showSnackBar( + text = "\"Match your subject schedule\" option is in development. Check back soon.", + clearPrevious = true + ) + return@f + } + val dataTemp = getMainViewModel().appSettings.value.clone( newsBackgroundSubjectEnabled = code ) getMainViewModel().appSettings.value = dataTemp getMainViewModel().saveSettings() + showSnackBar( + text = "Done! You will notify \"${ + when (code) { + -1 -> "nothing" + 0 -> "all subject news notifications" + 1 -> "news match your subject schedule" + 2 -> "news match your filter list" + else -> "(unknown)" + } + }\".", + clearPrevious = true + ) }, subjectFilterList = getMainViewModel().appSettings.value.newsBackgroundFilterList, onSubjectFilterAdd = { @@ -164,12 +201,14 @@ fun SettingsActivity.NewsNotificationSettings( } } +@OptIn(ExperimentalLayoutApi::class) @Composable private fun MainView( padding: PaddingValues = PaddingValues(0.dp), fetchNewsInBackgroundDuration: Int = 0, - onFetchNewsStateChanged: (Boolean) -> Unit, - onFetchNewsDurationClicked: () -> Unit, + onFetchNewsStateChanged: ((Int) -> Unit)? = null, + isNewSubjectNotificationParseEnabled: Boolean = false, + onNewSubjectNotificationParseStateChanged: (() -> Unit)? = null, isNewsGlobalEnabled: Boolean = false, onNewsGlobalStateChanged: ((Boolean) -> Unit)? = null, isNewsSubjectEnabled: Int = -1, @@ -180,11 +219,9 @@ private fun MainView( onSubjectFilterClear: (() -> Unit)? = null, opacity: Float = 1f ) { - // isNewsSubjectEnabled: - // - -1: Off - // - 0: All - // - 1: Your subject schedule list - // - 2: Custom list + val durationTemp = remember { + mutableIntStateOf(fetchNewsInBackgroundDuration) + } Column( modifier = Modifier @@ -196,8 +233,11 @@ private fun MainView( enabled = true, checked = fetchNewsInBackgroundDuration > 0, onCheckedChange = { - // TODO: Refresh news state changed, default is 30 minutes - onFetchNewsStateChanged(!(fetchNewsInBackgroundDuration > 0)) + // Refresh news state changed, default is 30 minutes + onFetchNewsStateChanged?.let { it(when { + (fetchNewsInBackgroundDuration > 0) -> 0 + else -> 30 + }) } } ) ContentRegion( @@ -205,24 +245,104 @@ private fun MainView( textModifier = Modifier.padding(horizontal = 20.dp), text = "Notification settings" ) { - OptionItem( - modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), + SimpleCardItem( + padding = PaddingValues(horizontal = 20.4.dp, vertical = 5.dp), title = "Fetch news duration", - description = when { - (fetchNewsInBackgroundDuration > 0) -> - String.format( - "Every %d minute%s", - fetchNewsInBackgroundDuration, - if (fetchNewsInBackgroundDuration != 1) "s" else "" + clicked = { }, + opacity = opacity, + content = { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(horizontal = 15.dp) + .padding(top = 5.dp, bottom = 10.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + "Current duration settings: ${ when (fetchNewsInBackgroundDuration) { + 0 -> "Disabled" + 1 -> "1 minute" + else -> "$fetchNewsInBackgroundDuration minutes" + } + }", + modifier = Modifier.padding(bottom = 10.dp) + ) + Slider( + valueRange = 5f..240f, + steps = 236, + value = durationTemp.intValue.toFloat(), + enabled = fetchNewsInBackgroundDuration > 0, + colors = SliderDefaults.colors( + activeTickColor = Color.Transparent, + activeTrackColor = MaterialTheme.colorScheme.primary, + inactiveTickColor = Color.Transparent, + inactiveTrackColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.4f) + ), + onValueChange = { + durationTemp.intValue = it.toInt() + } + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + content = { + Text("${durationTemp.intValue} minute${if (durationTemp.intValue != 1) "s" else ""}") + } + ) + FlowRow( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 7.dp), + horizontalArrangement = Arrangement.Center, + content = { + listOf(15, 30, 60).forEach { min -> + SuggestionChip( + modifier = Modifier.padding(horizontal = 5.dp), + icon = { + if (durationTemp.intValue == min) { + Icon( + Icons.Default.Check, + "Selected", + modifier = Modifier.size(20.dp) + ) + } + }, + onClick = { + if (fetchNewsInBackgroundDuration > 0) { + durationTemp.intValue = min + } + }, + label = { Text(if (min == 0) "Turn off" else "$min min") } + ) + } + } + ) + Spacer(modifier = Modifier.size(5.dp)) + ElevatedButton( + onClick = { + if (fetchNewsInBackgroundDuration > 0) { + onFetchNewsStateChanged?.let { it(durationTemp.intValue) } + } + }, + content = { + Text("Save") + } ) - else -> "Disabled" - }, - onClick = { - if (fetchNewsInBackgroundDuration > 0) { - onFetchNewsDurationClicked() } } ) + OptionItem( + modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), + title = "News parse method on notification", + description = when (isNewSubjectNotificationParseEnabled) { + true -> "Enabled (special notification for news subject)" + false -> "Disabled (regular notification for news subject)" + }, + onClick = { onNewSubjectNotificationParseStateChanged?.let { it() } } + ) } DividerItem(padding = PaddingValues(top = 5.dp, bottom = 15.dp)) ContentRegion( @@ -381,9 +501,8 @@ private fun MainView( @Composable private fun MainViewPreview() { MainView( - fetchNewsInBackgroundDuration = 0, + fetchNewsInBackgroundDuration = 30, onFetchNewsStateChanged = { }, - onFetchNewsDurationClicked = { }, isNewsGlobalEnabled = true, subjectFilterList = arrayListOf( SubjectCode("19", "12", "Nhập môn ngành"), From b6a556c2875ceb5b88bf2d2af1884b35428fb5ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BA=BF=20T=C3=B9ng?= Date: Wed, 6 Mar 2024 14:07:42 +0700 Subject: [PATCH 04/21] Update project - Updated notifications panel in main view. - Add some icons for options in settings. - Move some contents in MainActivity from activity/MainActivity.kt to ui.view.main.MainViewDashboard.kt. --- .gitignore | 1 + .idea/deploymentTargetDropDown.xml | 10 - app/build.gradle | 32 +-- .../dutschedule/activity/BaseActivity.kt | 10 +- .../dutschedule/activity/MainActivity.kt | 247 +---------------- .../activity/PermissionRequestActivity.kt | 2 +- .../service/NewsBackgroundUpdateService.kt | 10 +- .../ui/component/base/DialogBase.kt | 2 +- .../ui/component/base/OptionSwitchItem.kt | 6 + .../NotificationHistoryBottomSheet.kt | 17 +- .../DialogFetchNewsInBackgroundSettings.kt | 117 -------- .../ui/view/main/MainViewDashboard.kt | 250 +++++++++++++++--- .../ui/view/settings/ExperimentSettings.kt | 40 +-- .../dutschedule/ui/view/settings/MainView.kt | 146 ++++++---- .../view/settings/NewsNotificationSettings.kt | 16 +- .../settings/ParseNewsSubjectNotification.kt | 2 +- .../dutschedule/viewmodel/MainViewModel.kt | 8 +- .../res/drawable/google_fonts_globe_24.xml | 9 + .../res/drawable/google_fonts_science_24.xml | 9 + .../res/drawable/ic_baseline_contrast_24.xml | 5 + .../res/drawable/ic_baseline_dark_mode_24.xml | 5 + .../res/drawable/ic_baseline_image_24.xml | 5 + .../drawable/ic_outline_calendar_clock_24.xml | 5 + app/src/main/res/values-vi/strings.xml | 8 + app/src/main/res/values/strings.xml | 8 + 25 files changed, 426 insertions(+), 544 deletions(-) delete mode 100644 .idea/deploymentTargetDropDown.xml delete mode 100644 app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogFetchNewsInBackgroundSettings.kt create mode 100644 app/src/main/res/drawable/google_fonts_globe_24.xml create mode 100644 app/src/main/res/drawable/google_fonts_science_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_contrast_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_dark_mode_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_image_24.xml create mode 100644 app/src/main/res/drawable/ic_outline_calendar_clock_24.xml diff --git a/.gitignore b/.gitignore index 8ed3e72..38d628e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ .cxx local.properties *.log +.idea/deploymentTargetDropDown.xml diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml deleted file mode 100644 index 0c0c338..0000000 --- a/.idea/deploymentTargetDropDown.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 605f5d0..5c040b0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -43,9 +43,11 @@ android { composeOptions { kotlinCompilerExtensionVersion '1.4.3' } + + // https://stackoverflow.com/a/77068664 packaging { resources { - excludes += '/META-INF/{AL2.0,LGPL2.1}' + it.excludes += '/META-INF/{AL2.0,LGPL2.1}' } } } @@ -54,26 +56,23 @@ dependencies { implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' implementation 'androidx.activity:activity-compose:1.8.2' - implementation platform('androidx.compose:compose-bom:2023.10.01') - implementation "androidx.compose.ui:ui:1.6.1" - implementation "androidx.compose.ui:ui-tooling-preview:1.6.1" + implementation platform('androidx.compose:compose-bom:2024.02.01') + implementation "androidx.compose.ui:ui:1.6.2" + implementation "androidx.compose.ui:ui-tooling-preview:1.6.2" implementation 'androidx.compose.material3:material3' - implementation platform('androidx.compose:compose-bom:2023.10.01') + implementation platform('androidx.compose:compose-bom:2024.02.01') implementation 'androidx.compose.ui:ui-graphics' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - androidTestImplementation platform('androidx.compose:compose-bom:2023.10.01') - androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.6.1" - androidTestImplementation platform('androidx.compose:compose-bom:2023.10.01') - debugImplementation "androidx.compose.ui:ui-tooling:1.6.1" - debugImplementation "androidx.compose.ui:ui-test-manifest:1.6.1" + androidTestImplementation platform('androidx.compose:compose-bom:2024.02.01') + androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.6.2" + androidTestImplementation platform('androidx.compose:compose-bom:2024.02.01') + debugImplementation "androidx.compose.ui:ui-tooling:1.6.2" + debugImplementation "androidx.compose.ui:ui-test-manifest:1.6.2" implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.appcompat:appcompat-resources:1.6.1" - - // implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0' - // https://mvnrepository.com/artifact/androidx.navigation/navigation-compose implementation 'androidx.navigation:navigation-compose:2.7.7' @@ -94,7 +93,7 @@ dependencies { implementation 'androidx.work:work-runtime-ktx:2.9.0' // https://mvnrepository.com/artifact/androidx.compose.material/material-icons-extended - runtimeOnly 'androidx.compose.material:material-icons-extended:1.6.1' + runtimeOnly 'androidx.compose.material:material-icons-extended:1.6.2' // Google Dagger/Hilt implementation 'com.google.dagger:hilt-android:2.49' @@ -111,9 +110,6 @@ dependencies { // https://mvnrepository.com/artifact/com.google.accompanist/accompanist-pager-indicators runtimeOnly 'com.google.accompanist:accompanist-pager-indicators:0.33.2-alpha' - // Accompanist - Drawable Painter - // implementation 'com.google.accompanist:accompanist-drawablepainter:0.23.1' - // Jsoup HTML parser library - https://mvnrepository.com/artifact/org.jsoup/jsoup implementation 'org.jsoup:jsoup:1.17.2' @@ -121,7 +117,7 @@ dependencies { implementation 'com.google.code.gson:gson:2.10.1' // Android Browser (use WebView in app) - https://mvnrepository.com/artifact/androidx.browser/browser - implementation 'androidx.browser:browser:1.8.0-beta02' + implementation 'androidx.browser:browser:1.8.0-rc01' // https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12' diff --git a/app/src/main/java/io/zoemeow/dutschedule/activity/BaseActivity.kt b/app/src/main/java/io/zoemeow/dutschedule/activity/BaseActivity.kt index d7b5d87..3529c5b 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/activity/BaseActivity.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/activity/BaseActivity.kt @@ -181,16 +181,13 @@ abstract class BaseActivity: ComponentActivity() { } } - fun saveSettings() { - mainViewModel.saveSettings() - } - fun showSnackBar( text: String, clearPrevious: Boolean = false, duration: SnackbarDuration = SnackbarDuration.Short, actionText: String? = null, - action: (() -> Unit)? = null + action: (() -> Unit)? = null, + onDismiss: (() -> Unit)? = null ) { snackBarScope.launch { if (clearPrevious) { @@ -207,6 +204,9 @@ abstract class BaseActivity: ComponentActivity() { SnackbarResult.ActionPerformed -> { if (actionText != null) action?.let { it() } } + SnackbarResult.Dismissed -> { + onDismiss?.let { it() } + } else -> { } } } diff --git a/app/src/main/java/io/zoemeow/dutschedule/activity/MainActivity.kt b/app/src/main/java/io/zoemeow/dutschedule/activity/MainActivity.kt index 8d62b44..5071c3b 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/activity/MainActivity.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/activity/MainActivity.kt @@ -3,38 +3,14 @@ package io.zoemeow.dutschedule.activity import android.content.Context import android.content.Intent import android.util.Log -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp import dagger.hilt.android.AndroidEntryPoint -import io.zoemeow.dutschedule.model.CustomClock -import io.zoemeow.dutschedule.model.ProcessState -import io.zoemeow.dutschedule.model.news.NewsCache import io.zoemeow.dutschedule.service.BaseService import io.zoemeow.dutschedule.service.NewsBackgroundUpdateService -import io.zoemeow.dutschedule.ui.component.main.DateAndTimeSummaryItem -import io.zoemeow.dutschedule.ui.component.main.LessonTodaySummaryItem -import io.zoemeow.dutschedule.ui.component.main.SchoolNewsSummaryItem -import io.zoemeow.dutschedule.ui.component.main.UpdateAvailableSummaryItem -import io.zoemeow.dutschedule.ui.component.main.notification.NotificationHistoryBottomSheet import io.zoemeow.dutschedule.ui.view.main.MainViewDashboard -import io.zoemeow.dutschedule.utils.CustomDateUtil import io.zoemeow.dutschedule.utils.NotificationsUtil -import kotlinx.coroutines.launch -import kotlinx.datetime.Clock -import kotlinx.datetime.LocalDateTime -import kotlinx.datetime.LocalTime -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toInstant -import kotlinx.datetime.toLocalDateTime -import kotlin.time.Duration.Companion.days @AndroidEntryPoint class MainActivity : BaseActivity() { @@ -49,7 +25,6 @@ class MainActivity : BaseActivity() { NotificationsUtil.initializeNotificationChannel(this) } - @OptIn(ExperimentalMaterial3Api::class) @Composable override fun OnMainView( context: Context, @@ -57,26 +32,12 @@ class MainActivity : BaseActivity() { containerColor: Color, contentColor: Color ) { - val isNotificationOpened = remember { mutableStateOf(false) } - val notificationModalBottomSheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true - ) - val notificationSheetScope = rememberCoroutineScope() MainViewDashboard( + context = context, snackBarHostState = snackBarHostState, containerColor = containerColor, contentColor = contentColor, - notificationList = getMainViewModel().notificationHistory, - notificationClicked = { - // Notification list requested - notificationSheetScope.launch { - if (!isNotificationOpened.value) { - isNotificationOpened.value = true - } - notificationModalBottomSheetState.expand() - } - }, newsClicked = { context.startActivity(Intent(context, NewsActivity::class.java)) }, @@ -90,214 +51,8 @@ class MainActivity : BaseActivity() { val intent = Intent(context, HelpActivity::class.java) intent.action = "view_externallink" context.startActivity(intent) - }, - content = { - DateAndTimeSummaryItem( - padding = PaddingValues(bottom = 10.dp, start = 15.dp, end = 15.dp), - isLoading = getMainViewModel().currentSchoolWeek.processState.value == ProcessState.Running, - currentSchoolWeek = getMainViewModel().currentSchoolWeek.data.value, - opacity = getControlBackgroundAlpha() - ) - LessonTodaySummaryItem( - padding = PaddingValues(bottom = 10.dp, start = 15.dp, end = 15.dp), - hasLoggedIn = getMainViewModel().accountSession.accountSession.processState.value == ProcessState.Successful, - isLoading = getMainViewModel().accountSession.accountSession.processState.value == ProcessState.Running || getMainViewModel().accountSession.subjectSchedule.processState.value == ProcessState.Running, - clicked = { - getMainViewModel().accountSession.reLogin( - onCompleted = { - if (it) { - val intent = Intent(context, AccountActivity::class.java) - intent.action = "subject_schedule" - context.startActivity(intent) - } - } - ) - }, - affectedList = getMainViewModel().accountSession.subjectSchedule.data.filter { subSch -> - subSch.subjectStudy.scheduleList.any { schItem -> schItem.dayOfWeek + 1 == CustomDateUtil.getCurrentDayOfWeek() } && - subSch.subjectStudy.scheduleList.any { schItem -> - schItem.lesson.end >= CustomClock.getCurrent().toDUTLesson2().lesson - } - }.toList(), - opacity = getControlBackgroundAlpha() - ) -// AffectedLessonsSummaryItem( -// padding = PaddingValues(bottom = 10.dp, start = 15.dp, end = 15.dp), -// hasLoggedIn = getMainViewModel().accountSession.value.processState == ProcessState.Successful, -// isLoading = getMainViewModel().accountSession.value.processState == ProcessState.Running || getMainViewModel().subjectSchedule.processState.value == ProcessState.Running, -// clicked = {}, -// affectedList = arrayListOf("ie1i0921d - i029di12", "ie1i0921d - i029di12","ie1i0921d - i029di12","ie1i0921d - i029di12","ie1i0921d - i029di12"), -// opacity = getControlBackgroundAlpha() -// ) - SchoolNewsSummaryItem( - padding = PaddingValues(bottom = 10.dp, start = 15.dp, end = 15.dp), - newsToday = getNews(false), - newsThisWeek = getNews(true), - clicked = { - context.startActivity(Intent(context, NewsActivity::class.java)) - }, - isLoading = getMainViewModel().newsGlobal.processState.value == ProcessState.Running, - opacity = getControlBackgroundAlpha() - ) - UpdateAvailableSummaryItem( - padding = PaddingValues(bottom = 10.dp, start = 15.dp, end = 15.dp), - isLoading = false, - updateAvailable = false, - latestVersionString = "", - clicked = { - openLink( - url = "https://github.com/ZoeMeow1027/DutSchedule/releases", - context = context, - customTab = false, - ) - }, - opacity = getControlBackgroundAlpha() - ) -// Text( -// "Navigation", -// modifier = Modifier -// .fillMaxWidth() -// .padding(top = 15.dp, bottom = 10.dp, start = 15.dp, end = 15.dp), -// textAlign = TextAlign.Start, -// style = MaterialTheme.typography.titleMedium -// ) -// FlowRow( -// modifier = Modifier -// .fillMaxWidth() -// .padding(bottom = 10.dp, start = 15.dp, end = 15.dp), -// maxItemsInEachRow = 2, -// horizontalArrangement = Arrangement.spacedBy(10.dp), -// content = { -// NavButton( -// modifier = Modifier.weight(0.5f).padding(bottom = 10.dp), -// opacity = getControlBackgroundAlpha(), -// badgeText = null, -// badgeContent = { -// Icon( -// painter = painterResource(id = R.drawable.ic_baseline_newspaper_24), -// "News", -// modifier = Modifier.size(27.dp) -// ) -// }, -// clicked = { -// context.startActivity(Intent(context, NewsActivity::class.java)) -// }, -// title = "News", -// description = null -// ) -// NavButton( -// modifier = Modifier.weight(0.5f).padding(bottom = 10.dp), -// opacity = getControlBackgroundAlpha(), -// badgeText = null, -// badgeContent = { -// when (getMainViewModel().accountSession.value.processState) { -// ProcessState.Running -> CircularProgressIndicator( -// modifier = Modifier.size(27.dp), -// strokeWidth = 3.dp -// ) -// else -> Icon( -// Icons.Outlined.AccountCircle, -// "Account", -// modifier = Modifier.size(27.dp) -// ) -// } -// }, -// clicked = { -// context.startActivity(Intent(context, AccountActivity::class.java)) -// }, -// title = "Account", -// description = getMainViewModel().accountSession.value.let { -// when (it.processState) { -// ProcessState.NotRunYet -> "Not logged in" -// ProcessState.Running -> "Fetching..." -// ProcessState.Failed -> when (it.data.accountAuth.username == null) { -// true -> "Not logged in" -// false -> String.format( -// "%s (expired)", -// it.data.accountAuth.username -// ) -// } -// else -> it.data.accountAuth.username ?: "Unknown" -// } -// } -// ) -// NavButton( -// modifier = Modifier.weight(0.5f).padding(bottom = 10.dp), -// opacity = getControlBackgroundAlpha(), -// badgeText = when (getMainViewModel().accountSession.value.processState) { -// ProcessState.Failed -> "" -// else -> null -// }, -// badgeContent = { -// Icon( -// painter = painterResource(id = R.drawable.ic_baseline_web_24), -// "External links", -// modifier = Modifier.size(27.dp) -// ) -// }, -// clicked = { -// val intent = Intent(context, HelpActivity::class.java) -// intent.action = "view_externallink" -// context.startActivity(intent) -// }, -// title = "External links", -// description = null -// ) -// NavButton( -// modifier = Modifier.weight(0.5f).padding(bottom = 10.dp), -// opacity = getControlBackgroundAlpha(), -// badgeText = null, -// badgeContent = { -// Icon( -// Icons.Default.Settings, -// "Settings", -// modifier = Modifier.size(27.dp) -// ) -// }, -// clicked = { -// context.startActivity(Intent(context, SettingsActivity::class.java)) -// }, -// title = "Settings", -// description = null -// ) -// } -// ) } ) - NotificationHistoryBottomSheet( - itemList = getMainViewModel().notificationHistory, - visible = isNotificationOpened.value, - sheetState = notificationModalBottomSheetState, - onDismiss = { isNotificationOpened.value = false }, - onClearItem = { }, - onClearAll = { } - ) - } - - private fun getNews(byWeek: Boolean = false): Int { - var data = 0 - val today = LocalDateTime( - Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date, - LocalTime(0, 0, 0) - ).toInstant(TimeZone.UTC) - val before7Days = today.minus(7.days) - - if (!byWeek) { - (getMainViewModel().newsGlobal.data.value ?: NewsCache()).newsListByDate.firstOrNull { - // https://stackoverflow.com/questions/77368433/how-to-get-current-date-with-reset-time-0000-with-kotlinx-localdatetime - it.date == today.toEpochMilliseconds() - }.also { - if (it != null) data = it.itemList.count() - } - } else { - (getMainViewModel().newsGlobal.data.value ?: NewsCache()).newsListByDate.forEach { - // https://stackoverflow.com/questions/77368433/how-to-get-current-date-with-reset-time-0000-with-kotlinx-localdatetime - if (it.date <= today.toEpochMilliseconds() && it.date >= before7Days.toEpochMilliseconds()) { - data += it.itemList.count() - } - } - } - return data } override fun onStop() { diff --git a/app/src/main/java/io/zoemeow/dutschedule/activity/PermissionRequestActivity.kt b/app/src/main/java/io/zoemeow/dutschedule/activity/PermissionRequestActivity.kt index 6a6561d..a7f14ee 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/activity/PermissionRequestActivity.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/activity/PermissionRequestActivity.kt @@ -185,7 +185,7 @@ class PermissionRequestActivity : BaseActivity() { ActivityResultContracts.RequestMultiplePermissions() ) { result -> // val permissionResultList = arrayListOf>() - result.toList().forEach { item -> + result.toList().forEach { _ -> // item -> // permissionResultList.add(Pair(item.first, item.second)) } diff --git a/app/src/main/java/io/zoemeow/dutschedule/service/NewsBackgroundUpdateService.kt b/app/src/main/java/io/zoemeow/dutschedule/service/NewsBackgroundUpdateService.kt index cd30791..56561cb 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/service/NewsBackgroundUpdateService.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/service/NewsBackgroundUpdateService.kt @@ -63,12 +63,11 @@ class NewsBackgroundUpdateService : BaseService( // Notify? // 0: All, 1: News global only, 2: News subject only, 3: News global and news subject with filter. - val nofityType = intent?.getIntExtra("news.service.variable.notifytype", 0) ?: 0 + // val nofityType = intent?.getIntExtra("news.service.variable.notifytype", 0) ?: 0 when (intent?.action) { "news.service.action.fetchglobal" -> { fetchNewsGlobal( - notify = nofityType, fetchType = when (fetchType) { 0 -> NewsFetchType.NextPage 1 -> NewsFetchType.FirstPage @@ -79,7 +78,6 @@ class NewsBackgroundUpdateService : BaseService( } "news.service.action.fetchsubject" -> { fetchNewsSubject( - notify = nofityType, fetchType = when (fetchType) { 0 -> NewsFetchType.NextPage 1 -> NewsFetchType.FirstPage @@ -108,11 +106,9 @@ class NewsBackgroundUpdateService : BaseService( } "news.service.action.fetchallpage1background" -> { fetchNewsGlobal( - notify = nofityType, fetchType = NewsFetchType.FirstPage ) fetchNewsSubject( - notify = nofityType, fetchType = NewsFetchType.FirstPage ) @@ -134,7 +130,6 @@ class NewsBackgroundUpdateService : BaseService( } private fun fetchNewsGlobal( - notify: Int = 0, fetchType: NewsFetchType = NewsFetchType.NextPage ) { try { @@ -255,7 +250,6 @@ class NewsBackgroundUpdateService : BaseService( } private fun fetchNewsSubject( - notify: Int = 0, fetchType: NewsFetchType = NewsFetchType.NextPage ) { try { @@ -413,7 +407,7 @@ class NewsBackgroundUpdateService : BaseService( addToNotificationList( title = newsItem.title, description = newsItem.contentString, - newsDate = System.currentTimeMillis(), + newsDate = newsItem.date, type = NewsType.Global, jsonData = Gson().toJson(newsItem) ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/DialogBase.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/DialogBase.kt index 6fb1459..f5c2d2d 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/DialogBase.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/DialogBase.kt @@ -81,7 +81,7 @@ fun DialogBase( ) { Text( title, - style = TextStyle(fontSize = 27.sp), + style = TextStyle(fontSize = 24.sp), modifier = Modifier.fillMaxWidth().padding(bottom = 15.dp), textAlign = if (isTitleCentered) TextAlign.Center else TextAlign.Start ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/OptionSwitchItem.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/OptionSwitchItem.kt index 82f76a3..8ab2d4f 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/OptionSwitchItem.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/OptionSwitchItem.kt @@ -1,14 +1,18 @@ package io.zoemeow.dutschedule.ui.component.base import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.Switch import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp @Composable fun OptionSwitchItem( modifier: Modifier = Modifier, + modifierInside: Modifier = Modifier.padding(vertical = 15.dp), + leadingIcon: (@Composable () -> Unit)? = null, title: String, description: String? = null, onValueChanged: (Boolean) -> Unit, @@ -18,8 +22,10 @@ fun OptionSwitchItem( ) { OptionItem( modifier = modifier, + modifierInside = modifierInside, title = title, description = description, + leadingIcon = leadingIcon, trailingIcon = { Switch( checked = isChecked, diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationHistoryBottomSheet.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationHistoryBottomSheet.kt index f25f9c8..3a94d1d 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationHistoryBottomSheet.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationHistoryBottomSheet.kt @@ -35,6 +35,7 @@ import io.zoemeow.dutschedule.utils.getRandomString @OptIn(ExperimentalMaterial3Api::class) @Composable fun MainActivity.NotificationHistoryBottomSheet( + snackbarHost: (@Composable () -> Unit)? = null, visible: Boolean = false, sheetState: SheetState, itemList: List = listOf(), @@ -49,6 +50,7 @@ fun MainActivity.NotificationHistoryBottomSheet( sheetState = sheetState, ) { MainView( + snackbarHost = snackbarHost, itemList = itemList, opacity = opacity, onClearItem = onClearItem, @@ -61,6 +63,7 @@ fun MainActivity.NotificationHistoryBottomSheet( @Composable private fun MainView( itemList: List, + snackbarHost: (@Composable () -> Unit)? = null, clearAllRequested: (() -> Unit)? = null, onClearItem: ((NotificationHistory) -> Unit)? = null, opacity: Float = 1f @@ -70,7 +73,7 @@ private fun MainView( .fillMaxWidth() .fillMaxHeight(0.7f) .padding(horizontal = 15.dp) - .verticalScroll(rememberScrollState()) + .verticalScroll(rememberScrollState()), ) { Row( modifier = Modifier @@ -96,7 +99,9 @@ private fun MainView( } if (itemList.isEmpty()) { Box( - modifier = Modifier.fillMaxSize().padding(top = 15.dp), + modifier = Modifier + .fillMaxSize() + .padding(top = 15.dp), contentAlignment = Alignment.Center ) { Text("No notifications") @@ -122,6 +127,14 @@ private fun MainView( } Spacer(modifier = Modifier.size(9.dp)) } + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(0.7f), + verticalArrangement = Arrangement.Bottom + ) { + snackbarHost?.let { it() } + } } @Preview() diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogFetchNewsInBackgroundSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogFetchNewsInBackgroundSettings.kt deleted file mode 100644 index a699cec..0000000 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogFetchNewsInBackgroundSettings.kt +++ /dev/null @@ -1,117 +0,0 @@ -package io.zoemeow.dutschedule.ui.component.settings.dialog - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Slider -import androidx.compose.material3.SliderDefaults -import androidx.compose.material3.SuggestionChip -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import io.zoemeow.dutschedule.activity.SettingsActivity -import io.zoemeow.dutschedule.ui.component.base.DialogBase - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun SettingsActivity.DialogFetchNewsInBackgroundSettings( - isVisible: Boolean = false, - value: Int = 0, - onDismiss: () -> Unit, - onValueChanged: (Int) -> Unit -) { - val duration = remember { mutableIntStateOf(0) } - - LaunchedEffect(isVisible) { - duration.intValue = value - } - - DialogBase( - modifier = Modifier - .fillMaxWidth() - .padding(25.dp), - title = "Fetch news in background", - isVisible = isVisible, - canDismiss = true, - isTitleCentered = true, - dismissClicked = onDismiss, - content = { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, - modifier = Modifier.fillMaxWidth(), - ) { - Text( - "Drag slider below to adjust news background duration." + - "\n - Drag slider to 0 to disable this function." + - "\n - If you set this value below than 5 minutes, this will automatically adjust back to 5 minutes.", - modifier = Modifier.padding(bottom = 10.dp) - ) - Slider( - valueRange = 0f..240f, - steps = 241, - value = duration.intValue.toFloat(), - colors = SliderDefaults.colors( - activeTickColor = Color.Transparent, - activeTrackColor = MaterialTheme.colorScheme.primary, - inactiveTickColor = Color.Transparent, - inactiveTrackColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.4f) - ), - onValueChange = { - duration.intValue = it.toInt() - } - ) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - content = { - Text("${duration.intValue} minute${if (duration.intValue != 1) "s" else ""}") - } - ) - FlowRow( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(top = 7.dp), - horizontalArrangement = Arrangement.Center, - content = { - listOf(0, 5, 15, 30, 60).forEach { min -> - SuggestionChip( - modifier = Modifier.padding(horizontal = 5.dp), - onClick = { - duration.intValue = min - }, - label = { Text(if (min == 0) "Turn off" else "$min min") } - ) - } - } - ) - } - }, - actionButtons = { - TextButton( - onClick = { onValueChanged(duration.intValue) }, - content = { Text("Save") }, - modifier = Modifier.padding(start = 8.dp), - ) - TextButton( - onClick = onDismiss, - content = { Text("Cancel") }, - modifier = Modifier.padding(start = 8.dp), - ) - } - ) -} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt index 8ea6933..2359b05 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt @@ -1,9 +1,11 @@ package io.zoemeow.dutschedule.ui.view.main -import androidx.compose.foundation.clickable +import android.content.Context +import android.content.Intent +import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -23,6 +25,7 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -31,7 +34,11 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -39,25 +46,72 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import io.zoemeow.dutschedule.R +import io.zoemeow.dutschedule.activity.AccountActivity import io.zoemeow.dutschedule.activity.MainActivity -import io.zoemeow.dutschedule.model.NotificationHistory +import io.zoemeow.dutschedule.activity.NewsActivity +import io.zoemeow.dutschedule.model.CustomClock import io.zoemeow.dutschedule.model.ProcessState +import io.zoemeow.dutschedule.model.news.NewsCache +import io.zoemeow.dutschedule.ui.component.main.DateAndTimeSummaryItem +import io.zoemeow.dutschedule.ui.component.main.LessonTodaySummaryItem +import io.zoemeow.dutschedule.ui.component.main.SchoolNewsSummaryItem +import io.zoemeow.dutschedule.ui.component.main.UpdateAvailableSummaryItem +import io.zoemeow.dutschedule.ui.component.main.notification.NotificationHistoryBottomSheet +import io.zoemeow.dutschedule.utils.CustomDateUtil +import kotlinx.coroutines.launch +import kotlinx.datetime.Clock +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.LocalTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant +import kotlinx.datetime.toLocalDateTime +import kotlin.time.Duration.Companion.days @OptIn(ExperimentalMaterial3Api::class) @Composable fun MainActivity.MainViewDashboard( + context: Context, snackBarHostState: SnackbarHostState, containerColor: Color, contentColor: Color, - notificationList: List, newsClicked: (() -> Unit)? = null, accountClicked: (() -> Unit)? = null, settingsClicked: (() -> Unit)? = null, - externalLinkClicked: (() -> Unit)? = null, - notificationClicked: (() -> Unit)? = null, - content: (@Composable ColumnScope.() -> Unit)? = null + externalLinkClicked: (() -> Unit)? = null ) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + val isNotificationOpened = remember { mutableStateOf(false) } + val needConfirmClearAllNotifications = remember { mutableStateOf(true) } + val notificationModalBottomSheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true + ) + val notificationSheetScope = rememberCoroutineScope() + + fun getNews(byWeek: Boolean = false): Int { + var data = 0 + val today = LocalDateTime( + Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date, + LocalTime(0, 0, 0) + ).toInstant(TimeZone.UTC) + val before7Days = today.minus(7.days) + + if (!byWeek) { + (getMainViewModel().newsGlobal.data.value ?: NewsCache()).newsListByDate.firstOrNull { + // https://stackoverflow.com/questions/77368433/how-to-get-current-date-with-reset-time-0000-with-kotlinx-localdatetime + it.date == today.toEpochMilliseconds() + }.also { + if (it != null) data = it.itemList.count() + } + } else { + (getMainViewModel().newsGlobal.data.value ?: NewsCache()).newsListByDate.forEach { + // https://stackoverflow.com/questions/77368433/how-to-get-current-date-with-reset-time-0000-with-kotlinx-localdatetime + if (it.date <= today.toEpochMilliseconds() && it.date >= before7Days.toEpochMilliseconds()) { + data += it.itemList.count() + } + } + } + return data + } Scaffold( modifier = Modifier @@ -80,66 +134,79 @@ fun MainActivity.MainViewDashboard( ), actions = { BadgedBox( - modifier = Modifier - .padding(start = 15.dp, end = 15.dp) - .clickable { newsClicked?.let { it() } }, + // modifier = Modifier.padding(start = 15.dp, end = 15.dp), badge = { // Badge { } - }, - content = { + } + ) { + IconButton( + onClick = { newsClicked?.let { it() } } + ) { Icon( painter = painterResource(id = R.drawable.ic_baseline_newspaper_24), "News", modifier = Modifier.size(27.dp) ) } - ) + } BadgedBox( - modifier = Modifier - .padding(end = 15.dp) - .clickable { settingsClicked?.let { it() } }, + // modifier = Modifier.padding(end = 15.dp), badge = { // Badge { } - }, - content = { + } + ) { + IconButton( + onClick = { settingsClicked?.let { it() } } + ) { Icon( Icons.Default.Settings, "Settings", modifier = Modifier.size(27.dp) ) } - ) + } BadgedBox( - modifier = Modifier - .padding(end = 15.dp) - .clickable { externalLinkClicked?.let { it() } }, + // modifier = Modifier.padding(end = 15.dp), badge = { // Badge { } - }, - content = { + } + ) { + IconButton(onClick = { externalLinkClicked?.let { it() } }) { Icon( painter = painterResource(id = R.drawable.ic_baseline_web_24), "External links", modifier = Modifier.size(27.dp) ) } - ) + } BadgedBox( - modifier = Modifier.padding(end = 15.dp) - .clickable { notificationClicked?.let { it() } }, + // modifier = Modifier.padding(end = 15.dp), badge = { - if (notificationList.isNotEmpty()) { + if (getMainViewModel().notificationHistory.isNotEmpty()) { Badge { - Text(notificationList.size.toString()) + Text(getMainViewModel().notificationHistory.size.toString()) } } }, content = { - Icon( - imageVector = Icons.Default.Notifications, - "Notifications", - modifier = Modifier.size(27.dp), - ) + IconButton( + onClick = { + // Open notification bottom sheet + // Notification list requested + notificationSheetScope.launch { + if (!isNotificationOpened.value) { + isNotificationOpened.value = true + } + notificationModalBottomSheetState.expand() + } + } + ) { + Icon( + imageVector = Icons.Default.Notifications, + "Notifications", + modifier = Modifier.size(27.dp), + ) + } } ) }, @@ -160,13 +227,6 @@ fun MainActivity.MainViewDashboard( when (it) { ProcessState.NotRunYet -> "Not logged in" ProcessState.Running -> "Fetching..." -// ProcessState.Failed -> when (it.data.accountAuth.username == null) { -// true -> "Not logged in" -// false -> String.format( -// "%s (failed)", -// it.data.accountAuth.username -// ) -// } else -> getMainViewModel().accountSession.accountSession.data.value?.accountAuth?.username ?: "unknown" }, style = MaterialTheme.typography.bodySmall @@ -214,11 +274,117 @@ fun MainActivity.MainViewDashboard( .fillMaxWidth() .verticalScroll(rememberScrollState()), content = { - content?.let { it() } + DateAndTimeSummaryItem( + padding = PaddingValues(bottom = 10.dp, start = 15.dp, end = 15.dp), + isLoading = getMainViewModel().currentSchoolWeek.processState.value == ProcessState.Running, + currentSchoolWeek = getMainViewModel().currentSchoolWeek.data.value, + opacity = getControlBackgroundAlpha() + ) + LessonTodaySummaryItem( + padding = PaddingValues(bottom = 10.dp, start = 15.dp, end = 15.dp), + hasLoggedIn = getMainViewModel().accountSession.accountSession.processState.value == ProcessState.Successful, + isLoading = getMainViewModel().accountSession.accountSession.processState.value == ProcessState.Running || getMainViewModel().accountSession.subjectSchedule.processState.value == ProcessState.Running, + clicked = { + getMainViewModel().accountSession.reLogin( + onCompleted = { + if (it) { + val intent = Intent(context, AccountActivity::class.java) + intent.action = "subject_schedule" + context.startActivity(intent) + } + } + ) + }, + affectedList = getMainViewModel().accountSession.subjectSchedule.data.filter { subSch -> + subSch.subjectStudy.scheduleList.any { schItem -> schItem.dayOfWeek + 1 == CustomDateUtil.getCurrentDayOfWeek() } && + subSch.subjectStudy.scheduleList.any { schItem -> + schItem.lesson.end >= CustomClock.getCurrent().toDUTLesson2().lesson + } + }.toList(), + opacity = getControlBackgroundAlpha() + ) + // AffectedLessonsSummaryItem( +// padding = PaddingValues(bottom = 10.dp, start = 15.dp, end = 15.dp), +// hasLoggedIn = getMainViewModel().accountSession.value.processState == ProcessState.Successful, +// isLoading = getMainViewModel().accountSession.value.processState == ProcessState.Running || getMainViewModel().subjectSchedule.processState.value == ProcessState.Running, +// clicked = {}, +// affectedList = arrayListOf("ie1i0921d - i029di12", "ie1i0921d - i029di12","ie1i0921d - i029di12","ie1i0921d - i029di12","ie1i0921d - i029di12"), +// opacity = getControlBackgroundAlpha() +// ) + SchoolNewsSummaryItem( + padding = PaddingValues(bottom = 10.dp, start = 15.dp, end = 15.dp), + newsToday = getNews(false), + newsThisWeek = getNews(true), + clicked = { + context.startActivity(Intent(context, NewsActivity::class.java)) + }, + isLoading = getMainViewModel().newsGlobal.processState.value == ProcessState.Running, + opacity = getControlBackgroundAlpha() + ) + UpdateAvailableSummaryItem( + padding = PaddingValues(bottom = 10.dp, start = 15.dp, end = 15.dp), + isLoading = false, + updateAvailable = false, + latestVersionString = "", + clicked = { + openLink( + url = "https://github.com/ZoeMeow1027/DutSchedule/releases", + context = context, + customTab = false, + ) + }, + opacity = getControlBackgroundAlpha() + ) }, ) } ) + NotificationHistoryBottomSheet( + snackbarHost = { SnackbarHost(hostState = snackBarHostState) }, + itemList = getMainViewModel().notificationHistory, + visible = isNotificationOpened.value, + sheetState = notificationModalBottomSheetState, + onDismiss = { isNotificationOpened.value = false }, + onClearItem = { + val itemTemp = it.clone() + getMainViewModel().notificationHistory.remove(it) + getMainViewModel().saveSettings() + showSnackBar( + text = "Deleted notifications!", + actionText = "Undo", + action = { + getMainViewModel().notificationHistory.add(itemTemp) + getMainViewModel().saveSettings() + } + ) + }, + onClearAll = { + if (needConfirmClearAllNotifications.value) { + needConfirmClearAllNotifications.value = false + showSnackBar( + text = "This action is undone! To confirm, click \"Clear all\" icon again.", + onDismiss = { + needConfirmClearAllNotifications.value = true + }, + clearPrevious = true + ) + } else { + needConfirmClearAllNotifications.value = true + getMainViewModel().notificationHistory.clear() + getMainViewModel().saveSettings() + showSnackBar( + text = "Successfully cleared all notifications!", + clearPrevious = true + ) + } + } + ) } ) + + BackHandler(isNotificationOpened.value) { + if (isNotificationOpened.value) { + isNotificationOpened.value = false + } + } } \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt index 3ac70b3..97aeb11 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt @@ -1,7 +1,6 @@ package io.zoemeow.dutschedule.ui.view.settings import android.content.Context -import android.content.Intent import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -82,12 +81,12 @@ fun SettingsActivity.ExperimentSettings( .verticalScroll(rememberScrollState()), content = { ContentRegion( - modifier = Modifier - .padding(horizontal = 20.dp) - .padding(top = 10.dp), + modifier = Modifier.padding(top = 10.dp), + textModifier = Modifier.padding(horizontal = 20.dp), text = "Global variable settings", content = { OptionItem( + modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), title = "Current school year settings", description = String.format( "Year: 20%d-20%d, Semester: %s%s", @@ -108,30 +107,12 @@ fun SettingsActivity.ExperimentSettings( ) DividerItem(padding = PaddingValues(top = 5.dp, bottom = 15.dp)) ContentRegion( - modifier = Modifier - .padding(horizontal = 20.dp) - .padding(top = 10.dp), - text = "Notifications", - content = { - OptionItem( - title = "News notifications in background settings", - description = "Configure your settings", - onClick = { - Intent(context, SettingsActivity::class.java).apply { - action = "settings_newsnotificaitonsettings" - }.also { intent -> context.startActivity(intent) } - } - ) - } - ) - DividerItem(padding = PaddingValues(top = 5.dp, bottom = 15.dp)) - ContentRegion( - modifier = Modifier - .padding(horizontal = 20.dp) - .padding(top = 10.dp), + modifier = Modifier.padding(top = 10.dp), + textModifier = Modifier.padding(horizontal = 20.dp), text = "Appearance", content = { OptionItem( + modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), title = "Background opacity", description = String.format( "%2.0f%% %s", @@ -146,6 +127,7 @@ fun SettingsActivity.ExperimentSettings( } ) OptionItem( + modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), title = "Component opacity", description = String.format( "%2.0f%% %s", @@ -163,12 +145,12 @@ fun SettingsActivity.ExperimentSettings( ) DividerItem(padding = PaddingValues(top = 5.dp, bottom = 15.dp)) ContentRegion( - modifier = Modifier - .padding(horizontal = 20.dp) - .padding(top = 10.dp), + modifier = Modifier.padding(top = 10.dp), + textModifier = Modifier.padding(horizontal = 20.dp), text = "Troubleshooting", content = { OptionItem( + modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), title = "Debug log (not work yet)", description = "Get debug log for this application to troubleshoot issues.", onClick = { @@ -190,7 +172,7 @@ fun SettingsActivity.ExperimentSettings( getMainViewModel().appSettings.value = getMainViewModel().appSettings.value.clone( currentSchoolYear = it ) - saveSettings() + getMainViewModel().saveSettings() dialogSchoolYear.value = false } ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt index af03cce..0dcd03d 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Notifications import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -31,9 +32,12 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import io.zoemeow.dutschedule.BuildConfig +import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.PermissionRequestActivity import io.zoemeow.dutschedule.activity.SettingsActivity import io.zoemeow.dutschedule.model.settings.BackgroundImageOption @@ -44,7 +48,6 @@ import io.zoemeow.dutschedule.ui.component.base.OptionSwitchItem import io.zoemeow.dutschedule.ui.component.settings.ContentRegion import io.zoemeow.dutschedule.ui.component.settings.dialog.DialogAppBackgroundSettings import io.zoemeow.dutschedule.ui.component.settings.dialog.DialogAppThemeSettings -import io.zoemeow.dutschedule.ui.component.settings.dialog.DialogFetchNewsInBackgroundSettings import java.util.Locale @OptIn(ExperimentalMaterial3Api::class) @@ -58,7 +61,6 @@ fun SettingsActivity.MainView( ) { val dialogAppTheme: MutableState = remember { mutableStateOf(false) } val dialogBackground: MutableState = remember { mutableStateOf(false) } - val dialogFetchNews: MutableState = remember { mutableStateOf(false) } val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() @@ -71,7 +73,7 @@ fun SettingsActivity.MainView( contentColor = contentColor, topBar = { LargeTopAppBar( - title = { Text("Settings") }, + title = { Text(getString(R.string.settings_name)) }, colors = TopAppBarDefaults.largeTopAppBarColors(containerColor = Color.Transparent, scrolledContainerColor = Color.Transparent), navigationIcon = { IconButton( @@ -99,39 +101,29 @@ fun SettingsActivity.MainView( content = { ContentRegion( modifier = Modifier - .padding(horizontal = 20.dp) .padding(top = 10.dp), - text = "Notifications", + textModifier = Modifier.padding(horizontal = 20.dp), + text = getString(R.string.settings_category_notifications), content = { OptionItem( - title = "Fetch news in background", - description = when { - (getMainViewModel().appSettings.value.newsBackgroundDuration > 0) -> - String.format( - "Enabled, every %d minute%s", - getMainViewModel().appSettings.value.newsBackgroundDuration, - if (getMainViewModel().appSettings.value.newsBackgroundDuration != 1) "s" else "" - ) - else -> "Disabled" + modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_outline_calendar_clock_24), + getString(R.string.settings_option_newsschedule), + modifier = Modifier.padding(end = 15.dp) + ) }, + title = getString(R.string.settings_option_newsschedule), + description = getString(R.string.settings_option_newsschedule_description), onClick = { - if (PermissionRequestActivity.checkPermissionScheduleExactAlarm(context).isGranted) { - dialogFetchNews.value = true - } else { - showSnackBar( - text = "You need to enable Alarms & Reminders in Android app settings to use this feature.", - clearPrevious = true, - actionText = "Open", - action = { - Intent(context, PermissionRequestActivity::class.java).also { - context.startActivity(it) - } - } - ) - } + Intent(context, SettingsActivity::class.java).apply { + action = "settings_newsnotificaitonsettings" + }.also { intent -> context.startActivity(intent) } } ) OptionItem( + modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), title = "News filter settings", description = "Make your filter to only receive your preferred subject news.", onClick = { @@ -142,8 +134,16 @@ fun SettingsActivity.MainView( ) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { OptionItem( - title = "System notification settings", - description = "Click here to manage app notifications in Android app settings.", + modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), + leadingIcon = { + Icon( + Icons.Default.Notifications, + getString(R.string.settings_option_notificationoutside), + modifier = Modifier.padding(end = 15.dp) + ) + }, + title = getString(R.string.settings_option_notificationoutside), + description = getString(R.string.settings_option_notificationoutside_description), onClick = { context.startActivity(Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).also { intent -> intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName) @@ -156,12 +156,20 @@ fun SettingsActivity.MainView( DividerItem(padding = PaddingValues(top = 5.dp, bottom = 15.dp)) ContentRegion( modifier = Modifier - .padding(horizontal = 20.dp) .padding(top = 10.dp), - text = "Appearance", + textModifier = Modifier.padding(horizontal = 20.dp), + text = getString(R.string.settings_category_appearance), content = { OptionItem( - title = "App theme", + modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_baseline_dark_mode_24), + getString(R.string.settings_option_apptheme), + modifier = Modifier.padding(end = 15.dp) + ) + }, + title = getString(R.string.settings_option_apptheme), description = String.format( "%s%s", when (getMainViewModel().appSettings.value.themeMode) { @@ -174,6 +182,14 @@ fun SettingsActivity.MainView( onClick = { dialogAppTheme.value = true } ) OptionSwitchItem( + modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_baseline_contrast_24), + "Black background settings", + modifier = Modifier.padding(end = 15.dp) + ) + }, title = "Black background", description = "Make app background to black color. Only in dark mode and turned off background image.", isChecked = getMainViewModel().appSettings.value.blackBackground, @@ -182,10 +198,18 @@ fun SettingsActivity.MainView( getMainViewModel().appSettings.value.clone( blackBackground = value ) - saveSettings() + getMainViewModel().saveSettings() } ) OptionItem( + modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_baseline_image_24), + "Background image settings", + modifier = Modifier.padding(end = 15.dp) + ) + }, title = "Background image", description = when (getMainViewModel().appSettings.value.backgroundImage) { BackgroundImageOption.None -> "None" @@ -198,12 +222,19 @@ fun SettingsActivity.MainView( ) DividerItem(padding = PaddingValues(top = 5.dp, bottom = 15.dp)) ContentRegion( - modifier = Modifier - .padding(horizontal = 20.dp) - .padding(top = 10.dp), + modifier = Modifier.padding(top = 10.dp), + textModifier = Modifier.padding(horizontal = 20.dp), text = "Miscellaneous settings", content = { OptionItem( + modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.google_fonts_globe_24), + "App language", + modifier = Modifier.padding(end = 15.dp) + ) + }, title = "App language", description = Locale.getDefault().displayName, onClick = { @@ -219,6 +250,7 @@ fun SettingsActivity.MainView( } ) OptionItem( + modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), title = "Application permissions", description = "Click here for allow and manage app permissions you granted.", onClick = { @@ -231,6 +263,14 @@ fun SettingsActivity.MainView( } ) OptionSwitchItem( + modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_baseline_web_24), + "App language", + modifier = Modifier.padding(end = 15.dp) + ) + }, title = "Open link inside app", description = "Open clicked link without leaving this app. Turn off to open link in default browser.", isChecked = getMainViewModel().appSettings.value.openLinkInsideApp, @@ -239,10 +279,18 @@ fun SettingsActivity.MainView( getMainViewModel().appSettings.value.clone( openLinkInsideApp = value ) - saveSettings() + getMainViewModel().saveSettings() } ) OptionItem( + modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.google_fonts_science_24), + "Experiment settings", + modifier = Modifier.padding(end = 15.dp) + ) + }, title = "Experiment settings", description = "Our current experiment settings before public.", onClick = { @@ -256,11 +304,12 @@ fun SettingsActivity.MainView( DividerItem(padding = PaddingValues(top = 5.dp, bottom = 15.dp)) ContentRegion( modifier = Modifier - .padding(horizontal = 20.dp) .padding(top = 10.dp), + textModifier = Modifier.padding(horizontal = 20.dp), text = "About", content = { OptionItem( + modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), title = "Version", description = "Current version: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})\nClick here to check for update", onClick = { @@ -269,6 +318,7 @@ fun SettingsActivity.MainView( } ) OptionItem( + modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), title = "Changelogs", description = "Tap to view app changelog", onClick = { @@ -280,6 +330,7 @@ fun SettingsActivity.MainView( } ) OptionItem( + modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), title = "GitHub (click to open link)", description = "https://github.com/ZoeMeow1027/DutSchedule", onClick = { @@ -306,7 +357,7 @@ fun SettingsActivity.MainView( themeMode = themeMode, dynamicColor = dynamicColor ) - saveSettings() + getMainViewModel().saveSettings() } ) DialogAppBackgroundSettings( @@ -349,27 +400,14 @@ fun SettingsActivity.MainView( } dialogBackground.value = false - saveSettings() - } - ) - DialogFetchNewsInBackgroundSettings( - isVisible = dialogFetchNews.value, - value = getMainViewModel().appSettings.value.newsBackgroundDuration, - onDismiss = { dialogFetchNews.value = false }, - onValueChanged = { value -> - dialogFetchNews.value = false - getMainViewModel().appSettings.value = getMainViewModel().appSettings.value.clone( - fetchNewsBackgroundDuration = value - ) getMainViewModel().saveSettings() } ) BackHandler( - enabled = dialogAppTheme.value || dialogBackground.value || dialogFetchNews.value, + enabled = dialogAppTheme.value || dialogBackground.value, onBack = { dialogAppTheme.value = false dialogBackground.value = false - dialogFetchNews.value = false } ) } \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt index 0992411..18e10b6 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt @@ -109,7 +109,7 @@ fun SettingsActivity.NewsNotificationSettings( fetchNewsBackgroundDuration = duration ) getMainViewModel().appSettings.value = dataTemp - getMainViewModel().saveSettings() + getMainViewModel().saveSettings(saveSettingsOnly = true) showSnackBar( text = "Successfully enabled fetch news in background! News will refresh every $duration minute(s).", clearPrevious = true @@ -131,7 +131,7 @@ fun SettingsActivity.NewsNotificationSettings( fetchNewsBackgroundDuration = 0 ) getMainViewModel().appSettings.value = dataTemp - getMainViewModel().saveSettings() + getMainViewModel().saveSettings(saveSettingsOnly = true) showSnackBar( text = "Successfully disabled fetch news in background!", clearPrevious = true @@ -150,7 +150,7 @@ fun SettingsActivity.NewsNotificationSettings( newsBackgroundGlobalEnabled = enabled ) getMainViewModel().appSettings.value = dataTemp - getMainViewModel().saveSettings() + getMainViewModel().saveSettings(saveSettingsOnly = true) showSnackBar( text = "Successfully ${ if (enabled) "enabled" else "disabled" @@ -172,7 +172,7 @@ fun SettingsActivity.NewsNotificationSettings( newsBackgroundSubjectEnabled = code ) getMainViewModel().appSettings.value = dataTemp - getMainViewModel().saveSettings() + getMainViewModel().saveSettings(saveSettingsOnly = true) showSnackBar( text = "Done! You will notify \"${ when (code) { @@ -188,13 +188,13 @@ fun SettingsActivity.NewsNotificationSettings( }, subjectFilterList = getMainViewModel().appSettings.value.newsBackgroundFilterList, onSubjectFilterAdd = { - + // TODO: Add a filter }, - onSubjectFilterDelete = { code -> - + onSubjectFilterDelete = { _ -> + // TODO: Delete a filter }, onSubjectFilterClear = { - + // TODO: Delete all filters }, opacity = getControlBackgroundAlpha() ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ParseNewsSubjectNotification.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ParseNewsSubjectNotification.kt index 5b7f9e0..5153c5f 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ParseNewsSubjectNotification.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ParseNewsSubjectNotification.kt @@ -117,7 +117,7 @@ fun SettingsActivity.ParseNewsSubjectNotification( getMainViewModel().appSettings.value = getMainViewModel().appSettings.value.clone( newsBackgroundParseNewsSubject = !getMainViewModel().appSettings.value.newsBackgroundParseNewsSubject ) - saveSettings() + getMainViewModel().saveSettings() } ) Column( diff --git a/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt b/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt index 0a7ab2e..caaa737 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt @@ -286,12 +286,16 @@ class MainViewModel @Inject constructor( /** * Save all current settings to file in storage. */ - fun saveSettings() { + fun saveSettings(saveSettingsOnly: Boolean = false) { launchOnScope( script = { fileModuleRepository.saveAppSettings(appSettings.value) fileModuleRepository.saveAccountSession(accountSession.getAccountSession() ?: AccountSession()) - fileModuleRepository.saveAccountSubjectScheduleCache(ArrayList(accountSession.getSubjectScheduleCache())) + + if (!saveSettingsOnly) { + fileModuleRepository.saveAccountSubjectScheduleCache(ArrayList(accountSession.getSubjectScheduleCache())) + fileModuleRepository.saveNotificationHistory(ArrayList(notificationHistory.toList())) + } } ) } diff --git a/app/src/main/res/drawable/google_fonts_globe_24.xml b/app/src/main/res/drawable/google_fonts_globe_24.xml new file mode 100644 index 0000000..dff31e2 --- /dev/null +++ b/app/src/main/res/drawable/google_fonts_globe_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/google_fonts_science_24.xml b/app/src/main/res/drawable/google_fonts_science_24.xml new file mode 100644 index 0000000..9020af4 --- /dev/null +++ b/app/src/main/res/drawable/google_fonts_science_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_contrast_24.xml b/app/src/main/res/drawable/ic_baseline_contrast_24.xml new file mode 100644 index 0000000..fe34a64 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_contrast_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_baseline_dark_mode_24.xml b/app/src/main/res/drawable/ic_baseline_dark_mode_24.xml new file mode 100644 index 0000000..b5d162c --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_dark_mode_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_baseline_image_24.xml b/app/src/main/res/drawable/ic_baseline_image_24.xml new file mode 100644 index 0000000..fd890ce --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_image_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_outline_calendar_clock_24.xml b/app/src/main/res/drawable/ic_outline_calendar_clock_24.xml new file mode 100644 index 0000000..ab5042f --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_calendar_clock_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 8dd1895..ae01a60 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -16,4 +16,12 @@ học bù Vào ngày %s với tiết học %s Phòng sẽ học bù: %s + Cài đặt + Thông báo + Cài đặt cập nhật tin tức trong nền + Cấu hình cài đặt thông báo tin tức cho bạn, bao gồm thời gian cập nhật, tin tức nào sẽ được bật, cài đặt bộ lọc tin tức và hơn thế nữa. + Cài đặt thông báo ngoài ứng dụng + Nhấn vào đây để quản lí thông báo ứng dụng trong cài đặt Android. + Hiển thị + Chủ đề ứng dụng \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f04d110..8f8882b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,4 +16,12 @@ Making up On %s at lesson(s) %s Room will make up: %s + Settings + Notifications + News schedule in background settings + Configure your news notification settings, include duration, which news is enabled, news filter settings, and more. + Notification settings outside app + Click here to manage app notifications in Android app settings. + Appearance + App theme \ No newline at end of file From 4490404e3ca71f1c652e546e552c7b55faba66d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BA=BF=20T=C3=B9ng?= Date: Fri, 15 Mar 2024 11:08:04 +0700 Subject: [PATCH 05/21] Update project - Removed LargeTopAppBar. - Removed News filter settings. This is replaced by News notification settings. - Addressed a issue cause triggered relogin. - Added "Add", "Delete a filter" and "Delete all" in news notification settings. - [Issue] Clear a notification item and undo, and reopen it won't functionally anymore. --- .../dutschedule/activity/MainActivity.kt | 1 - .../dutschedule/activity/SettingsActivity.kt | 9 - .../dutschedule/model/DUTAccountSession.kt | 116 ++++---- .../dutschedule/model/VariableListState.kt | 34 +++ .../dutschedule/model/VariableState.kt | 32 +++ .../zoemeow/dutschedule/model/news/DUTNews.kt | 38 +++ .../dutschedule/model/news/NewsCache2.kt | 13 + .../NotificationHistoryBottomSheet.kt | 18 +- .../main/notification/NotificationItem.kt | 4 +- .../settings/AddNewSubjectFilterDialog.kt | 107 ++++++++ .../settings/DeleteASubjectFilterDialog.kt | 69 +++++ .../settings/DeleteAllSubjectFilterDialog.kt | 60 +++++ .../newsfilter/NewsFilterAddInNewsSubject.kt | 29 -- .../newsfilter/NewsFilterAddManually.kt | 109 -------- .../settings/newsfilter/NewsFilterClearAll.kt | 55 ---- .../newsfilter/NewsFilterCurrentFilter.kt | 64 ----- .../ui/view/account/AccountInformation.kt | 16 +- .../dutschedule/ui/view/account/MainView.kt | 15 +- .../dutschedule/ui/view/account/SubjectFee.kt | 16 +- .../ui/view/account/SubjectInformation.kt | 15 +- .../ui/view/account/TrainingResult.kt | 16 +- .../ui/view/main/MainViewDashboard.kt | 26 +- .../ui/view/settings/ExperimentSettings.kt | 15 +- .../ui/view/settings/LanguageSettings.kt | 14 +- .../dutschedule/ui/view/settings/MainView.kt | 26 +- .../ui/view/settings/NewsFilterSettings.kt | 249 ------------------ .../view/settings/NewsNotificationSettings.kt | 86 +++++- .../settings/ParseNewsSubjectNotification.kt | 16 +- 28 files changed, 557 insertions(+), 711 deletions(-) create mode 100644 app/src/main/java/io/zoemeow/dutschedule/model/VariableListState.kt create mode 100644 app/src/main/java/io/zoemeow/dutschedule/model/VariableState.kt create mode 100644 app/src/main/java/io/zoemeow/dutschedule/model/news/DUTNews.kt create mode 100644 app/src/main/java/io/zoemeow/dutschedule/model/news/NewsCache2.kt create mode 100644 app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/AddNewSubjectFilterDialog.kt create mode 100644 app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/DeleteASubjectFilterDialog.kt create mode 100644 app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/DeleteAllSubjectFilterDialog.kt delete mode 100644 app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterAddInNewsSubject.kt delete mode 100644 app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterAddManually.kt delete mode 100644 app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterClearAll.kt delete mode 100644 app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterCurrentFilter.kt delete mode 100644 app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsFilterSettings.kt diff --git a/app/src/main/java/io/zoemeow/dutschedule/activity/MainActivity.kt b/app/src/main/java/io/zoemeow/dutschedule/activity/MainActivity.kt index 5071c3b..c6aef47 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/activity/MainActivity.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/activity/MainActivity.kt @@ -32,7 +32,6 @@ class MainActivity : BaseActivity() { containerColor: Color, contentColor: Color ) { - MainViewDashboard( context = context, snackBarHostState = snackBarHostState, diff --git a/app/src/main/java/io/zoemeow/dutschedule/activity/SettingsActivity.kt b/app/src/main/java/io/zoemeow/dutschedule/activity/SettingsActivity.kt index fd76c3a..379efd5 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/activity/SettingsActivity.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/activity/SettingsActivity.kt @@ -12,7 +12,6 @@ import io.zoemeow.dutschedule.model.settings.BackgroundImageOption import io.zoemeow.dutschedule.ui.view.settings.ExperimentSettings import io.zoemeow.dutschedule.ui.view.settings.LanguageSettings import io.zoemeow.dutschedule.ui.view.settings.MainView -import io.zoemeow.dutschedule.ui.view.settings.NewsFilterSettings import io.zoemeow.dutschedule.ui.view.settings.NewsNotificationSettings import io.zoemeow.dutschedule.ui.view.settings.ParseNewsSubjectNotification import io.zoemeow.dutschedule.utils.BackgroundImageUtil @@ -47,14 +46,6 @@ class SettingsActivity : BaseActivity() { contentColor: Color ) { when (intent.action) { - "settings_newsfilter" -> { - NewsFilterSettings( - snackBarHostState = snackBarHostState, - containerColor = containerColor, - contentColor = contentColor - ) - } - "settings_newssubjectnewparse" -> { ParseNewsSubjectNotification( snackBarHostState = snackBarHostState, diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/DUTAccountSession.kt b/app/src/main/java/io/zoemeow/dutschedule/model/DUTAccountSession.kt index 8777c89..507b21f 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/DUTAccountSession.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/DUTAccountSession.kt @@ -1,12 +1,8 @@ package io.zoemeow.dutschedule.model import android.util.Log -import androidx.compose.runtime.MutableLongState import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableLongStateOf -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.snapshots.SnapshotStateList import io.dutwrapper.dutwrapper.model.accounts.AccountInformation import io.dutwrapper.dutwrapper.model.accounts.SubjectFeeItem import io.dutwrapper.dutwrapper.model.accounts.SubjectScheduleItem @@ -40,56 +36,6 @@ class DUTAccountSession( val accountInformation: VariableState = VariableState(data = mutableStateOf(null)) val accountTrainingStatus: VariableState = VariableState(data = mutableStateOf(null)) - data class VariableState( - val data: MutableState, - val lastRequest: MutableLongState = mutableLongStateOf(0), - val processState: MutableState = mutableStateOf(ProcessState.NotRunYet) - ) { - fun isExpired(): Boolean { - return (lastRequest.longValue + ProcessVariable.expiredDuration) < System.currentTimeMillis() - } - - fun isSuccessfulRequestExpired(): Boolean { - return when (processState.value) { - ProcessState.Successful -> isExpired() - else -> true - } - } - - fun resetValue() { - if (processState.value != ProcessState.Running) { - data.value = null - lastRequest.longValue = 0 - processState.value = ProcessState.NotRunYet - } - } - } - - data class VariableListState( - val data: SnapshotStateList = mutableStateListOf(), - val lastRequest: MutableLongState = mutableLongStateOf(0), - val processState: MutableState = mutableStateOf(ProcessState.NotRunYet) - ) { - fun isExpired(): Boolean { - return (lastRequest.longValue + ProcessVariable.expiredDuration) < System.currentTimeMillis() - } - - fun isSuccessfulRequestExpired(): Boolean { - return when (processState.value) { - ProcessState.Successful -> isExpired() - else -> true - } - } - - fun resetValue() { - if (processState.value != ProcessState.Running) { - data.clear() - lastRequest.longValue = 0 - processState.value = ProcessState.NotRunYet - } - } - } - private fun launchOnScope( script: () -> Unit, onCompleted: ((Throwable?) -> Unit)? = null @@ -155,25 +101,48 @@ class DUTAccountSession( // If accountAuth isn't null, just login with new account if (accountAuth != null) { Log.d("login", "new account") - accountSession.data.value = AccountSession( - accountAuth = accountAuth.clone() - ) - dutRequestRepository.login( - accountSession = accountSession.data.value!!, - forceLogin = false, - onSessionChanged = { sId, dateUnix -> - if (dateUnix == null || dateUnix == 0L || sId == null) { - // TODO: Account session isn't valid! - throw Exception() + accountAuth.let { d1 -> + accountSession.data.value?.accountAuth.let { d2 -> + if (d1.username == d2?.username && d1.password == d2?.password) { + dutRequestRepository.login( + accountSession = accountSession.data.value!!, + forceLogin = false, + onSessionChanged = { sId, dateUnix -> + if (dateUnix == null || dateUnix == 0L || sId == null) { + // TODO: Account session isn't valid! + throw Exception() + } else { + accountSession.data.value = accountSession.data.value!!.clone( + accountAuth = accountSession.data.value!!.accountAuth, + sessionId = sId, + sessionLastRequest = dateUnix + ) + } + } + ) } else { - accountSession.data.value = accountSession.data.value!!.clone( - accountAuth = accountSession.data.value!!.accountAuth, - sessionId = sId, - sessionLastRequest = dateUnix + accountSession.data.value = AccountSession( + accountAuth = accountAuth.clone() + ) + dutRequestRepository.login( + accountSession = accountSession.data.value!!, + forceLogin = true, + onSessionChanged = { sId, dateUnix -> + if (dateUnix == null || dateUnix == 0L || sId == null) { + // TODO: Account session isn't valid! + throw Exception() + } else { + accountSession.data.value = accountSession.data.value!!.clone( + accountAuth = accountSession.data.value!!.accountAuth, + sessionId = sId, + sessionLastRequest = dateUnix + ) + } + } ) } } - ) + } } // If accountSession is exist, let's re-login. else if (accountSession.data.value != null) { @@ -262,7 +231,14 @@ class DUTAccountSession( launchOnScope( script = { // TODO: Fully logout from server - + CoroutineScope(Dispatchers.Main).launch { + withContext(Dispatchers.IO) { + accountSession.data.value?.let { + val temp = it.clone() + dutRequestRepository.logout(temp) + } + } + } accountSession.resetValue() subjectSchedule.resetValue() subjectFee.resetValue() diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/VariableListState.kt b/app/src/main/java/io/zoemeow/dutschedule/model/VariableListState.kt new file mode 100644 index 0000000..cf2f458 --- /dev/null +++ b/app/src/main/java/io/zoemeow/dutschedule/model/VariableListState.kt @@ -0,0 +1,34 @@ +package io.zoemeow.dutschedule.model + +import androidx.compose.runtime.MutableLongState +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.snapshots.SnapshotStateList + +data class VariableListState( + val data: SnapshotStateList = mutableStateListOf(), + val lastRequest: MutableLongState = mutableLongStateOf(0), + val processState: MutableState = mutableStateOf(ProcessState.NotRunYet), + val parameters: MutableMap = mutableMapOf() +) { + fun isExpired(): Boolean { + return (lastRequest.longValue + ProcessVariable.expiredDuration) < System.currentTimeMillis() + } + + fun isSuccessfulRequestExpired(): Boolean { + return when (processState.value) { + ProcessState.Successful -> isExpired() + else -> true + } + } + + fun resetValue() { + if (processState.value != ProcessState.Running) { + data.clear() + lastRequest.longValue = 0 + processState.value = ProcessState.NotRunYet + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/VariableState.kt b/app/src/main/java/io/zoemeow/dutschedule/model/VariableState.kt new file mode 100644 index 0000000..e8a37d5 --- /dev/null +++ b/app/src/main/java/io/zoemeow/dutschedule/model/VariableState.kt @@ -0,0 +1,32 @@ +package io.zoemeow.dutschedule.model + +import androidx.compose.runtime.MutableLongState +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf + +data class VariableState( + val data: MutableState, + val lastRequest: MutableLongState = mutableLongStateOf(0), + val processState: MutableState = mutableStateOf(ProcessState.NotRunYet), + val parameters: MutableMap = mutableMapOf() +) { + fun isExpired(): Boolean { + return (lastRequest.longValue + ProcessVariable.expiredDuration) < System.currentTimeMillis() + } + + fun isSuccessfulRequestExpired(): Boolean { + return when (processState.value) { + ProcessState.Successful -> isExpired() + else -> true + } + } + + fun resetValue() { + if (processState.value != ProcessState.Running) { + data.value = null + lastRequest.longValue = 0 + processState.value = ProcessState.NotRunYet + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/news/DUTNews.kt b/app/src/main/java/io/zoemeow/dutschedule/model/news/DUTNews.kt new file mode 100644 index 0000000..f0bdf70 --- /dev/null +++ b/app/src/main/java/io/zoemeow/dutschedule/model/news/DUTNews.kt @@ -0,0 +1,38 @@ +package io.zoemeow.dutschedule.model.news + +import io.dutwrapper.dutwrapper.model.news.NewsGlobalItem +import io.dutwrapper.dutwrapper.model.news.NewsSubjectItem +import io.zoemeow.dutschedule.model.VariableListState +import io.zoemeow.dutschedule.repository.DutRequestRepository + +/** + * @param onEventSent Event when done: + * 1: News global + * 2: News subject + */ +class DUTNews( + private val dutRequestRepository: DutRequestRepository, + private val onEventSent: ((Int) -> Unit)? = null +) { + companion object { + fun VariableListState.getPage(): Int { + return try { + this.parameters["page"]?.toInt() ?: 1 + } catch (_: Exception) { + 1 + } + } + + fun VariableListState.setPage(page: Int = 1) { + if (page < 1) { + throw Exception("") + } + this.parameters["page"] = page.toString() + } + } + + val newsGlobal: VariableListState = VariableListState() + val newsSubject: VariableListState = VariableListState() + + +} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/news/NewsCache2.kt b/app/src/main/java/io/zoemeow/dutschedule/model/news/NewsCache2.kt new file mode 100644 index 0000000..61e879b --- /dev/null +++ b/app/src/main/java/io/zoemeow/dutschedule/model/news/NewsCache2.kt @@ -0,0 +1,13 @@ +package io.zoemeow.dutschedule.model.news + +import com.google.gson.annotations.SerializedName +import java.io.Serializable + +data class NewsCache2( + @SerializedName("data") + val data: ArrayList = arrayListOf(), + @SerializedName("nextpage") + var nextPage: Int = 1, + @SerializedName("lastrequest") + var lastRequest: Long = 0 +) : Serializable diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationHistoryBottomSheet.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationHistoryBottomSheet.kt index 3a94d1d..691c7e6 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationHistoryBottomSheet.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationHistoryBottomSheet.kt @@ -40,7 +40,8 @@ fun MainActivity.NotificationHistoryBottomSheet( sheetState: SheetState, itemList: List = listOf(), onDismiss: () -> Unit, - onClearItem: ((NotificationHistory) -> Unit)? = null, + onClick: ((NotificationHistory) -> Unit)? = null, + onClear: ((NotificationHistory) -> Unit)? = null, onClearAll: (() -> Unit)? = null, opacity: Float = 1f ) { @@ -53,8 +54,9 @@ fun MainActivity.NotificationHistoryBottomSheet( snackbarHost = snackbarHost, itemList = itemList, opacity = opacity, - onClearItem = onClearItem, - clearAllRequested = onClearAll + onClick = onClick, + onClear = onClear, + onClearAll = onClearAll ) } } @@ -64,8 +66,9 @@ fun MainActivity.NotificationHistoryBottomSheet( private fun MainView( itemList: List, snackbarHost: (@Composable () -> Unit)? = null, - clearAllRequested: (() -> Unit)? = null, - onClearItem: ((NotificationHistory) -> Unit)? = null, + onClick: ((NotificationHistory) -> Unit)? = null, + onClear: ((NotificationHistory) -> Unit)? = null, + onClearAll: (() -> Unit)? = null, opacity: Float = 1f ) { Column( @@ -89,7 +92,7 @@ private fun MainView( if (itemList.isNotEmpty()) { IconButton( onClick = { - clearAllRequested?.let { it() } + onClearAll?.let { it() } }, content = { Icon(ImageVector.vectorResource(id = R.drawable.ic_baseline_clear_all_24), "") @@ -119,7 +122,8 @@ private fun MainView( NotificationItem( modifier = Modifier.padding(bottom = 5.dp), opacity = opacity, - onClear = { onClearItem?.let { it(item) } }, + onClick = { onClick?.let { it(item) } }, + onClear = { onClear?.let { it(item) } }, item = item ) } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationItem.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationItem.kt index 142b1ca..151c7eb 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationItem.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationItem.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.component.main.notification +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -30,11 +31,12 @@ fun NotificationItem( modifier: Modifier = Modifier, item: NotificationHistory, showDate: Boolean = false, + onClick: (() -> Unit)? = null, onClear: (() -> Unit)? = null, opacity: Float = 1f ) { Surface( - modifier = modifier, + modifier = modifier.clickable { onClick?.let { it() } }, shape = RoundedCornerShape(5.dp), color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = opacity), content = { diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/AddNewSubjectFilterDialog.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/AddNewSubjectFilterDialog.kt new file mode 100644 index 0000000..3c51764 --- /dev/null +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/AddNewSubjectFilterDialog.kt @@ -0,0 +1,107 @@ +package io.zoemeow.dutschedule.ui.component.settings + +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.zoemeow.dutschedule.activity.SettingsActivity +import io.zoemeow.dutschedule.ui.component.base.DialogBase + +@Composable +fun SettingsActivity.AddNewSubjectFilterDialog( + isVisible: Boolean = false, + onDismiss: (() -> Unit)? = null, + onDone: ((String, String, String) -> Unit)? = null +) { + val schoolYearId = remember { mutableStateOf("") } + val classId = remember { mutableStateOf("") } + val subjectName = remember { mutableStateOf("") } + + DialogBase( + modifier = Modifier.fillMaxWidth().padding(25.dp), + title = "Add new filter", + isVisible = isVisible, + canDismiss = false, + dismissClicked = { onDismiss?.let { it() } }, + content = { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = "Enter your subject filter (you can view templates in sv.dut.udn.vn) and tap \"Add\" to add to filter above.\n\nExample:\n - 19 | 01 | Subject A\n - xx | 94A | Subject B\n\nNote:\n- You need to enter carefully, otherwise you won\'t received notifications exactly.", + modifier = Modifier.padding(bottom = 5.dp) + ) + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(bottom = 10.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + OutlinedTextField( + value = schoolYearId.value, + onValueChange = { if (it.length <= 2) schoolYearId.value = it }, + label = { Text("School year ID") }, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .weight(0.5f) + ) + Spacer(modifier = Modifier.size(10.dp)) + OutlinedTextField( + value = classId.value, + onValueChange = { if (it.length <= 3) classId.value = it }, + label = { Text("Class ID") }, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .weight(0.5f) + ) + } + Spacer(modifier = Modifier.size(5.dp)) + OutlinedTextField( + value = subjectName.value, + onValueChange = { subjectName.value = it }, + label = { Text("Subject name") }, + modifier = Modifier.fillMaxWidth() + ) + } + } + }, + actionButtons = { + TextButton( + onClick = { onDismiss?.let { it() } }, + content = { Text("Cancel") }, + modifier = Modifier.padding(start = 8.dp), + ) + TextButton( + onClick = { onDone?.let { it(schoolYearId.value, classId.value,subjectName.value) } }, + content = { Text("Save") }, + modifier = Modifier.padding(start = 8.dp), + ) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/DeleteASubjectFilterDialog.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/DeleteASubjectFilterDialog.kt new file mode 100644 index 0000000..7a26bd2 --- /dev/null +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/DeleteASubjectFilterDialog.kt @@ -0,0 +1,69 @@ +package io.zoemeow.dutschedule.ui.component.settings + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import io.zoemeow.dutschedule.activity.SettingsActivity +import io.zoemeow.dutschedule.model.settings.SubjectCode +import io.zoemeow.dutschedule.ui.component.base.DialogBase + +@Composable +fun SettingsActivity.DeleteASubjectFilterDialog( + subjectCode: SubjectCode = SubjectCode("", "", ""), + isVisible: Boolean = false, + onDismiss: (() -> Unit)? = null, + onDone: (() -> Unit)? = null +) { + DialogBase( + modifier = Modifier + .fillMaxWidth() + .padding(25.dp), + title = "Delete subject filter?", + isVisible = isVisible, + canDismiss = false, + dismissClicked = { onDismiss?.let { it() } }, + content = { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = String.format( + "%s\n%s\n\n%s", + "Are you sure you want to delete this filter?", + String.format("%s [%s.Nh%s]", subjectCode.subjectName, subjectCode.studentYearId, subjectCode.classId), + "This action is undone!" + ), + modifier = Modifier.padding(bottom = 5.dp) + ) + } + }, + actionButtons = { + TextButton( + onClick = { onDismiss?.let { it() } }, + content = { Text("No, take me back") }, + modifier = Modifier.padding(start = 8.dp), + ) + ElevatedButton( + colors = ButtonDefaults.elevatedButtonColors().copy( + containerColor = Color.Red, + contentColor = Color.White + ), + onClick = { onDone?.let { it() } }, + content = { Text("Yes, delete it") }, + modifier = Modifier.padding(start = 8.dp), + ) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/DeleteAllSubjectFilterDialog.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/DeleteAllSubjectFilterDialog.kt new file mode 100644 index 0000000..bb6866b --- /dev/null +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/DeleteAllSubjectFilterDialog.kt @@ -0,0 +1,60 @@ +package io.zoemeow.dutschedule.ui.component.settings + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import io.zoemeow.dutschedule.activity.SettingsActivity +import io.zoemeow.dutschedule.ui.component.base.DialogBase + +@Composable +fun SettingsActivity.DeleteAllSubjectFilterDialog( + isVisible: Boolean = false, + onDismiss: (() -> Unit)? = null, + onDone: (() -> Unit)? = null +) { + DialogBase( + modifier = Modifier.fillMaxWidth().padding(25.dp), + title = "Delete all subject filters?", + isVisible = isVisible, + canDismiss = false, + dismissClicked = { onDismiss?.let { it() } }, + content = { + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = "Are you sure you want to delete all subject filter?\n\nThis action is undone!", + modifier = Modifier.padding(bottom = 5.dp) + ) + } + }, + actionButtons = { + TextButton( + onClick = { onDismiss?.let { it() } }, + content = { Text("No, take me back") }, + modifier = Modifier.padding(start = 8.dp), + ) + ElevatedButton( + colors = ButtonDefaults.elevatedButtonColors().copy( + containerColor = Color.Red, + contentColor = Color.White + ), + onClick = { onDone?.let { it() } }, + content = { Text("Yes, delete it") }, + modifier = Modifier.padding(start = 8.dp), + ) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterAddInNewsSubject.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterAddInNewsSubject.kt deleted file mode 100644 index 8df7837..0000000 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterAddInNewsSubject.kt +++ /dev/null @@ -1,29 +0,0 @@ -package io.zoemeow.dutschedule.ui.component.settings.newsfilter - -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import io.zoemeow.dutschedule.ui.component.base.ExpandableContent -import io.zoemeow.dutschedule.ui.component.base.ExpandableContentDefaultTitle - -@Composable -fun NewsFilterAddInNewsSubject( - modifier: Modifier = Modifier, - expanded: Boolean = false, - onExpanded: (() -> Unit)? = null, - opacity: Float = 1.0f -) { - ExpandableContent( - modifier = modifier, - opacity = opacity, - title = { - ExpandableContentDefaultTitle(title = "Add filter via news subject or subject schedule") - }, - isTitleCentered = true, - onTitleClicked = onExpanded, - content = { - Text("This will make you add filter easier than manually add option below.\n\nYou can do this by following one of these options below:\n - Navigate to your subject information, click a subject and click \"Add to news filter\". Note: You need to be logged in first.\n - Navigate to news subject, click a news subject announcement that contains your subject, and click \"Add to news filter\".") - }, - isContentVisible = expanded - ) -} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterAddManually.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterAddManually.kt deleted file mode 100644 index 5aa4523..0000000 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterAddManually.kt +++ /dev/null @@ -1,109 +0,0 @@ -package io.zoemeow.dutschedule.ui.component.settings.newsfilter - -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.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.material3.Button -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import io.zoemeow.dutschedule.ui.component.base.ExpandableContent -import io.zoemeow.dutschedule.ui.component.base.ExpandableContentDefaultTitle - -@Composable -fun NewsFilterAddManually( - modifier: Modifier = Modifier, - expanded: Boolean = false, - onExpanded: (() -> Unit)? = null, - onSubmit: ((String, String, String) -> Unit)? = null, - opacity: Float = 1.0f -) { - val studentYearId = remember { mutableStateOf("") } - val classId = remember { mutableStateOf("") } - val subjectName = remember { mutableStateOf("") } - - ExpandableContent( - modifier = modifier, - opacity = opacity, - title = { - ExpandableContentDefaultTitle(title = "Add filter manually") - }, - isTitleCentered = true, - onTitleClicked = onExpanded, - content = { - Text( - text = "Enter your subject filter (you can view templates in sv.dut.udn.vn) and tap \"Add\" to add to filter above.\n\nExample:\n - 19 | 01 | Subject A\n - xx | 94A | Subject B\n\nNote:\n- You need to enter carefully, otherwise you won\'t received notifications exactly.", - modifier = Modifier.padding(bottom = 5.dp) - ) - Column( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(bottom = 10.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - OutlinedTextField( - value = studentYearId.value, - onValueChange = { if (it.length <= 2) studentYearId.value = it }, - label = { Text("First value") }, - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .weight(0.5f) - ) - Spacer(modifier = Modifier.size(10.dp)) - OutlinedTextField( - value = classId.value, - onValueChange = { if (it.length <= 3) classId.value = it }, - label = { Text("Second value") }, - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .weight(0.5f) - ) - } - Spacer(modifier = Modifier.size(5.dp)) - OutlinedTextField( - value = subjectName.value, - onValueChange = { subjectName.value = it }, - label = { Text("Subject name") }, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.size(10.dp)) - Button( - content = { Text("Add") }, - onClick = { - if (studentYearId.value.length >= 2 && classId.value.length >= 2 && subjectName.value.length >= 3) { - onSubmit?.let { - it( - studentYearId.value, - classId.value, - subjectName.value - ) - } - } - }, - ) - } - }, - isContentVisible = expanded - ) -} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterClearAll.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterClearAll.kt deleted file mode 100644 index 07bd4b4..0000000 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterClearAll.kt +++ /dev/null @@ -1,55 +0,0 @@ -package io.zoemeow.dutschedule.ui.component.settings.newsfilter - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import io.zoemeow.dutschedule.ui.component.base.ExpandableContent -import io.zoemeow.dutschedule.ui.component.base.ExpandableContentDefaultTitle - -@Composable -fun NewsFilterClearAll( - modifier: Modifier = Modifier, - expanded: Boolean = false, - onExpanded: (() -> Unit)? = null, - onSubmit: (() -> Unit)? = null, - opacity: Float = 1.0f -) { - ExpandableContent( - modifier = modifier, - opacity = opacity, - title = { - ExpandableContentDefaultTitle(title = "Clear all filters") - }, - isTitleCentered = true, - onTitleClicked = onExpanded, - content = { - Column( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = "Just click button below to clear all.\nNote:\n- This will delete all filters you added before and cannot be undone.\n- If you want to revert, close this settings and choose UNSAVE CHANGES. This is your ONLY chance to undo your action.", - modifier = Modifier.padding(bottom = 5.dp) - ) - Button( - content = { Text("Clear all") }, - onClick = { - onSubmit?.let { it() } - } - ) - } - }, - isContentVisible = expanded - ) -} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterCurrentFilter.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterCurrentFilter.kt deleted file mode 100644 index ddce7a6..0000000 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/newsfilter/NewsFilterCurrentFilter.kt +++ /dev/null @@ -1,64 +0,0 @@ -package io.zoemeow.dutschedule.ui.component.settings.newsfilter - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.material3.InputChip -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import io.zoemeow.dutschedule.model.settings.SubjectCode -import io.zoemeow.dutschedule.ui.component.base.ExpandableContent -import io.zoemeow.dutschedule.ui.component.base.ExpandableContentDefaultTitle - -@OptIn(ExperimentalLayoutApi::class) -@Composable -fun NewsFilterCurrentFilter( - modifier: Modifier = Modifier, - selectedSubjects: List? = null, - onRemoveRequested: ((SubjectCode) -> Unit)? = null, - opacity: Float = 1.0f -) { - ExpandableContent( - modifier = modifier, - opacity = opacity, - title = { - ExpandableContentDefaultTitle(title = "Your current filter") - }, - isTitleCentered = true, - content = { - when { - selectedSubjects.isNullOrEmpty() -> { - Text("Your filter list will be here.\n\n- Look like you aren't set up your filter yet.\n- That\'s mean, all subject news will notify you.") - } - else -> { - Text( - "Your current filter list (click a item to remove):", - modifier = Modifier.padding(bottom = 10.dp) - ) - FlowRow( - modifier = Modifier.fillMaxWidth().wrapContentHeight(), - horizontalArrangement = Arrangement.Center, - verticalArrangement = Arrangement.Top - ) { - selectedSubjects.forEach { item -> - InputChip( - selected = false, - onClick = { - onRemoveRequested?.let { it(item) } - }, - label = { Text(item.toString()) }, - modifier = Modifier.padding(horizontal = 3.dp) - ) - } - } - } - } - }, - isContentVisible = true - ) -} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/AccountInformation.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/AccountInformation.kt index ede7bc7..fb9acea 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/AccountInformation.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/AccountInformation.kt @@ -17,18 +17,17 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.unit.dp import io.zoemeow.dutschedule.activity.AccountActivity import io.zoemeow.dutschedule.model.ProcessState @@ -41,19 +40,15 @@ fun AccountActivity.AccountInformation( containerColor: Color, contentColor: Color ) { - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - Scaffold( - modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), + modifier = Modifier.fillMaxSize(), snackbarHost = { SnackbarHost(hostState = snackBarHostState) }, containerColor = containerColor, contentColor = contentColor, topBar = { - LargeTopAppBar( + TopAppBar( title = { Text("Basic Information") }, - colors = TopAppBarDefaults.largeTopAppBarColors(containerColor = Color.Transparent, scrolledContainerColor = Color.Transparent), + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), navigationIcon = { IconButton( onClick = { @@ -68,8 +63,7 @@ fun AccountActivity.AccountInformation( ) } ) - }, - scrollBehavior = scrollBehavior + } ) }, floatingActionButton = { diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt index 89dc21f..66e0f13 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt @@ -22,11 +22,11 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf @@ -34,7 +34,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.unit.dp import io.zoemeow.dutschedule.activity.AccountActivity import io.zoemeow.dutschedule.model.ProcessState @@ -59,19 +58,16 @@ fun AccountActivity.MainView( val loginDialogVisible = remember { mutableStateOf(false) } val loginDialogEnabled = remember { mutableStateOf(true) } val logoutDialogVisible = remember { mutableStateOf(false) } - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() Scaffold( - modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), + modifier = Modifier.fillMaxSize(), snackbarHost = { SnackbarHost(hostState = snackBarHostState) }, containerColor = containerColor, contentColor = contentColor, topBar = { - LargeTopAppBar( + TopAppBar( title = { Text("Account") }, - colors = TopAppBarDefaults.largeTopAppBarColors(containerColor = Color.Transparent, scrolledContainerColor = Color.Transparent), + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), navigationIcon = { IconButton( onClick = { @@ -86,8 +82,7 @@ fun AccountActivity.MainView( ) } ) - }, - scrollBehavior = scrollBehavior + } ) }, content = { diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectFee.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectFee.kt index 29425bc..6d502e4 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectFee.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectFee.kt @@ -16,12 +16,12 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf @@ -29,7 +29,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.unit.dp import io.zoemeow.dutschedule.activity.AccountActivity import io.zoemeow.dutschedule.model.ProcessState @@ -42,19 +41,15 @@ fun AccountActivity.SubjectFee( containerColor: Color, contentColor: Color ) { - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - Scaffold( - modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), + modifier = Modifier.fillMaxSize(), snackbarHost = { SnackbarHost(hostState = snackBarHostState) }, containerColor = containerColor, contentColor = contentColor, topBar = { - LargeTopAppBar( + TopAppBar( title = { Text("Subject fee") }, - colors = TopAppBarDefaults.largeTopAppBarColors(containerColor = Color.Transparent, scrolledContainerColor = Color.Transparent), + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), navigationIcon = { IconButton( onClick = { @@ -69,8 +64,7 @@ fun AccountActivity.SubjectFee( ) } ) - }, - scrollBehavior = scrollBehavior + } ) }, floatingActionButton = { diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt index f4d0b74..c546c0b 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt @@ -16,12 +16,12 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState @@ -30,7 +30,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.unit.dp import io.dutwrapper.dutwrapper.model.accounts.SubjectScheduleItem import io.zoemeow.dutschedule.activity.AccountActivity @@ -47,19 +46,16 @@ fun AccountActivity.SubjectInformation( ) { val subjectScheduleItem: MutableState = remember { mutableStateOf(null) } val subjectDetailVisible = remember { mutableStateOf(false) } - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() Scaffold( - modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), + modifier = Modifier.fillMaxSize(), snackbarHost = { SnackbarHost(hostState = snackBarHostState) }, containerColor = containerColor, contentColor = contentColor, topBar = { - LargeTopAppBar( + TopAppBar( title = { Text("Subject Information") }, - colors = TopAppBarDefaults.largeTopAppBarColors(containerColor = Color.Transparent, scrolledContainerColor = Color.Transparent), + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), navigationIcon = { IconButton( onClick = { @@ -74,8 +70,7 @@ fun AccountActivity.SubjectInformation( ) } ) - }, - scrollBehavior = scrollBehavior + } ) }, floatingActionButton = { diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt index 6c07a7d..52f7188 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt @@ -20,12 +20,12 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf @@ -33,7 +33,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.unit.dp import io.zoemeow.dutschedule.activity.AccountActivity import io.zoemeow.dutschedule.model.ProcessState @@ -49,19 +48,15 @@ fun AccountActivity.TrainingResult( containerColor: Color, contentColor: Color ) { - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - Scaffold( - modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), + modifier = Modifier.fillMaxSize(), snackbarHost = { SnackbarHost(hostState = snackBarHostState) }, containerColor = containerColor, contentColor = contentColor, topBar = { - LargeTopAppBar( + TopAppBar( title = { Text("Account Training Result") }, - colors = TopAppBarDefaults.largeTopAppBarColors(containerColor = Color.Transparent, scrolledContainerColor = Color.Transparent), + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), navigationIcon = { IconButton( onClick = { @@ -76,8 +71,7 @@ fun AccountActivity.TrainingResult( ) } ) - }, - scrollBehavior = scrollBehavior + } ) }, floatingActionButton = { diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt index 2359b05..8c9d6c3 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt @@ -26,13 +26,13 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -42,7 +42,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import io.zoemeow.dutschedule.R @@ -79,7 +78,6 @@ fun MainActivity.MainViewDashboard( settingsClicked: (() -> Unit)? = null, externalLinkClicked: (() -> Unit)? = null ) { - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() val isNotificationOpened = remember { mutableStateOf(false) } val needConfirmClearAllNotifications = remember { mutableStateOf(true) } val notificationModalBottomSheetState = rememberModalBottomSheetState( @@ -114,17 +112,14 @@ fun MainActivity.MainViewDashboard( } Scaffold( - modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), + modifier = Modifier.fillMaxSize(), snackbarHost = { SnackbarHost(hostState = snackBarHostState) }, containerColor = containerColor, contentColor = contentColor, topBar = { - LargeTopAppBar( + TopAppBar( title = { Text(text = "DutSchedule") }, - colors = TopAppBarDefaults.largeTopAppBarColors(containerColor = Color.Transparent, scrolledContainerColor = Color.Transparent), - scrollBehavior = scrollBehavior + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent) ) }, bottomBar = { @@ -345,7 +340,18 @@ fun MainActivity.MainViewDashboard( visible = isNotificationOpened.value, sheetState = notificationModalBottomSheetState, onDismiss = { isNotificationOpened.value = false }, - onClearItem = { + onClick = { item -> + if (listOf(1, 2).contains(item.tag)) { + Intent(context, NewsActivity::class.java).also { + it.action = "activity_detail" + for (map1 in item.parameters) { + it.putExtra(map1.key, map1.value) + } + context.startActivity(it) + } + } + }, + onClear = { val itemTemp = it.clone() getMainViewModel().notificationHistory.remove(it) getMainViewModel().saveSettings() diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt index 97aeb11..eb9085f 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt @@ -14,18 +14,17 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.unit.dp import io.zoemeow.dutschedule.activity.SettingsActivity import io.zoemeow.dutschedule.model.settings.BackgroundImageOption @@ -42,20 +41,17 @@ fun SettingsActivity.ExperimentSettings( containerColor: Color, contentColor: Color ) { - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() val dialogSchoolYear = remember { mutableStateOf(false) } Scaffold( - modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), + modifier = Modifier.fillMaxSize(), snackbarHost = { SnackbarHost(hostState = snackBarHostState) }, containerColor = containerColor, contentColor = contentColor, topBar = { - LargeTopAppBar( + TopAppBar( title = { Text("Experiment settings") }, - colors = TopAppBarDefaults.largeTopAppBarColors(containerColor = Color.Transparent, scrolledContainerColor = Color.Transparent), + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), navigationIcon = { IconButton( onClick = { @@ -70,8 +66,7 @@ fun SettingsActivity.ExperimentSettings( ) } ) - }, - scrollBehavior = scrollBehavior + } ) }, content = { diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/LanguageSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/LanguageSettings.kt index 93df743..02abb40 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/LanguageSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/LanguageSettings.kt @@ -24,20 +24,19 @@ import androidx.compose.material.icons.filled.Check import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -54,19 +53,15 @@ fun SettingsActivity.LanguageSettings( containerColor: Color, contentColor: Color ) { - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - Scaffold( - modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), + modifier = Modifier.fillMaxSize(), snackbarHost = { SnackbarHost(hostState = snackBarHostState) }, containerColor = containerColor, contentColor = contentColor, topBar = { - LargeTopAppBar( + TopAppBar( title = { Text("App Language") }, - colors = TopAppBarDefaults.largeTopAppBarColors(containerColor = Color.Transparent, scrolledContainerColor = Color.Transparent), + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), navigationIcon = { IconButton( onClick = { @@ -82,7 +77,6 @@ fun SettingsActivity.LanguageSettings( } ) }, - scrollBehavior = scrollBehavior, actions = { IconButton( onClick = { diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt index 0dcd03d..86d7219 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt @@ -20,11 +20,11 @@ import androidx.compose.material.icons.filled.Notifications import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState @@ -33,7 +33,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import io.zoemeow.dutschedule.BuildConfig @@ -62,19 +61,15 @@ fun SettingsActivity.MainView( val dialogAppTheme: MutableState = remember { mutableStateOf(false) } val dialogBackground: MutableState = remember { mutableStateOf(false) } - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - Scaffold( - modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), + modifier = Modifier.fillMaxSize(), snackbarHost = { SnackbarHost(hostState = snackBarHostState) }, containerColor = containerColor, contentColor = contentColor, topBar = { - LargeTopAppBar( + TopAppBar( title = { Text(getString(R.string.settings_name)) }, - colors = TopAppBarDefaults.largeTopAppBarColors(containerColor = Color.Transparent, scrolledContainerColor = Color.Transparent), + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), navigationIcon = { IconButton( onClick = { @@ -89,8 +84,7 @@ fun SettingsActivity.MainView( ) } ) - }, - scrollBehavior = scrollBehavior + } ) }, content = { @@ -122,16 +116,6 @@ fun SettingsActivity.MainView( }.also { intent -> context.startActivity(intent) } } ) - OptionItem( - modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), - title = "News filter settings", - description = "Make your filter to only receive your preferred subject news.", - onClick = { - val intent = Intent(context, SettingsActivity::class.java) - intent.action = "settings_newsfilter" - context.startActivity(intent) - } - ) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { OptionItem( modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsFilterSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsFilterSettings.kt deleted file mode 100644 index 27de487..0000000 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsFilterSettings.kt +++ /dev/null @@ -1,249 +0,0 @@ -package io.zoemeow.dutschedule.ui.view.settings - -import androidx.activity.ComponentActivity -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.unit.dp -import io.zoemeow.dutschedule.R -import io.zoemeow.dutschedule.activity.SettingsActivity -import io.zoemeow.dutschedule.model.settings.SubjectCode -import io.zoemeow.dutschedule.ui.component.base.DialogBase -import io.zoemeow.dutschedule.ui.component.settings.newsfilter.NewsFilterAddInNewsSubject -import io.zoemeow.dutschedule.ui.component.settings.newsfilter.NewsFilterAddManually -import io.zoemeow.dutschedule.ui.component.settings.newsfilter.NewsFilterClearAll -import io.zoemeow.dutschedule.ui.component.settings.newsfilter.NewsFilterCurrentFilter - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun SettingsActivity.NewsFilterSettings( - snackBarHostState: SnackbarHostState, - containerColor: Color, - contentColor: Color -) { - val tempFilterList = remember { - mutableStateListOf().also { - it.addAll(getMainViewModel().appSettings.value.newsBackgroundFilterList) - } - } - val modified = remember { mutableStateOf(false) } - val exitWithoutSavingDialog = remember { mutableStateOf(false) } - - fun saveChanges(exit: Boolean = false) { - getMainViewModel().appSettings.value = getMainViewModel().appSettings.value.clone( - newsFilterList = getMainViewModel().appSettings.value.newsBackgroundFilterList.also { - it.clear() - it.addAll(tempFilterList.toList()) - } - ) - getMainViewModel().saveSettings() - modified.value = false - - if (!exit) { - showSnackBar( - text = "Saved changes!", - clearPrevious = true - ) - } else { - tempFilterList.clear() - setResult(ComponentActivity.RESULT_OK) - finish() - } - } - - fun discardChangesAndExit() { - tempFilterList.clear() - setResult(ComponentActivity.RESULT_CANCELED) - finish() - } - - Scaffold( - modifier = Modifier.fillMaxSize(), - snackbarHost = { SnackbarHost(hostState = snackBarHostState) }, - containerColor = containerColor, - contentColor = contentColor, - topBar = { - TopAppBar( - title = { Text("News filter settings") }, - colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), - navigationIcon = { - IconButton( - onClick = { - if (!modified.value) { - discardChangesAndExit() - } else { - exitWithoutSavingDialog.value = true - } - }, - content = { - Icon( - Icons.AutoMirrored.Filled.ArrowBack, - "", - modifier = Modifier.size(25.dp) - ) - } - ) - }, - actions = { - IconButton( - onClick = { - saveChanges() - }, - content = { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_baseline_save_24), - "", - modifier = Modifier.size(25.dp) - ) - } - ) - } - ) - }, - content = { - val tabIndex = remember { mutableIntStateOf(1) } - - Column( - modifier = Modifier - .padding(it) - .padding(horizontal = 7.dp) - .verticalScroll(rememberScrollState()), - content = { - NewsFilterCurrentFilter( - opacity = getControlBackgroundAlpha(), - selectedSubjects = tempFilterList, - onRemoveRequested = { subjectCode -> - tempFilterList.remove(subjectCode) - modified.value = true - showSnackBar( - text = "Removed $subjectCode. Save changes to apply your settings.", - clearPrevious = true - ) - } - ) - NewsFilterAddInNewsSubject( - opacity = getControlBackgroundAlpha(), - expanded = tabIndex.intValue == 1, - onExpanded = { tabIndex.intValue = 1 } - ) - NewsFilterAddManually( - opacity = getControlBackgroundAlpha(), - expanded = tabIndex.intValue == 2, - onExpanded = { tabIndex.intValue = 2 }, - onSubmit = { schoolYearItem, classItem, subjectName -> - tempFilterList.add( - SubjectCode( - studentYearId = schoolYearItem, - classId = classItem, - subjectName = subjectName - ) - ) - modified.value = true - showSnackBar( - text = "Added ${schoolYearItem}.${classItem}. Save changes to apply your settings.", - clearPrevious = true - ) - } - ) - NewsFilterClearAll( - opacity = getControlBackgroundAlpha(), - expanded = tabIndex.intValue == 3, - onExpanded = { tabIndex.intValue = 3 }, - onSubmit = { - if (tempFilterList.isNotEmpty()) { - tempFilterList.clear() - modified.value = true - showSnackBar( - text = "Cleared! Remember to save changes to apply your settings.", - clearPrevious = true - ) - } else { - showSnackBar( - text = "Nothing to clear!", - clearPrevious = true - ) - } - } - ) - } - ) - } - ) - DialogBase( - modifier = Modifier - .fillMaxWidth() - .padding(25.dp), - canDismiss = false, - isTitleCentered = true, - title = "Exit without saving?", - isVisible = exitWithoutSavingDialog.value, - content = { - Column( - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.Top, - modifier = Modifier.fillMaxWidth(), - ) { - Text("You have modified changes. Save them now?\n\n- Yes: Save changes and exit\n- No: Discard changes and exit\n- Cancel: Just close this dialog.") - } - }, - actionButtons = { - TextButton( - onClick = { - exitWithoutSavingDialog.value = false - saveChanges(exit = true) - }, - content = { Text("Yes") }, - modifier = Modifier.padding(start = 8.dp), - ) - TextButton( - onClick = { - exitWithoutSavingDialog.value = false - discardChangesAndExit() - }, - content = { Text("No") }, - modifier = Modifier.padding(start = 8.dp), - ) - TextButton( - onClick = { - exitWithoutSavingDialog.value = false - }, - content = { Text("Cancel") }, - modifier = Modifier.padding(start = 8.dp), - ) - } - ) - BackHandler( - enabled = modified.value, - onBack = { - exitWithoutSavingDialog.value = true - } - ) -} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt index 18e10b6..a10a5ee 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt @@ -3,6 +3,7 @@ package io.zoemeow.dutschedule.ui.view.settings import android.content.Context import android.content.Intent import androidx.activity.ComponentActivity +import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi @@ -37,7 +38,9 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -53,7 +56,10 @@ import io.zoemeow.dutschedule.ui.component.base.OptionItem import io.zoemeow.dutschedule.ui.component.base.RadioButtonOption import io.zoemeow.dutschedule.ui.component.base.SimpleCardItem import io.zoemeow.dutschedule.ui.component.base.SwitchWithTextInSurface +import io.zoemeow.dutschedule.ui.component.settings.AddNewSubjectFilterDialog import io.zoemeow.dutschedule.ui.component.settings.ContentRegion +import io.zoemeow.dutschedule.ui.component.settings.DeleteASubjectFilterDialog +import io.zoemeow.dutschedule.ui.component.settings.DeleteAllSubjectFilterDialog @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -64,6 +70,10 @@ fun SettingsActivity.NewsNotificationSettings( contentColor: Color ) { // val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + val dialogAddNew = remember { mutableStateOf(false) } + val tempDeleteItem: MutableState = remember { mutableStateOf(SubjectCode("","","")) } + val dialogDeleteItem = remember { mutableStateOf(false) } + val dialogDeleteAll = remember { mutableStateOf(false) } Scaffold( modifier = Modifier.fillMaxSize(), @@ -189,16 +199,88 @@ fun SettingsActivity.NewsNotificationSettings( subjectFilterList = getMainViewModel().appSettings.value.newsBackgroundFilterList, onSubjectFilterAdd = { // TODO: Add a filter + dialogAddNew.value = true }, - onSubjectFilterDelete = { _ -> + onSubjectFilterDelete = { data -> // TODO: Delete a filter + tempDeleteItem.value = data + dialogDeleteItem.value = true }, onSubjectFilterClear = { // TODO: Delete all filters + dialogDeleteAll.value = true }, opacity = getControlBackgroundAlpha() ) } + AddNewSubjectFilterDialog( + isVisible = dialogAddNew.value, + onDismiss = { dialogAddNew.value = false }, + onDone = { syId, cId, subName -> + // TODO: Add item manually + try { + val item = SubjectCode(syId, cId, subName) + getMainViewModel().appSettings.value.newsBackgroundFilterList.add(item) + getMainViewModel().saveSettings(saveSettingsOnly = true) + showSnackBar( + String.format("Successfully added %s [%s.Nh%s]", subName, syId, subName), + clearPrevious = true + ) + } catch (_: Exception) { } + + dialogAddNew.value = false + } + ) + DeleteASubjectFilterDialog( + subjectCode = tempDeleteItem.value, + isVisible = dialogDeleteItem.value, + onDismiss = { dialogDeleteItem.value = false }, + onDone = { + // TODO: Clear item on tempDeleteItem.value + try { + getMainViewModel().appSettings.value.newsBackgroundFilterList.remove(tempDeleteItem.value) + getMainViewModel().saveSettings(saveSettingsOnly = true) + showSnackBar( + String.format( + "Successfully deleted %s [%s.Nh%s]", + tempDeleteItem.value.subjectName, + tempDeleteItem.value.studentYearId, + tempDeleteItem.value.classId + ), + clearPrevious = true + ) + } catch (_: Exception) { } + + dialogDeleteItem.value = false + } + ) + DeleteAllSubjectFilterDialog( + isVisible = dialogDeleteAll.value, + onDismiss = { dialogDeleteAll.value = false }, + onDone = { + // TODO: Clear all items + try { + getMainViewModel().appSettings.value.newsBackgroundFilterList.clear() + getMainViewModel().saveSettings(saveSettingsOnly = true) + showSnackBar( + "Successfully cleared all filters!", + clearPrevious = true + ) + } catch (_: Exception) { } + dialogDeleteAll.value = false + } + ) + BackHandler(dialogAddNew.value || dialogDeleteItem.value || dialogDeleteAll.value) { + if (dialogAddNew.value) { + dialogAddNew.value = false + } + if (dialogDeleteItem.value) { + dialogDeleteItem.value = false + } + if (dialogDeleteAll.value) { + dialogDeleteAll.value = false + } + } } @OptIn(ExperimentalLayoutApi::class) @@ -454,7 +536,7 @@ private fun MainView( OptionItem( modifier = Modifier.padding(vertical = 3.dp), modifierInside = Modifier, - title = "${code.subjectName} - ${code.subjectName}.Nh${code.classId}", + title = "${code.subjectName} [${code.studentYearId}.Nh${code.classId}]", onClick = { }, trailingIcon = { IconButton( diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ParseNewsSubjectNotification.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ParseNewsSubjectNotification.kt index 5153c5f..9e3eca5 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ParseNewsSubjectNotification.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ParseNewsSubjectNotification.kt @@ -16,20 +16,19 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import io.zoemeow.dutschedule.R @@ -43,19 +42,15 @@ fun SettingsActivity.ParseNewsSubjectNotification( containerColor: Color, contentColor: Color ) { - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - Scaffold( - modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), + modifier = Modifier.fillMaxSize(), snackbarHost = { SnackbarHost(hostState = snackBarHostState) }, containerColor = containerColor, contentColor = contentColor, topBar = { - LargeTopAppBar( + TopAppBar( title = { Text("New parse method on notification") }, - colors = TopAppBarDefaults.largeTopAppBarColors(containerColor = Color.Transparent, scrolledContainerColor = Color.Transparent), + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), navigationIcon = { IconButton( onClick = { @@ -70,8 +65,7 @@ fun SettingsActivity.ParseNewsSubjectNotification( ) } ) - }, - scrollBehavior = scrollBehavior + } ) }, content = { From 03eda63ccee30e6014c3683ab1889578e5fdf16a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BA=BF=20T=C3=B9ng?= <47247560+ZoeMeow1027@users.noreply.github.com> Date: Sun, 24 Mar 2024 01:50:12 +0700 Subject: [PATCH 06/21] Update project - Updated gradle to 8.6. - Fixed logout isn't working correctly. - New login view and instruction on dut fanpage if forgot password. - Reduce multi requests if you access some functions with same request. - Custom notification (instead of ModalBottomSheet). --- .../dutschedule/model/DUTAccountSession.kt | 24 +- .../ui/component/account/LoginBox.kt | 240 ++++++++++++++++++ .../ui/component/account/LoginDialog.kt | 165 ------------ .../ui/component/account/LogoutDialog.kt | 8 +- .../notification/NotificationDialogBox.kt | 156 ++++++++++++ .../NotificationHistoryBottomSheet.kt | 170 ------------- .../dutschedule/ui/view/account/MainView.kt | 174 +++++-------- .../ui/view/main/MainViewDashboard.kt | 58 ++--- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 10 files changed, 503 insertions(+), 496 deletions(-) create mode 100644 app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LoginBox.kt delete mode 100644 app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LoginDialog.kt create mode 100644 app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationDialogBox.kt delete mode 100644 app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationHistoryBottomSheet.kt diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/DUTAccountSession.kt b/app/src/main/java/io/zoemeow/dutschedule/model/DUTAccountSession.kt index 507b21f..c001543 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/DUTAccountSession.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/DUTAccountSession.kt @@ -110,6 +110,7 @@ class DUTAccountSession( onSessionChanged = { sId, dateUnix -> if (dateUnix == null || dateUnix == 0L || sId == null) { // TODO: Account session isn't valid! + accountSession.data.value = null throw Exception() } else { accountSession.data.value = accountSession.data.value!!.clone( @@ -231,24 +232,27 @@ class DUTAccountSession( launchOnScope( script = { // TODO: Fully logout from server - CoroutineScope(Dispatchers.Main).launch { - withContext(Dispatchers.IO) { - accountSession.data.value?.let { - val temp = it.clone() - dutRequestRepository.logout(temp) - } - } + var temp = AccountSession() + accountSession.processState.value = ProcessState.Successful + accountSession.data.value?.let { + temp = it.clone() } accountSession.resetValue() subjectSchedule.resetValue() subjectFee.resetValue() accountInformation.resetValue() accountTrainingStatus.resetValue() + + CoroutineScope(Dispatchers.Main).launch { + withContext(Dispatchers.IO) { + dutRequestRepository.logout(temp) + } + } }, onCompleted = { throwable -> accountSession.processState.value = ProcessState.NotRunYet - onCompleted?.let { it(throwable != null) } onEventSent?.let { it(1) } + onCompleted?.let { it(throwable != null) } } ) } @@ -290,6 +294,7 @@ class DUTAccountSession( (it != null) -> ProcessState.Failed else -> ProcessState.Successful } + subjectSchedule.lastRequest.longValue = System.currentTimeMillis() onEventSent?.let { it(2) } } ) @@ -332,6 +337,7 @@ class DUTAccountSession( (it != null) -> ProcessState.Failed else -> ProcessState.Successful } + subjectFee.lastRequest.longValue = System.currentTimeMillis() onEventSent?.let { it(3) } } ) @@ -367,6 +373,7 @@ class DUTAccountSession( (it != null) -> ProcessState.Failed else -> ProcessState.Successful } + accountInformation.lastRequest.longValue = System.currentTimeMillis() onEventSent?.let { it(4) } } ) @@ -402,6 +409,7 @@ class DUTAccountSession( (it != null) -> ProcessState.Failed else -> ProcessState.Successful } + accountTrainingStatus.lastRequest.longValue = System.currentTimeMillis() onEventSent?.let { it(5) } } ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LoginBox.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LoginBox.kt new file mode 100644 index 0000000..29a68d2 --- /dev/null +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LoginBox.kt @@ -0,0 +1,240 @@ +package io.zoemeow.dutschedule.ui.component.account + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.ClickableText +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.zoemeow.dutschedule.R + +@Composable +fun LoginBox( + modifier: Modifier = Modifier, + isVisible: Boolean = true, + isProcessing: Boolean = false, + isControlEnabled: Boolean = false, + isLoggedInBefore: Boolean = true, + clearOnInvisible: Boolean = true, + opacity: Float = 1f, + onForgotPass: (() -> Unit)? = null, + onClearLogin: (() -> Unit)? = null, + onSubmit: (String, String, Boolean) -> Unit +) { + val passTextFieldFocusRequester = remember { FocusRequester() } + val passwordShow: MutableState = remember { mutableStateOf(false) } + + val username: MutableState = remember { mutableStateOf("") } + val password: MutableState = remember { mutableStateOf("") } + val rememberLogin: MutableState = remember { mutableStateOf(false) } + + LaunchedEffect(isVisible) { + if (!isVisible && clearOnInvisible) { + username.value = "" + password.value = "" + rememberLogin.value = false + passwordShow.value = false + } + } + + if (isVisible) { + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + content = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + "Login", + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(bottom = 5.dp) + ) + Text( + "Use your account in sv.dut.udn.vn to login", + style = MaterialTheme.typography.bodyLarge + ) + Spacer(modifier = Modifier.size(8.dp)) + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 7.dp), + enabled = isControlEnabled && !isProcessing, + value = username.value, + onValueChange = { username.value = it }, + label = { Text("Username") }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Next + ), + keyboardActions = KeyboardActions( + onNext = { passTextFieldFocusRequester.requestFocus() } + ), + ) + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .focusRequester(passTextFieldFocusRequester) + .padding(bottom = 7.dp), + enabled = isControlEnabled && !isProcessing, + value = password.value, + onValueChange = { password.value = it }, + label = { Text("Password") }, + suffix = { + IconButton( + onClick = { passwordShow.value = !passwordShow.value }, + content = { + Icon( + painter = painterResource(id = when (passwordShow.value) { + false -> R.drawable.ic_baseline_visibility_24 + true -> R.drawable.ic_baseline_visibility_off_24 + }), + contentDescription = "" + ) + }, + modifier = Modifier.size(16.dp) + ) + }, + visualTransformation = when (passwordShow.value) { + false -> PasswordVisualTransformation() + true -> VisualTransformation.None + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Go + ), + keyboardActions = KeyboardActions( + onGo = { + if (isControlEnabled && !isProcessing) { + onSubmit(username.value, password.value, rememberLogin.value) + } + } + ) + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + modifier = Modifier + .fillMaxWidth() + .clickable { + if (isControlEnabled && !isProcessing) { + rememberLogin.value = !rememberLogin.value + } + } + .padding(bottom = 7.dp), + ) { + Checkbox( + enabled = isControlEnabled && !isProcessing, + checked = rememberLogin.value, + onCheckedChange = { if (isControlEnabled) rememberLogin.value = it }, + ) + Spacer(modifier = Modifier.size(5.dp)) + Text("Remember this login") + } + ElevatedButton( + enabled = when { + (isControlEnabled || isLoggedInBefore) && !isProcessing -> { + username.value.length >= 6 && password.value.length >= 6 + } + else -> false + }, + onClick = { + onSubmit(username.value, password.value, rememberLogin.value) + }, + content = { + Text("Login") + } + ) + if (isLoggedInBefore) { + ElevatedButton( + enabled = !isProcessing, + onClick = { onClearLogin?.let { it() } }, + content = { Text("Login with another account") } + ) + } + TextButton( + enabled = isControlEnabled && !isProcessing, + onClick = { onForgotPass?.let { it() } }, + content = { Text("Forgot your password?") } + ) + if (isProcessing || isLoggedInBefore) { + Surface( + modifier = Modifier.padding(top = 10.dp), + color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = opacity), + shape = RoundedCornerShape(5.dp), + content = { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 15.dp, horizontal = 15.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + content = { + if (isProcessing) { + CircularProgressIndicator() + Spacer(modifier = Modifier.size(10.dp)) + Text("Processing...") + } else if (isLoggedInBefore) { + Text("You have logged in before. Click \"Login\" button to proceed.") + } + } + ) + } + ) + } + } + } + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun LoginBoxPreview() { + LoginBox( + isProcessing = false, + isControlEnabled = true, + isLoggedInBefore = false, + onSubmit = { username, password, rememberLogin -> + + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LoginDialog.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LoginDialog.kt deleted file mode 100644 index de92523..0000000 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LoginDialog.kt +++ /dev/null @@ -1,165 +0,0 @@ -package io.zoemeow.dutschedule.ui.component.account - -import androidx.compose.foundation.clickable -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.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Checkbox -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.unit.dp -import io.zoemeow.dutschedule.R -import io.zoemeow.dutschedule.ui.component.base.DialogBase - -@Composable -fun LoginDialog( - loginClicked: (String, String, Boolean) -> Unit, - cancelRequested: () -> Unit, - canDismiss: Boolean = false, - dismissClicked: (() -> Unit)? = null, - isVisible: Boolean = false, - controlEnabled: Boolean = true, - clearOnClose: Boolean = true -) { - val passTextFieldFocusRequester = remember { FocusRequester() } - val passwordShow: MutableState = remember { mutableStateOf(false) } - - val username: MutableState = remember { mutableStateOf("") } - val password: MutableState = remember { mutableStateOf("") } - val rememberLogin: MutableState = remember { mutableStateOf(false) } - - LaunchedEffect(isVisible) { - if (!isVisible && clearOnClose) { - username.value = "" - password.value = "" - rememberLogin.value = false - passwordShow.value = false - } - } - - DialogBase( - modifier = Modifier - .fillMaxWidth() - .padding(25.dp), - isVisible = isVisible, - title = "Login", - isTitleCentered = true, - canDismiss = canDismiss, - dismissClicked = { - dismissClicked?.let { it() } - }, - content = { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - ) { - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 7.dp), - enabled = controlEnabled, - value = username.value, - onValueChange = { username.value = it }, - label = { Text("Username") }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Text, - imeAction = ImeAction.Next - ), - keyboardActions = KeyboardActions( - onNext = { passTextFieldFocusRequester.requestFocus() } - ), - ) - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .focusRequester(passTextFieldFocusRequester) - .padding(bottom = 7.dp), - enabled = controlEnabled, - value = password.value, - onValueChange = { password.value = it }, - label = { Text("Password") }, - suffix = { - IconButton( - onClick = { passwordShow.value = !passwordShow.value }, - content = { - Icon( - painter = painterResource(id = when (passwordShow.value) { - false -> R.drawable.ic_baseline_visibility_24 - true -> R.drawable.ic_baseline_visibility_off_24 - }), - contentDescription = "" - ) - }, - modifier = Modifier.size(16.dp) - ) - }, - visualTransformation = when (passwordShow.value) { - false -> PasswordVisualTransformation() - true -> VisualTransformation.None - }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Password, - imeAction = ImeAction.Go - ), - keyboardActions = KeyboardActions( - onGo = { - loginClicked(username.value, password.value, rememberLogin.value) - } - ) - ) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Start, - modifier = Modifier - .fillMaxWidth() - .clickable { - if (controlEnabled) { - rememberLogin.value = !rememberLogin.value - } - } - .padding(bottom = 7.dp), - ) { - Checkbox( - checked = rememberLogin.value, - onCheckedChange = { if (controlEnabled) rememberLogin.value = it }, - ) - Spacer(modifier = Modifier.size(5.dp)) - Text("Remember this login") - } - } - }, - actionButtons = { - TextButton( - onClick = { loginClicked(username.value, password.value, rememberLogin.value) }, - content = { Text("Login") }, - ) - TextButton( - onClick = { cancelRequested() }, - content = { Text("Cancel") }, - modifier = Modifier.padding(start = 8.dp), - ) - } - ) -} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LogoutDialog.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LogoutDialog.kt index 8cd94c4..bd1dd30 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LogoutDialog.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LogoutDialog.kt @@ -39,15 +39,15 @@ fun LogoutDialog( } }, actionButtons = { - TextButton( - onClick = { logoutClicked?.let { it() } }, - content = { Text("Logout") }, - ) TextButton( onClick = { dismissClicked?.let { it() } }, content = { Text("Cancel") }, modifier = Modifier.padding(start = 8.dp), ) + TextButton( + onClick = { logoutClicked?.let { it() } }, + content = { Text("Logout") }, + ) } ) } \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationDialogBox.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationDialogBox.kt new file mode 100644 index 0000000..ef752bd --- /dev/null +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationDialogBox.kt @@ -0,0 +1,156 @@ +package io.zoemeow.dutschedule.ui.component.main.notification + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import io.zoemeow.dutschedule.R +import io.zoemeow.dutschedule.model.NotificationHistory +import io.zoemeow.dutschedule.utils.CustomDateUtil + +@Composable +fun NotificationDialogBox( + modifier: Modifier = Modifier, + isVisible: Boolean = false, + itemList: List, + snackbarHost: (@Composable () -> Unit)? = null, + onDismiss: (() -> Unit)? = null, + onClick: ((NotificationHistory) -> Unit)? = null, + onClear: ((NotificationHistory) -> Unit)? = null, + onClearAll: (() -> Unit)? = null, + height: Float = 0.7f, + opacity: Float = 1f +) { + AnimatedVisibility( + visible = isVisible, + enter = slideInVertically( + initialOffsetY = { + it / 2 + }, + ), + exit = slideOutVertically( + targetOffsetY = { + it + }, + ), + content = { + Column( + modifier = modifier.fillMaxSize(), + verticalArrangement = Arrangement.Bottom, + content = { + Surface( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(height), + color = MaterialTheme.colorScheme.surface.copy(alpha = opacity), + content = { + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(height) + .padding(top = 5.dp) + .padding(horizontal = 15.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 3.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + "Notifications", + style = MaterialTheme.typography.headlineSmall, + ) + Row { + if (itemList.isNotEmpty()) { + IconButton( + onClick = { onClearAll?.let { it() } }, + content = { + Icon(ImageVector.vectorResource(id = R.drawable.ic_baseline_clear_all_24), "") + } + ) + Spacer(modifier = Modifier.size(3.dp)) + } + IconButton( + onClick = { onDismiss?.let { it() } }, + content = { + Icon(Icons.Default.Clear, "Close") + } + ) + } + } + Column( + modifier = Modifier.verticalScroll(rememberScrollState()), + ) { + if (itemList.isEmpty()) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(top = 15.dp), + contentAlignment = Alignment.Center + ) { + Text("No notifications") + } + } else { + itemList.groupBy { p -> p.timestamp } + .toSortedMap(compareByDescending { it }) + .forEach(action = { group -> + Text( + CustomDateUtil.unixToDuration(group.key), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(top = 5.dp, bottom = 4.dp) + ) + group.value.forEach { item -> + NotificationItem( + modifier = Modifier.padding(bottom = 5.dp), + opacity = opacity, + onClick = { onClick?.let { it(item) } }, + onClear = { onClear?.let { it(item) } }, + item = item + ) + } + }) + } + Spacer(modifier = Modifier.size(9.dp)) + } + } + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(height), + verticalArrangement = Arrangement.Bottom + ) { + snackbarHost?.let { it() } + } + } + ) + } + ) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationHistoryBottomSheet.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationHistoryBottomSheet.kt deleted file mode 100644 index 691c7e6..0000000 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationHistoryBottomSheet.kt +++ /dev/null @@ -1,170 +0,0 @@ -package io.zoemeow.dutschedule.ui.component.main.notification - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.SheetState -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import io.zoemeow.dutschedule.R -import io.zoemeow.dutschedule.activity.MainActivity -import io.zoemeow.dutschedule.model.NotificationHistory -import io.zoemeow.dutschedule.utils.CustomDateUtil -import io.zoemeow.dutschedule.utils.getRandomString - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun MainActivity.NotificationHistoryBottomSheet( - snackbarHost: (@Composable () -> Unit)? = null, - visible: Boolean = false, - sheetState: SheetState, - itemList: List = listOf(), - onDismiss: () -> Unit, - onClick: ((NotificationHistory) -> Unit)? = null, - onClear: ((NotificationHistory) -> Unit)? = null, - onClearAll: (() -> Unit)? = null, - opacity: Float = 1f -) { - if (visible) { - ModalBottomSheet( - onDismissRequest = onDismiss, - sheetState = sheetState, - ) { - MainView( - snackbarHost = snackbarHost, - itemList = itemList, - opacity = opacity, - onClick = onClick, - onClear = onClear, - onClearAll = onClearAll - ) - } - } -} - -@Composable -private fun MainView( - itemList: List, - snackbarHost: (@Composable () -> Unit)? = null, - onClick: ((NotificationHistory) -> Unit)? = null, - onClear: ((NotificationHistory) -> Unit)? = null, - onClearAll: (() -> Unit)? = null, - opacity: Float = 1f -) { - Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(0.7f) - .padding(horizontal = 15.dp) - .verticalScroll(rememberScrollState()), - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 3.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - "Notifications", - style = MaterialTheme.typography.headlineSmall, - ) - if (itemList.isNotEmpty()) { - IconButton( - onClick = { - onClearAll?.let { it() } - }, - content = { - Icon(ImageVector.vectorResource(id = R.drawable.ic_baseline_clear_all_24), "") - } - ) - } - } - if (itemList.isEmpty()) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(top = 15.dp), - contentAlignment = Alignment.Center - ) { - Text("No notifications") - } - } else { - itemList.groupBy { p -> p.timestamp } - .toSortedMap(compareByDescending { it }) - .forEach(action = { group -> - Text( - CustomDateUtil.unixToDuration(group.key), - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(top = 5.dp, bottom = 4.dp) - ) - group.value.forEach { item -> - NotificationItem( - modifier = Modifier.padding(bottom = 5.dp), - opacity = opacity, - onClick = { onClick?.let { it(item) } }, - onClear = { onClear?.let { it(item) } }, - item = item - ) - } - }) - } - Spacer(modifier = Modifier.size(9.dp)) - } - Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(0.7f), - verticalArrangement = Arrangement.Bottom - ) { - snackbarHost?.let { it() } - } -} - -@Preview() -@Composable -private fun Preview() { - MainView( - opacity = 0.7f, - itemList = listOf( - NotificationHistory( - id = getRandomString(32), - title = "Thầy ___ thông báo đến lớp: Phương pháp luận nghiên cứu khoa học [20.Nh29]", - description = "Chiều mai (thứ sáu, 23/2) thầy Hùng bận việc từ 16.00 nên ngày mai ta nghỉ tiết 9-10 (HP PPNCKH). Ta còn nhiều tuần để bù (báo các em biết).", - tag = 1, - timestamp = 1708534800000, - parameters = mapOf(), - isRead = false - ), - NotificationHistory( - id = getRandomString(32), - title = "News global", - description = "V/v Xét giao Đồ án tốt nghiệp học kỳ 2/23-24", - tag = 1, - timestamp = 1708534800000, - parameters = mapOf(), - isRead = false - ) - ) - ) -} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt index 66e0f13..4977f31 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt @@ -7,18 +7,14 @@ import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -31,7 +27,6 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp @@ -39,8 +34,7 @@ import io.zoemeow.dutschedule.activity.AccountActivity import io.zoemeow.dutschedule.model.ProcessState import io.zoemeow.dutschedule.model.account.AccountAuth import io.zoemeow.dutschedule.ui.component.account.AccountInfoBanner -import io.zoemeow.dutschedule.ui.component.account.LoginBannerNotLoggedIn -import io.zoemeow.dutschedule.ui.component.account.LoginDialog +import io.zoemeow.dutschedule.ui.component.account.LoginBox import io.zoemeow.dutschedule.ui.component.account.LogoutDialog import io.zoemeow.dutschedule.ui.component.base.ButtonBase import kotlinx.coroutines.CoroutineScope @@ -86,60 +80,71 @@ fun AccountActivity.MainView( ) }, content = { - Column( - modifier = Modifier - .fillMaxSize() - .padding(it) - .verticalScroll(rememberScrollState()), - content = { - when (getMainViewModel().accountSession.accountSession.processState.value) { - ProcessState.NotRunYet -> { - LoginBannerNotLoggedIn( - opacity = getControlBackgroundAlpha(), - padding = PaddingValues(10.dp), - clicked = { - loginDialogVisible.value = true - }, - ) - } - ProcessState.Failed -> { - ButtonBase( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 10.dp, vertical = 5.dp), - modifierInside = Modifier.padding(vertical = 7.dp), - content = { Text("Try to login again") }, - horizontalArrangement = Arrangement.Start, - opacity = getControlBackgroundAlpha(), - clicked = { - getMainViewModel().accountSession.reLogin() - } - ) - ButtonBase( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 10.dp, vertical = 5.dp), - modifierInside = Modifier.padding(vertical = 7.dp), - content = { Text("Logout") }, - horizontalArrangement = Arrangement.Start, - opacity = getControlBackgroundAlpha(), - clicked = { - logoutDialogVisible.value = true - } - ) - } - ProcessState.Running -> { - Row( - modifier = Modifier.fillMaxWidth().height(120.dp).padding(10.dp), - verticalAlignment = Alignment.CenterVertically, - content = { - CircularProgressIndicator() - Spacer(modifier = Modifier.size(5.dp)) - Text("Logging in...") + getMainViewModel().accountSession.accountSession.processState.value.let { state -> + LoginBox( + modifier = Modifier + .padding(it) + .padding(horizontal = 15.dp), + isVisible = state != ProcessState.Successful, + isProcessing = state == ProcessState.Running, + isControlEnabled = state != ProcessState.Running, + isLoggedInBefore = state == ProcessState.Failed, + clearOnInvisible = true, + opacity = getControlBackgroundAlpha(), + onForgotPass = { + openLink( + url = "https://www.facebook.com/ctsvdhbkdhdn/posts/pfbid02G5sza1p8x7tEJ7S1Cac6a66EW3exgxLNmR9L26RZ8sX8xjhbEnguoeAXms31i7oxl", + context = context, + customTab = getMainViewModel().appSettings.value.openLinkInsideApp + ) + }, + onClearLogin = { }, + onSubmit = { username, password, rememberLogin -> + run { + CoroutineScope(Dispatchers.IO).launch { + loginDialogEnabled.value = false + showSnackBar( + text = "Logging you in...", + clearPrevious = true, + ) + } + getMainViewModel().accountSession.login( + accountAuth = AccountAuth( + username = username, + password = password, + rememberLogin = rememberLogin + ), + onCompleted = {loggedIn -> + when (loggedIn) { + true -> { + loginDialogEnabled.value = true + loginDialogVisible.value = false + getMainViewModel().accountSession.reLogin() + showSnackBar( + text = "Successfully logged in!", + clearPrevious = true, + ) + } + false -> { + loginDialogEnabled.value = true + showSnackBar( + text = "Login failed! Please check your login information and try again.", + clearPrevious = true, + ) + } + } } ) } - ProcessState.Successful -> { + } + ) + if (state == ProcessState.Successful) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(it) + .verticalScroll(rememberScrollState()), + content = { getMainViewModel().accountSession.accountInformation.let { accInfo -> AccountInfoBanner( opacity = getControlBackgroundAlpha(), @@ -219,59 +224,8 @@ fun AccountActivity.MainView( } ) } - } - } - ) - } - ) - LoginDialog( - isVisible = loginDialogVisible.value, - controlEnabled = loginDialogEnabled.value, - loginClicked = { username, password, rememberLogin -> - run { - CoroutineScope(Dispatchers.IO).launch { - loginDialogEnabled.value = false - showSnackBar( - text = "Logging you in...", - clearPrevious = true, ) } - getMainViewModel().accountSession.login( - accountAuth = AccountAuth( - username = username, - password = password, - rememberLogin = rememberLogin - ), - onCompleted = { - when (it) { - true -> { - loginDialogEnabled.value = true - loginDialogVisible.value = false - getMainViewModel().accountSession.reLogin() - showSnackBar( - text = "Successfully logged in!", - clearPrevious = true, - ) - } - false -> { - loginDialogEnabled.value = true - showSnackBar( - text = "Login failed! Please check your login information and try again.", - clearPrevious = true, - ) - } - } - } - ) - } - }, - cancelRequested = { - loginDialogVisible.value = false - }, - canDismiss = false, - dismissClicked = { - if (loginDialogEnabled.value) { - loginDialogVisible.value = false } } ) @@ -280,7 +234,7 @@ fun AccountActivity.MainView( canDismiss = true, logoutClicked = { run { - loginDialogVisible.value = false + logoutDialogVisible.value = false getMainViewModel().accountSession.logout( onCompleted = { showSnackBar( diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt index 8c9d6c3..74d7abf 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt @@ -34,11 +34,9 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -55,9 +53,8 @@ import io.zoemeow.dutschedule.ui.component.main.DateAndTimeSummaryItem import io.zoemeow.dutschedule.ui.component.main.LessonTodaySummaryItem import io.zoemeow.dutschedule.ui.component.main.SchoolNewsSummaryItem import io.zoemeow.dutschedule.ui.component.main.UpdateAvailableSummaryItem -import io.zoemeow.dutschedule.ui.component.main.notification.NotificationHistoryBottomSheet +import io.zoemeow.dutschedule.ui.component.main.notification.NotificationDialogBox import io.zoemeow.dutschedule.utils.CustomDateUtil -import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalTime @@ -79,11 +76,6 @@ fun MainActivity.MainViewDashboard( externalLinkClicked: (() -> Unit)? = null ) { val isNotificationOpened = remember { mutableStateOf(false) } - val needConfirmClearAllNotifications = remember { mutableStateOf(true) } - val notificationModalBottomSheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true - ) - val notificationSheetScope = rememberCoroutineScope() fun getNews(byWeek: Boolean = false): Int { var data = 0 @@ -188,11 +180,8 @@ fun MainActivity.MainViewDashboard( onClick = { // Open notification bottom sheet // Notification list requested - notificationSheetScope.launch { - if (!isNotificationOpened.value) { - isNotificationOpened.value = true - } - notificationModalBottomSheetState.expand() + if (!isNotificationOpened.value) { + isNotificationOpened.value = true } } ) { @@ -334,11 +323,10 @@ fun MainActivity.MainViewDashboard( ) } ) - NotificationHistoryBottomSheet( - snackbarHost = { SnackbarHost(hostState = snackBarHostState) }, + NotificationDialogBox( + modifier = Modifier.padding(padding), itemList = getMainViewModel().notificationHistory, - visible = isNotificationOpened.value, - sheetState = notificationModalBottomSheetState, + isVisible = isNotificationOpened.value, onDismiss = { isNotificationOpened.value = false }, onClick = { item -> if (listOf(1, 2).contains(item.tag)) { @@ -365,25 +353,21 @@ fun MainActivity.MainViewDashboard( ) }, onClearAll = { - if (needConfirmClearAllNotifications.value) { - needConfirmClearAllNotifications.value = false - showSnackBar( - text = "This action is undone! To confirm, click \"Clear all\" icon again.", - onDismiss = { - needConfirmClearAllNotifications.value = true - }, - clearPrevious = true - ) - } else { - needConfirmClearAllNotifications.value = true - getMainViewModel().notificationHistory.clear() - getMainViewModel().saveSettings() - showSnackBar( - text = "Successfully cleared all notifications!", - clearPrevious = true - ) - } - } + showSnackBar( + text = "This action is undone! To confirm, click \"Confirm\" to clear all.", + actionText = "Confirm", + action = { + getMainViewModel().notificationHistory.clear() + getMainViewModel().saveSettings() + showSnackBar( + text = "Successfully cleared all notifications!", + clearPrevious = true + ) + }, + clearPrevious = true + ) + }, + height = 1f ) } ) diff --git a/build.gradle b/build.gradle index dd42872..8b559fb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '8.3.0' apply false + id 'com.android.application' version '8.3.1' apply false id 'org.jetbrains.kotlin.android' version '1.8.10' apply false id 'com.google.dagger.hilt.android' version '2.44' apply false } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index b269193..913292d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Wed Oct 04 23:40:00 ICT 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From c1ebb7dcccc267fbd2a03cf1ed9923d16edcbff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BA=BF=20T=C3=B9ng?= <47247560+ZoeMeow1027@users.noreply.github.com> Date: Sun, 24 Mar 2024 13:26:34 +0700 Subject: [PATCH 07/21] Update project - Updated gradle to 8.7. - Updated dependencies to latest version. - Changed icon to svg format (thanks to Adobe). - DUTAccountSession is changed to DUTAccountInstance. - Moved all news variable in MainViewModel to DUTNewsInstnace. - Optimize news fetch. --- .idea/icon.svg | 125 ++++ app/build.gradle | 24 +- app/src/main/ic_launcher-playstore.png | Bin 101753 -> 46363 bytes .../io/zoemeow/dutschedule/GlobalVariables.kt | 5 + ...ccountSession.kt => DUTAccountInstance.kt} | 7 +- .../dutschedule/model/DUTNewsInstance.kt | 277 ++++++++ .../dutschedule/model/ProcessVariable.kt | 7 +- .../dutschedule/model/VariableListState.kt | 3 +- .../dutschedule/model/VariableState.kt | 3 +- .../dutschedule/model/VariableTimestamp.kt | 33 - .../zoemeow/dutschedule/model/news/DUTNews.kt | 38 -- .../dutschedule/model/news/NewsCache.kt | 13 - .../dutschedule/model/news/NewsCache2.kt | 13 - .../dutschedule/model/news/NewsGlobalItem.kt | 17 + .../dutschedule/model/news/NewsGroupByDate.kt | 20 - .../dutschedule/model/news/NewsSubjectItem.kt | 23 + .../repository/FileModuleRepository.kt | 70 +- .../service/NewsBackgroundUpdateService.kt | 311 +++++---- .../ui/component/account/LoginBox.kt | 9 +- .../ui/component/news/NewsListPage.kt | 9 +- .../ui/view/main/MainViewDashboard.kt | 18 +- .../dutschedule/ui/view/news/MainView.kt | 39 +- .../dutschedule/viewmodel/MainViewModel.kt | 241 +------ .../drawable-v24/ic_launcher_foreground.xml | 30 - .../res/drawable/ic_launcher_foreground.xml | 611 ++++++++++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 4 +- .../mipmap-anydpi-v26/ic_launcher_round.xml | 4 +- app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 2474 -> 2140 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 4312 -> 3744 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 1376 -> 1228 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 2506 -> 2220 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 3696 -> 3076 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 6522 -> 5604 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 6868 -> 5264 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 11428 -> 9176 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 10710 -> 7710 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 15358 -> 13266 bytes .../res/values/ic_launcher_background.xml | 4 + gradle/wrapper/gradle-wrapper.properties | 4 +- 39 files changed, 1346 insertions(+), 616 deletions(-) create mode 100644 .idea/icon.svg rename app/src/main/java/io/zoemeow/dutschedule/model/{DUTAccountSession.kt => DUTAccountInstance.kt} (98%) create mode 100644 app/src/main/java/io/zoemeow/dutschedule/model/DUTNewsInstance.kt delete mode 100644 app/src/main/java/io/zoemeow/dutschedule/model/VariableTimestamp.kt delete mode 100644 app/src/main/java/io/zoemeow/dutschedule/model/news/DUTNews.kt delete mode 100644 app/src/main/java/io/zoemeow/dutschedule/model/news/NewsCache.kt delete mode 100644 app/src/main/java/io/zoemeow/dutschedule/model/news/NewsCache2.kt create mode 100644 app/src/main/java/io/zoemeow/dutschedule/model/news/NewsGlobalItem.kt delete mode 100644 app/src/main/java/io/zoemeow/dutschedule/model/news/NewsGroupByDate.kt create mode 100644 app/src/main/java/io/zoemeow/dutschedule/model/news/NewsSubjectItem.kt delete mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/values/ic_launcher_background.xml diff --git a/.idea/icon.svg b/.idea/icon.svg new file mode 100644 index 0000000..db2cdb5 --- /dev/null +++ b/.idea/icon.svg @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/build.gradle b/app/build.gradle index 5c040b0..2caee8d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,20 +56,20 @@ dependencies { implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' implementation 'androidx.activity:activity-compose:1.8.2' - implementation platform('androidx.compose:compose-bom:2024.02.01') - implementation "androidx.compose.ui:ui:1.6.2" - implementation "androidx.compose.ui:ui-tooling-preview:1.6.2" + implementation platform('androidx.compose:compose-bom:2024.02.02') + implementation "androidx.compose.ui:ui:1.6.3" + implementation "androidx.compose.ui:ui-tooling-preview:1.6.3" implementation 'androidx.compose.material3:material3' - implementation platform('androidx.compose:compose-bom:2024.02.01') + implementation platform('androidx.compose:compose-bom:2024.02.02') implementation 'androidx.compose.ui:ui-graphics' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - androidTestImplementation platform('androidx.compose:compose-bom:2024.02.01') - androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.6.2" - androidTestImplementation platform('androidx.compose:compose-bom:2024.02.01') - debugImplementation "androidx.compose.ui:ui-tooling:1.6.2" - debugImplementation "androidx.compose.ui:ui-test-manifest:1.6.2" + androidTestImplementation platform('androidx.compose:compose-bom:2024.02.02') + androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.6.3" + androidTestImplementation platform('androidx.compose:compose-bom:2024.02.02') + debugImplementation "androidx.compose.ui:ui-tooling:1.6.3" + debugImplementation "androidx.compose.ui:ui-test-manifest:1.6.3" implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.appcompat:appcompat-resources:1.6.1" @@ -85,7 +85,7 @@ dependencies { runtimeOnly 'androidx.fragment:fragment-ktx:1.7.0-alpha10' // https://mvnrepository.com/artifact/androidx.compose.material3/material3 - runtimeOnly 'androidx.compose.material3:material3:1.2.0' + runtimeOnly 'androidx.compose.material3:material3:1.2.1' // AlarmManager for restart service after closed // Required to avoid crash on Android 12 - API 31 @@ -93,7 +93,7 @@ dependencies { implementation 'androidx.work:work-runtime-ktx:2.9.0' // https://mvnrepository.com/artifact/androidx.compose.material/material-icons-extended - runtimeOnly 'androidx.compose.material:material-icons-extended:1.6.2' + // runtimeOnly 'androidx.compose.material:material-icons-extended:1.6.3' // Google Dagger/Hilt implementation 'com.google.dagger:hilt-android:2.49' @@ -117,7 +117,7 @@ dependencies { implementation 'com.google.code.gson:gson:2.10.1' // Android Browser (use WebView in app) - https://mvnrepository.com/artifact/androidx.browser/browser - implementation 'androidx.browser:browser:1.8.0-rc01' + implementation 'androidx.browser:browser:1.8.0' // https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.12' diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png index c24e99eeff3421daf78ee490e9e09ce0d33ade88..88426674a9b8e5fd4308fd7cae621cea4cdbc72b 100644 GIT binary patch literal 46363 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 literal 101753 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 diff --git a/app/src/main/java/io/zoemeow/dutschedule/GlobalVariables.kt b/app/src/main/java/io/zoemeow/dutschedule/GlobalVariables.kt index 82388d5..193a907 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/GlobalVariables.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/GlobalVariables.kt @@ -1,2 +1,7 @@ package io.zoemeow.dutschedule +class GlobalVariables { + companion object { + const val requestExpiredDuration = 1000 * 60 * 5 + } +} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/DUTAccountSession.kt b/app/src/main/java/io/zoemeow/dutschedule/model/DUTAccountInstance.kt similarity index 98% rename from app/src/main/java/io/zoemeow/dutschedule/model/DUTAccountSession.kt rename to app/src/main/java/io/zoemeow/dutschedule/model/DUTAccountInstance.kt index c001543..0d18798 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/DUTAccountSession.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/DUTAccountInstance.kt @@ -25,7 +25,7 @@ import kotlinx.coroutines.withContext * 4: Account information * 5: Account training status */ -class DUTAccountSession( +class DUTAccountInstance( private val dutRequestRepository: DutRequestRepository, private val onEventSent: ((Int) -> Unit)? = null ) { @@ -73,6 +73,11 @@ class DUTAccountSession( this.accountSession.data.value = accountSession.clone() } + fun setSubjectScheduleCache(data: List) { + this.subjectSchedule.data.clear() + this.subjectSchedule.data.addAll(data) + } + fun getSubjectScheduleCache(): List { return this.subjectSchedule.data.toList() } diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/DUTNewsInstance.kt b/app/src/main/java/io/zoemeow/dutschedule/model/DUTNewsInstance.kt new file mode 100644 index 0000000..d513f15 --- /dev/null +++ b/app/src/main/java/io/zoemeow/dutschedule/model/DUTNewsInstance.kt @@ -0,0 +1,277 @@ +package io.zoemeow.dutschedule.model + +import androidx.compose.runtime.referentialEqualityPolicy +import io.zoemeow.dutschedule.model.news.NewsFetchType +import io.zoemeow.dutschedule.model.news.NewsGlobalItem +import io.zoemeow.dutschedule.model.news.NewsSubjectItem +import io.zoemeow.dutschedule.repository.DutRequestRepository +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * @param onEventSent Event when done: + * 1: Done + */ +class DUTNewsInstance( + private val dutRequestRepository: DutRequestRepository, + private val onEventSent: ((Int) -> Unit)? = null +) { + val newsGlobal: VariableListState = VariableListState( + parameters = mutableMapOf("nextPage" to "1") + ) + + val newsSubject: VariableListState = VariableListState( + parameters = mutableMapOf("nextPage" to "1") + ) + + fun loadNewsCache( + globalNewsList: List? = null, + globalNewsIndex: Int? = null, + globalNewsLastRequest: Long? = null, + subjectNewsList: List? = null, + subjectNewsIndex: Int? = null, + subjectNewsLastRequest: Long? = null + ) { + if (globalNewsList != null && globalNewsIndex != null) { + newsGlobal.let { + it.data.clear() + it.data.addAll(globalNewsList) + it.parameters["nextPage"] = globalNewsIndex.toString() + } + } + globalNewsLastRequest?.let { newsGlobal.lastRequest.longValue = it } + if (subjectNewsList != null && subjectNewsIndex != null) { + newsSubject.let { + it.data.clear() + it.data.addAll(subjectNewsList) + it.parameters["nextPage"] = subjectNewsIndex.toString() + } + } + subjectNewsLastRequest?.let { newsSubject.lastRequest.longValue = it } + } + + fun exportNewsCache( + onDataExported: ( + List, + Int, + List, + Int + ) -> Unit + ) { + onDataExported( + newsGlobal.data, + newsGlobal.parameters["nextPage"]?.toIntOrNull() ?: 1, + newsSubject.data, + newsSubject.parameters["nextPage"]?.toIntOrNull() ?: 1, + ) + } + + private fun launchOnScope( + script: () -> Unit, + invokeOnCompleted: ((Throwable?) -> Unit)? = null + ) { + CoroutineScope(Dispatchers.Main).launch { + withContext(Dispatchers.IO) { + script() + } + }.invokeOnCompletion { thr -> + invokeOnCompleted?.let { it(thr) } + } + } + + fun fetchGlobalNews( + fetchType: NewsFetchType = NewsFetchType.NextPage, + forceRequest: Boolean = true + ) { + if (!newsGlobal.isSuccessfulRequestExpired() && !forceRequest) { + return + } + if (newsGlobal.processState.value == ProcessState.Running) { + return + } + newsGlobal.processState.value = ProcessState.Running + + launchOnScope( + script = { + // Get news from internet + val newsFromInternet = dutRequestRepository.getNewsGlobal( + page = when (fetchType) { + NewsFetchType.NextPage -> newsGlobal.parameters["nextPage"]?.toIntOrNull() ?: 1 + NewsFetchType.FirstPage -> 1 + NewsFetchType.ClearAndFirstPage -> 1 + } + ) + + // If requested clear old news + if (fetchType == NewsFetchType.ClearAndFirstPage) { + newsGlobal.data.clear() + } + + // - Filter latest news into a variable + // - Remove duplicated news + // - Update news from server + val latestNews = arrayListOf() + newsFromInternet.forEach { newsTargetItem -> + val anyMatch = newsGlobal.data.any { newsSourceItem -> + newsSourceItem.date == newsTargetItem.date + && newsSourceItem.title == newsTargetItem.title + && newsSourceItem.contentString == newsTargetItem.contentString + } + val anyNeedUpdated = newsGlobal.data.any { newsSourceItem -> + newsSourceItem.date == newsTargetItem.date + && newsSourceItem.title == newsTargetItem.title + } + + when { + // Ignore when entire match + anyMatch -> {} + // Update when match title + anyNeedUpdated -> { + newsGlobal.data.first {newsSourceItem -> + newsSourceItem.date == newsTargetItem.date + && newsSourceItem.title == newsTargetItem.title + }.update(newsTargetItem) + } + // Otherwise, add to latest news collection + else -> { + val newsTemp = NewsGlobalItem() + newsTemp.update(newsTargetItem) + latestNews.add(newsTemp) + } + } + } + + // Reverse latest news collection + // Add all news in latestNews to global variable + if (fetchType == NewsFetchType.FirstPage) { + latestNews.reverse() + latestNews.forEach { newsGlobal.data.add(0, it) } + } else { + newsGlobal.data.addAll(latestNews) + } + + // Adjust index + newsGlobal.parameters.let { + when (fetchType) { + NewsFetchType.NextPage -> { + it["nextPage"] = ((it["nextPage"]?.toIntOrNull() ?: 1) + 1).toString() + } + NewsFetchType.FirstPage -> { + it["nextPage"] = (it["nextPage"]?.toIntOrNull() ?: 1).toString() + } + NewsFetchType.ClearAndFirstPage -> { + it["nextPage"] = 2.toString() + } + } + } + }, + invokeOnCompleted = { + newsGlobal.lastRequest.longValue = System.currentTimeMillis() + newsGlobal.processState.value = when { + it == null -> ProcessState.Successful + else -> ProcessState.Failed + } + onEventSent?.let { it(1) } + } + ) + } + + fun fetchSubjectNews( + fetchType: NewsFetchType = NewsFetchType.NextPage, + forceRequest: Boolean = true + ) { + if (!newsSubject.isSuccessfulRequestExpired() && !forceRequest) { + return + } + if (newsSubject.processState.value == ProcessState.Running) { + return + } + newsSubject.processState.value = ProcessState.Running + + launchOnScope( + script = { + // Get news from internet + val newsFromInternet = dutRequestRepository.getNewsSubject( + page = when (fetchType) { + NewsFetchType.NextPage -> newsSubject.parameters["nextPage"]?.toIntOrNull() ?: 1 + NewsFetchType.FirstPage -> 1 + NewsFetchType.ClearAndFirstPage -> 1 + } + ) + + // If requested clear old news + if (fetchType == NewsFetchType.ClearAndFirstPage) { + newsSubject.data.clear() + } + + // - Filter latest news into a variable + // - Remove duplicated news + // - Update news from server + val latestNews = arrayListOf() + newsFromInternet.forEach { newsTargetItem -> + val anyMatch = newsSubject.data.any { newsSourceItem -> + newsSourceItem.date == newsTargetItem.date + && newsSourceItem.title == newsTargetItem.title + && newsSourceItem.contentString == newsTargetItem.contentString + } + val anyNeedUpdated = newsSubject.data.any { newsSourceItem -> + newsSourceItem.date == newsTargetItem.date + && newsSourceItem.title == newsTargetItem.title + } + + when { + // Ignore when entire match + anyMatch -> {} + // Update when match title + anyNeedUpdated -> { + newsSubject.data.first {newsSourceItem -> + newsSourceItem.date == newsTargetItem.date + && newsSourceItem.title == newsTargetItem.title + }.update(newsTargetItem) + } + // Otherwise, add to latest news collection + else -> { + val newsTemp = NewsSubjectItem() + newsTemp.update(newsTargetItem) + latestNews.add(newsTemp) + } + } + } + + // Reverse latest news collection + // Add all news in latestNews to global variable + if (fetchType == NewsFetchType.FirstPage) { + latestNews.reverse() + latestNews.forEach { newsSubject.data.add(0, it) } + } else { + newsSubject.data.addAll(latestNews) + } + + // Adjust index + newsSubject.parameters.let { + when (fetchType) { + NewsFetchType.NextPage -> { + it["nextPage"] = ((it["nextPage"]?.toIntOrNull() ?: 1) + 1).toString() + } + NewsFetchType.FirstPage -> { + it["nextPage"] = (if ((it["nextPage"]?.toIntOrNull() ?: 1) == 1) 2 else (it["nextPage"]?.toIntOrNull() ?: 2)).toString() + } + NewsFetchType.ClearAndFirstPage -> { + it["nextPage"] = 2.toString() + } + } + } + }, + invokeOnCompleted = { + newsSubject.lastRequest.longValue = System.currentTimeMillis() + newsSubject.processState.value = when { + it == null -> ProcessState.Successful + else -> ProcessState.Failed + } + onEventSent?.let { it(1) } + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/ProcessVariable.kt b/app/src/main/java/io/zoemeow/dutschedule/model/ProcessVariable.kt index d534ab8..51b572a 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/ProcessVariable.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/ProcessVariable.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.MutableLongState import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf +import io.zoemeow.dutschedule.GlobalVariables import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -18,7 +19,7 @@ data class ProcessVariable( val onAfterRefresh: ((Boolean) -> Unit)? = null ) { private fun isExpired(): Boolean { - return (lastRequest.longValue + expiredDuration) < System.currentTimeMillis() + return (lastRequest.longValue + GlobalVariables.requestExpiredDuration) < System.currentTimeMillis() } private fun isSuccessfulRequestExpired(): Boolean { @@ -78,8 +79,4 @@ data class ProcessVariable( } ) } - - companion object { - const val expiredDuration = 1000 * 60 * 5 - } } diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/VariableListState.kt b/app/src/main/java/io/zoemeow/dutschedule/model/VariableListState.kt index cf2f458..c18ce1a 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/VariableListState.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/VariableListState.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.snapshots.SnapshotStateList +import io.zoemeow.dutschedule.GlobalVariables data class VariableListState( val data: SnapshotStateList = mutableStateListOf(), @@ -14,7 +15,7 @@ data class VariableListState( val parameters: MutableMap = mutableMapOf() ) { fun isExpired(): Boolean { - return (lastRequest.longValue + ProcessVariable.expiredDuration) < System.currentTimeMillis() + return (lastRequest.longValue + GlobalVariables.requestExpiredDuration) < System.currentTimeMillis() } fun isSuccessfulRequestExpired(): Boolean { diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/VariableState.kt b/app/src/main/java/io/zoemeow/dutschedule/model/VariableState.kt index e8a37d5..73bf944 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/VariableState.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/VariableState.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.MutableLongState import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf +import io.zoemeow.dutschedule.GlobalVariables data class VariableState( val data: MutableState, @@ -12,7 +13,7 @@ data class VariableState( val parameters: MutableMap = mutableMapOf() ) { fun isExpired(): Boolean { - return (lastRequest.longValue + ProcessVariable.expiredDuration) < System.currentTimeMillis() + return (lastRequest.longValue + GlobalVariables.requestExpiredDuration) < System.currentTimeMillis() } fun isSuccessfulRequestExpired(): Boolean { diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/VariableTimestamp.kt b/app/src/main/java/io/zoemeow/dutschedule/model/VariableTimestamp.kt deleted file mode 100644 index 2c26044..0000000 --- a/app/src/main/java/io/zoemeow/dutschedule/model/VariableTimestamp.kt +++ /dev/null @@ -1,33 +0,0 @@ -package io.zoemeow.dutschedule.model - -data class VariableTimestamp( - val lastRequest: Long = 0, - val processState: ProcessState = ProcessState.NotRunYet, - val data: T, -) { - // Session ID duration in milliseconds - private val expiredDuration = 1000 * 60 * 10 - - fun clone( - lastRequest: Long? = null, - processState: ProcessState? = null, - data: T? = null - ): VariableTimestamp { - return VariableTimestamp( - lastRequest = lastRequest ?: this.lastRequest, - processState = processState ?: this.processState, - data = data ?: this.data - ) - } - - fun isExpired(): Boolean { - return (lastRequest + expiredDuration) < System.currentTimeMillis() - } - - fun isSuccessfulRequestExpired(): Boolean { - return when (processState) { - ProcessState.Successful -> isExpired() - else -> true - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/news/DUTNews.kt b/app/src/main/java/io/zoemeow/dutschedule/model/news/DUTNews.kt deleted file mode 100644 index f0bdf70..0000000 --- a/app/src/main/java/io/zoemeow/dutschedule/model/news/DUTNews.kt +++ /dev/null @@ -1,38 +0,0 @@ -package io.zoemeow.dutschedule.model.news - -import io.dutwrapper.dutwrapper.model.news.NewsGlobalItem -import io.dutwrapper.dutwrapper.model.news.NewsSubjectItem -import io.zoemeow.dutschedule.model.VariableListState -import io.zoemeow.dutschedule.repository.DutRequestRepository - -/** - * @param onEventSent Event when done: - * 1: News global - * 2: News subject - */ -class DUTNews( - private val dutRequestRepository: DutRequestRepository, - private val onEventSent: ((Int) -> Unit)? = null -) { - companion object { - fun VariableListState.getPage(): Int { - return try { - this.parameters["page"]?.toInt() ?: 1 - } catch (_: Exception) { - 1 - } - } - - fun VariableListState.setPage(page: Int = 1) { - if (page < 1) { - throw Exception("") - } - this.parameters["page"] = page.toString() - } - } - - val newsGlobal: VariableListState = VariableListState() - val newsSubject: VariableListState = VariableListState() - - -} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/news/NewsCache.kt b/app/src/main/java/io/zoemeow/dutschedule/model/news/NewsCache.kt deleted file mode 100644 index f539467..0000000 --- a/app/src/main/java/io/zoemeow/dutschedule/model/news/NewsCache.kt +++ /dev/null @@ -1,13 +0,0 @@ -package io.zoemeow.dutschedule.model.news - -import com.google.gson.annotations.SerializedName -import java.io.Serializable - -data class NewsCache( - @SerializedName("news_list") - val newsListByDate: ArrayList> = arrayListOf(), - @SerializedName("page_current") - var pageCurrent: Int = 1, - @SerializedName("last_modified_date") - var lastModifiedDate: Long = 0 -) : Serializable diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/news/NewsCache2.kt b/app/src/main/java/io/zoemeow/dutschedule/model/news/NewsCache2.kt deleted file mode 100644 index 61e879b..0000000 --- a/app/src/main/java/io/zoemeow/dutschedule/model/news/NewsCache2.kt +++ /dev/null @@ -1,13 +0,0 @@ -package io.zoemeow.dutschedule.model.news - -import com.google.gson.annotations.SerializedName -import java.io.Serializable - -data class NewsCache2( - @SerializedName("data") - val data: ArrayList = arrayListOf(), - @SerializedName("nextpage") - var nextPage: Int = 1, - @SerializedName("lastrequest") - var lastRequest: Long = 0 -) : Serializable diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/news/NewsGlobalItem.kt b/app/src/main/java/io/zoemeow/dutschedule/model/news/NewsGlobalItem.kt new file mode 100644 index 0000000..381be94 --- /dev/null +++ b/app/src/main/java/io/zoemeow/dutschedule/model/news/NewsGlobalItem.kt @@ -0,0 +1,17 @@ +package io.zoemeow.dutschedule.model.news + +data class NewsGlobalItem( + var updated: Boolean = false +) : io.dutwrapper.dutwrapper.model.news.NewsGlobalItem() { + fun update(newsItem: io.dutwrapper.dutwrapper.model.news.NewsGlobalItem) { + if (this.title == newsItem.title && this.date == newsItem.date) { + this.updated = true + } + + this.title = newsItem.title + this.content = newsItem.content + this.contentString = newsItem.contentString + this.links = newsItem.links + this.date = newsItem.date + } +} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/news/NewsGroupByDate.kt b/app/src/main/java/io/zoemeow/dutschedule/model/news/NewsGroupByDate.kt deleted file mode 100644 index e2564b4..0000000 --- a/app/src/main/java/io/zoemeow/dutschedule/model/news/NewsGroupByDate.kt +++ /dev/null @@ -1,20 +0,0 @@ -package io.zoemeow.dutschedule.model.news - -import com.google.gson.annotations.SerializedName -import java.io.Serializable - -data class NewsGroupByDate( - @SerializedName("item_list") - val itemList: ArrayList = ArrayList(), - - @SerializedName("date") - val date: Long = 0, -): Serializable { - fun addAll(newsList: List) { - itemList.addAll(newsList) - } - - fun add(newsItem: T) { - itemList.add(newsItem) - } -} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/news/NewsSubjectItem.kt b/app/src/main/java/io/zoemeow/dutschedule/model/news/NewsSubjectItem.kt new file mode 100644 index 0000000..068d7ad --- /dev/null +++ b/app/src/main/java/io/zoemeow/dutschedule/model/news/NewsSubjectItem.kt @@ -0,0 +1,23 @@ +package io.zoemeow.dutschedule.model.news + +data class NewsSubjectItem( + var updated: Boolean = false +) : io.dutwrapper.dutwrapper.model.news.NewsSubjectItem() { + fun update(newsItem: io.dutwrapper.dutwrapper.model.news.NewsSubjectItem) { + this.title = newsItem.title + this.content = newsItem.content + this.contentString = newsItem.contentString + this.links = newsItem.links + this.date = newsItem.date + + this.affectedClass = newsItem.affectedClass + this.affectedDate = newsItem.affectedDate + this.lessonStatus = newsItem.lessonStatus + this.affectedLesson = newsItem.affectedLesson + this.affectedRoom = newsItem.affectedRoom + this.lecturerName = newsItem.lecturerName + this.lecturerGender = newsItem.lecturerGender + + this.updated = true + } +} diff --git a/app/src/main/java/io/zoemeow/dutschedule/repository/FileModuleRepository.kt b/app/src/main/java/io/zoemeow/dutschedule/repository/FileModuleRepository.kt index ff265e0..d6b9d5d 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/repository/FileModuleRepository.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/repository/FileModuleRepository.kt @@ -4,24 +4,22 @@ import android.content.Context import com.google.gson.Gson import com.google.gson.reflect.TypeToken import io.dutwrapper.dutwrapper.model.accounts.SubjectScheduleItem -import io.dutwrapper.dutwrapper.model.news.NewsGlobalItem -import io.dutwrapper.dutwrapper.model.news.NewsSubjectItem import io.dutwrapper.dutwrapper.model.utils.DutSchoolYearItem import io.zoemeow.dutschedule.model.NotificationHistory import io.zoemeow.dutschedule.model.account.AccountSession -import io.zoemeow.dutschedule.model.news.NewsCache +import io.zoemeow.dutschedule.model.news.NewsGlobalItem import io.zoemeow.dutschedule.model.news.NewsSearchHistory +import io.zoemeow.dutschedule.model.news.NewsSubjectItem import io.zoemeow.dutschedule.model.settings.AppSettings import java.io.File - class FileModuleRepository( context: Context ) { // context.cacheDir - private val PATH_CACHE_NEWSGLOBAL = "${context.filesDir.path}/cache_news_global.json" - private val PATH_CACHE_NEWSSUBJECT = "${context.filesDir.path}/cache_news_subject.json" + private val PATH_CACHE_NEWSGLOBAL = "${context.filesDir.path}/news.global.cache.json" + private val PATH_CACHE_NEWSSUBJECT = "${context.filesDir.path}/news.subject.cache.json" private val PATH_APPSETTINGS = "${context.filesDir.path}/settings.json" private val PATH_ACCOUNT = "${context.filesDir.path}/account.json" private val PATH_ACCOUNT_SUBJECTSCHEDULE_CACHE = "${context.filesDir.path}/account.subjectschedule.cache.json" @@ -78,52 +76,84 @@ class FileModuleRepository( } fun saveCacheNewsGlobal( - newsCacheGlobal: NewsCache + newsList: List, + newsNextPage: Int, + lastRequest: Long = 0 ) { + val map = mapOf( + "data" to Gson().toJson(newsList), + "nextPage" to newsNextPage.toString(), + "lastRequest" to lastRequest.toString() + ) val file = File(PATH_CACHE_NEWSGLOBAL) - file.writeText(Gson().toJson(newsCacheGlobal)) + file.writeText(Gson().toJson(map)) } - fun getCacheNewsGlobal(): NewsCache { + fun getCacheNewsGlobal( + onDataExported: (List, Int, Long) -> Unit + ) { val file = File(PATH_CACHE_NEWSGLOBAL) try { file.bufferedReader().apply { val text = this.use { it.readText() } - val newsCacheGlobal = Gson().fromJson>( + val objItem = Gson().fromJson>( text, - (object : TypeToken>() {}.type) + (object : TypeToken>() {}.type) ) this.close() - return newsCacheGlobal + onDataExported( + Gson().fromJson( + objItem["data"], + (object : TypeToken>() {}.type) + ), + objItem["nextPage"]?.toIntOrNull() ?: 1, + objItem["lastRequest"]?.toLongOrNull() ?: 0 + ) } } catch (ex: Exception) { ex.printStackTrace() - return NewsCache() + onDataExported(listOf(), 1, 0) } } fun saveCacheNewsSubject( - newsCacheSubject: NewsCache + newsList: List, + newsNextPage: Int, + lastRequest: Long = 0 ) { + val map = mapOf( + "data" to Gson().toJson(newsList), + "nextPage" to newsNextPage.toString(), + "lastRequest" to lastRequest.toString() + ) val file = File(PATH_CACHE_NEWSSUBJECT) - file.writeText(Gson().toJson(newsCacheSubject)) + file.writeText(Gson().toJson(map)) } - fun getCacheNewsSubject(): NewsCache { + fun getCacheNewsSubject( + onDataExported: (List, Int, Long) -> Unit + ) { val file = File(PATH_CACHE_NEWSSUBJECT) try { file.bufferedReader().apply { val text = this.use { it.readText() } - val newsCacheGlobal = Gson().fromJson>( + val objItem = Gson().fromJson>( text, - (object : TypeToken>() {}.type) + (object : TypeToken>() {}.type) ) this.close() - return newsCacheGlobal + onDataExported( + Gson().fromJson( + objItem["data"], + (object : TypeToken>() {}.type) + ), + objItem["nextPage"]?.toIntOrNull() ?: 1, + objItem["lastRequest"]?.toLongOrNull() ?: 0 + ) } } catch (ex: Exception) { ex.printStackTrace() - return NewsCache() + onDataExported(listOf(), 1, 0) } } diff --git a/app/src/main/java/io/zoemeow/dutschedule/service/NewsBackgroundUpdateService.kt b/app/src/main/java/io/zoemeow/dutschedule/service/NewsBackgroundUpdateService.kt index 56561cb..d9d8f47 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/service/NewsBackgroundUpdateService.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/service/NewsBackgroundUpdateService.kt @@ -13,10 +13,10 @@ import io.dutwrapper.dutwrapper.model.news.NewsGlobalItem import io.dutwrapper.dutwrapper.model.news.NewsSubjectItem import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.PermissionRequestActivity +import io.zoemeow.dutschedule.model.DUTNewsInstance import io.zoemeow.dutschedule.model.NotificationHistory import io.zoemeow.dutschedule.model.ProcessState import io.zoemeow.dutschedule.model.news.NewsFetchType -import io.zoemeow.dutschedule.model.news.NewsGroupByDate import io.zoemeow.dutschedule.model.settings.AppSettings import io.zoemeow.dutschedule.model.settings.SubjectCode import io.zoemeow.dutschedule.repository.DutRequestRepository @@ -35,6 +35,7 @@ class NewsBackgroundUpdateService : BaseService( private lateinit var file: FileModuleRepository private lateinit var dutRequestRepository: DutRequestRepository private lateinit var settings: AppSettings + private lateinit var newsInstance: DUTNewsInstance override fun onInitialize() { file = FileModuleRepository(this) @@ -132,96 +133,97 @@ class NewsBackgroundUpdateService : BaseService( private fun fetchNewsGlobal( fetchType: NewsFetchType = NewsFetchType.NextPage ) { + val newsList: ArrayList = arrayListOf() + var newsIndex = 0 + var lastRequest = 0L try { // Get news cache - val newsCache = file.getCacheNewsGlobal() + file.getCacheNewsGlobal( + onDataExported = { list, index, lq -> + newsList.clear() + newsList.addAll(list) + newsIndex = index + lastRequest = lq + } + ) - if (newsCache.lastModifiedDate + (settings.newsBackgroundDuration * 60 * 1000) > System.currentTimeMillis()) { + if (lastRequest + (settings.newsBackgroundDuration * 60 * 1000) > System.currentTimeMillis()) { throw Exception("Request too fast. Try again later.") } // Get news from internet val newsFromInternet = dutRequestRepository.getNewsGlobal( page = when (fetchType) { - NewsFetchType.NextPage -> newsCache.pageCurrent + NewsFetchType.NextPage -> newsIndex NewsFetchType.FirstPage -> 1 NewsFetchType.ClearAndFirstPage -> 1 } ) - // If requested, clear cache + // If requested clear old news if (fetchType == NewsFetchType.ClearAndFirstPage) { - newsCache.newsListByDate.clear() + newsList.clear() } - // Remove duplicate news to new list - val newsFiltered = arrayListOf>() - newsFromInternet.forEach { newsItem -> - val anyMatch = newsCache.newsListByDate.any { newsSourceGroup -> - newsSourceGroup.itemList.any { newsSourceItem -> - newsSourceItem.date == newsItem.date - && newsSourceItem.title == newsItem.title - && newsSourceItem.contentString == newsItem.contentString - } + val notifyNews = arrayListOf() + + // - Filter latest news into a variable + // - Remove duplicated news + // - Update news from server + val latestNews = arrayListOf() + newsFromInternet.forEach { newsTargetItem -> + val anyMatch = newsList.any { newsSourceItem -> + newsSourceItem.date == newsTargetItem.date + && newsSourceItem.title == newsTargetItem.title + && newsSourceItem.contentString == newsTargetItem.contentString + } + val anyNeedUpdated = newsList.any { newsSourceItem -> + newsSourceItem.date == newsTargetItem.date + && newsSourceItem.title == newsTargetItem.title } - if (!anyMatch) { - // Check if date group exist - val groupExist = - newsFiltered.any { newsGroupTarget -> newsGroupTarget.date == newsItem.date } - if (!groupExist) { - val newsGroupNew = NewsGroupByDate( - date = newsItem.date, - itemList = arrayListOf(newsItem) - ) - newsFiltered.add(newsGroupNew) - } else { - newsFiltered.first { newsGroupTarget -> newsGroupTarget.date == newsItem.date } - .add(newsItem) + when { + // Ignore when entire match + anyMatch -> {} + // Update when match title + anyNeedUpdated -> { + newsList.first {newsSourceItem -> + newsSourceItem.date == newsTargetItem.date + && newsSourceItem.title == newsTargetItem.title + }.update(newsTargetItem) + val newsTemp = io.zoemeow.dutschedule.model.news.NewsGlobalItem() + newsTemp.update(newsTargetItem) + notifyNews.add(newsTemp) + } + // Otherwise, add to latest news collection + else -> { + val newsTemp = io.zoemeow.dutschedule.model.news.NewsGlobalItem() + newsTemp.update(newsTargetItem) + latestNews.add(newsTemp) + notifyNews.add(newsTemp) } } } - // Add to current cache - newsFiltered.forEach { newsGroup -> - var itemIndex = 0 - newsGroup.itemList.forEach { newsItem -> - if (newsCache.newsListByDate.any { group -> group.date == newsItem.date }) { - if (fetchType == NewsFetchType.FirstPage) { - newsCache.newsListByDate.first { group -> group.date == newsItem.date } - .itemList.add(itemIndex, newsItem) - itemIndex += 1 - } else { - newsCache.newsListByDate.first { group -> group.date == newsItem.date } - .itemList.add(newsItem) - } - } else { - val newsGroupNew = NewsGroupByDate( - date = newsItem.date, - itemList = arrayListOf(newsItem) - ) - newsCache.newsListByDate.add(newsGroupNew) - } - } + // Reverse latest news collection + // Add all news in latestNews to global variable + if (fetchType == NewsFetchType.FirstPage) { + latestNews.reverse() + latestNews.forEach { newsList.add(0, it) } + } else { + newsList.addAll(latestNews) } - newsCache.newsListByDate.sortByDescending { group -> group.date } - when (fetchType) { - NewsFetchType.NextPage -> { - newsCache.pageCurrent += 1 - } - NewsFetchType.FirstPage -> { - if (newsCache.pageCurrent <= 1) - newsCache.pageCurrent += 1 - } - NewsFetchType.ClearAndFirstPage -> { - newsCache.pageCurrent = 2 - } + // Adjust index + newsIndex = when (fetchType) { + NewsFetchType.NextPage -> newsIndex + 1 + NewsFetchType.FirstPage -> if (newsIndex > 1) newsIndex else 2 + NewsFetchType.ClearAndFirstPage -> 2 } - newsCache.lastModifiedDate = System.currentTimeMillis() + lastRequest = System.currentTimeMillis() - file.saveCacheNewsGlobal(newsCache) + file.saveCacheNewsGlobal(newsList, newsIndex, lastRequest) // Check if any news need to be notify here using newsFiltered! // If no notification permission, aborting... @@ -237,10 +239,8 @@ class NewsBackgroundUpdateService : BaseService( // TODO: Notify by notify variable... // Processing news global notifications for notify here! - newsFiltered.forEach { newsGroup -> - newsGroup.itemList.forEach { newsItem -> - notifyNewsGlobal(this, newsItem) - } + notifyNews.forEach { + notifyNewsGlobal(this, it) } Log.d("NewsBackgroundService", "Done executing function in news global.") } catch (ex: Exception) { @@ -252,96 +252,95 @@ class NewsBackgroundUpdateService : BaseService( private fun fetchNewsSubject( fetchType: NewsFetchType = NewsFetchType.NextPage ) { + val newsList: ArrayList = arrayListOf() + var newsIndex = 0 + var lastRequest = 0L try { // Get news cache - val newsCache = file.getCacheNewsSubject() + file.getCacheNewsSubject( + onDataExported = { list, index, lq -> + newsList.clear() + newsList.addAll(list) + newsIndex = index + lastRequest = lq + } + ) - if (newsCache.lastModifiedDate + (settings.newsBackgroundDuration * 60 * 1000) > System.currentTimeMillis()) { + if (lastRequest + (settings.newsBackgroundDuration * 60 * 1000) > System.currentTimeMillis()) { throw Exception("Request too fast. Try again later.") } // Get news from internet val newsFromInternet = dutRequestRepository.getNewsSubject( page = when (fetchType) { - NewsFetchType.NextPage -> newsCache.pageCurrent + NewsFetchType.NextPage -> newsIndex NewsFetchType.FirstPage -> 1 NewsFetchType.ClearAndFirstPage -> 1 } ) - // If requested, clear cache + // If requested clear old news if (fetchType == NewsFetchType.ClearAndFirstPage) { - newsCache.newsListByDate.clear() + newsList.clear() } - // Remove duplicate news to new list - val newsFiltered = arrayListOf>() - newsFromInternet.forEach { newsItem -> - val anyMatch = newsCache.newsListByDate.any { newsSourceGroup -> - newsSourceGroup.itemList.any { newsSourceItem -> - newsSourceItem.date == newsItem.date - && newsSourceItem.title == newsItem.title - && newsSourceItem.contentString == newsItem.contentString - } + val notifyNews = arrayListOf() + + // - Filter latest news into a variable + // - Remove duplicated news + // - Update news from server + val latestNews = arrayListOf() + newsFromInternet.forEach { newsTargetItem -> + val anyMatch = newsList.any { newsSourceItem -> + newsSourceItem.date == newsTargetItem.date + && newsSourceItem.title == newsTargetItem.title + && newsSourceItem.contentString == newsTargetItem.contentString + } + val anyNeedUpdated = newsList.any { newsSourceItem -> + newsSourceItem.date == newsTargetItem.date + && newsSourceItem.title == newsTargetItem.title } - if (!anyMatch) { - // Check if date group exist - val groupExist = - newsFiltered.any { newsGroupTarget -> newsGroupTarget.date == newsItem.date } - if (!groupExist) { - val newsGroupNew = NewsGroupByDate( - date = newsItem.date, - itemList = arrayListOf(newsItem) - ) - newsFiltered.add(newsGroupNew) - } else { - newsFiltered.first { newsGroupTarget -> newsGroupTarget.date == newsItem.date } - .add(newsItem) + when { + // Ignore when entire match + anyMatch -> {} + // Update when match title + anyNeedUpdated -> { + newsList.first {newsSourceItem -> + newsSourceItem.date == newsTargetItem.date + && newsSourceItem.title == newsTargetItem.title + }.update(newsTargetItem) + val newsTemp = io.zoemeow.dutschedule.model.news.NewsSubjectItem() + newsTemp.update(newsTargetItem) + notifyNews.add(newsTemp) + } + // Otherwise, add to latest news collection + else -> { + val newsTemp = io.zoemeow.dutschedule.model.news.NewsSubjectItem() + newsTemp.update(newsTargetItem) + latestNews.add(newsTemp) + notifyNews.add(newsTemp) } } } - // Add to current cache - newsFiltered.forEach { newsGroup -> - var itemIndex = 0 - newsGroup.itemList.forEach { newsItem -> - if (newsCache.newsListByDate.any { group -> group.date == newsItem.date }) { - if (fetchType == NewsFetchType.FirstPage) { - newsCache.newsListByDate.first { group -> group.date == newsItem.date } - .itemList.add(itemIndex, newsItem) - itemIndex += 1 - } else { - newsCache.newsListByDate.first { group -> group.date == newsItem.date } - .itemList.add(newsItem) - } - } else { - val newsGroupNew = NewsGroupByDate( - date = newsItem.date, - itemList = arrayListOf(newsItem) - ) - newsCache.newsListByDate.add(newsGroupNew) - } - } + if (fetchType == NewsFetchType.FirstPage) { + latestNews.reverse() + latestNews.forEach { newsList.add(0, it) } + } else { + newsList.addAll(latestNews) } - newsCache.newsListByDate.sortByDescending { group -> group.date } - when (fetchType) { - NewsFetchType.NextPage -> { - newsCache.pageCurrent += 1 - } - NewsFetchType.FirstPage -> { - if (newsCache.pageCurrent <= 1) - newsCache.pageCurrent += 1 - } - NewsFetchType.ClearAndFirstPage -> { - newsCache.pageCurrent = 2 - } + // Adjust index + newsIndex = when (fetchType) { + NewsFetchType.NextPage -> newsIndex + 1 + NewsFetchType.FirstPage -> if (newsIndex > 1) newsIndex else 2 + NewsFetchType.ClearAndFirstPage -> 2 } - newsCache.lastModifiedDate = System.currentTimeMillis() + lastRequest = System.currentTimeMillis() - file.saveCacheNewsSubject(newsCache) + file.saveCacheNewsSubject(newsList, newsIndex, lastRequest) // Check if any news need to be notify here using newsFiltered! // If no notification permission, aborting... @@ -357,39 +356,37 @@ class NewsBackgroundUpdateService : BaseService( // TODO: Notify by notify variable... // TODO: Processing news subject notifications for notify here! - newsFiltered.forEach newsGroupForEach@ { newsGroup -> - newsGroup.itemList.forEach newsItemForEach@ { newsItem -> - // Default value is false. - var notifyRequired = false - // If enabled news filter, do following. - - // settings.newsBackgroundSubjectEnabled == 0 -> All news enabled - if (settings.newsBackgroundSubjectEnabled == 0) { - notifyRequired = true - } - // TODO: settings.newsBackgroundSubjectEnabled == 1 action - // settings.newsBackgroundSubjectEnabled == 2 - else if (settings.newsBackgroundFilterList.any { source -> - newsItem.affectedClass.any { targetGroup -> - targetGroup.codeList.any { target -> - source.isEquals( - SubjectCode( - target.studentYearId, - target.classId, - targetGroup.subjectName - ) + notifyNews.forEach { newsItem -> + // Default value is false. + var notifyRequired = false + // If enabled news filter, do following. + + // settings.newsBackgroundSubjectEnabled == 0 -> All news enabled + if (settings.newsBackgroundSubjectEnabled == 0) { + notifyRequired = true + } + // TODO: settings.newsBackgroundSubjectEnabled == 1 action + // settings.newsBackgroundSubjectEnabled == 2 + else if (settings.newsBackgroundFilterList.any { source -> + newsItem.affectedClass.any { targetGroup -> + targetGroup.codeList.any { target -> + source.isEquals( + SubjectCode( + target.studentYearId, + target.classId, + targetGroup.subjectName ) - } + ) } } - ) notifyRequired = true + } + ) notifyRequired = true - // TODO: If no notify/notify settings is off, continue with return@forEach. - // notifyRequired and notify variable + // TODO: If no notify/notify settings is off, continue with return@forEach. + // notifyRequired and notify variable - if (notifyRequired) { - notifyNewsSubject(this, newsItem) - } + if (notifyRequired) { + notifyNewsSubject(this, newsItem) } } Log.d("NewsBackgroundService", "Done executing function in news subject.") diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LoginBox.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LoginBox.kt index 29a68d2..68874f6 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LoginBox.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LoginBox.kt @@ -6,13 +6,10 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Checkbox @@ -35,12 +32,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.zoemeow.dutschedule.R @@ -233,8 +228,6 @@ private fun LoginBoxPreview() { isProcessing = false, isControlEnabled = true, isLoggedInBefore = false, - onSubmit = { username, password, rememberLogin -> - - } + onSubmit = { _, _, _ -> } ) } \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/news/NewsListPage.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/news/NewsListPage.kt index ae84b3f..9bcde2d 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/news/NewsListPage.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/news/NewsListPage.kt @@ -21,13 +21,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import io.dutwrapper.dutwrapper.model.news.NewsGlobalItem import io.zoemeow.dutschedule.model.ProcessState -import io.zoemeow.dutschedule.model.news.NewsGroupByDate import io.zoemeow.dutschedule.utils.CustomDateUtil import io.zoemeow.dutschedule.utils.endOfListReached @Composable fun NewsListPage( - newsList: List> = listOf(), + newsList: List = listOf(), processState: ProcessState = ProcessState.NotRunYet, endOfListReached: (() -> Unit)? = null, itemClicked: ((NewsGlobalItem) -> Unit)? = null, @@ -54,20 +53,20 @@ fun NewsListPage( content = { when { (newsList.isNotEmpty()) -> { - newsList.forEach { newsGroup -> + newsList.groupBy { p -> p.date }.forEach { newsGroup -> item { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, content = { Text( - text = CustomDateUtil.dateUnixToString(newsGroup.date, "dd/MM/yyyy"), + text = CustomDateUtil.dateUnixToString(newsGroup.key, "dd/MM/yyyy"), modifier = Modifier.padding(bottom = 5.dp) ) } ) } - items (newsGroup.itemList) { newsItem -> + items (newsGroup.value) { newsItem -> NewsListItem( title = newsItem.title ?: "", description = newsItem.contentString ?: "", diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt index 74d7abf..b14288c 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt @@ -48,7 +48,6 @@ import io.zoemeow.dutschedule.activity.MainActivity import io.zoemeow.dutschedule.activity.NewsActivity import io.zoemeow.dutschedule.model.CustomClock import io.zoemeow.dutschedule.model.ProcessState -import io.zoemeow.dutschedule.model.news.NewsCache import io.zoemeow.dutschedule.ui.component.main.DateAndTimeSummaryItem import io.zoemeow.dutschedule.ui.component.main.LessonTodaySummaryItem import io.zoemeow.dutschedule.ui.component.main.SchoolNewsSummaryItem @@ -85,20 +84,11 @@ fun MainActivity.MainViewDashboard( ).toInstant(TimeZone.UTC) val before7Days = today.minus(7.days) + // https://stackoverflow.com/questions/77368433/how-to-get-current-date-with-reset-time-0000-with-kotlinx-localdatetime if (!byWeek) { - (getMainViewModel().newsGlobal.data.value ?: NewsCache()).newsListByDate.firstOrNull { - // https://stackoverflow.com/questions/77368433/how-to-get-current-date-with-reset-time-0000-with-kotlinx-localdatetime - it.date == today.toEpochMilliseconds() - }.also { - if (it != null) data = it.itemList.count() - } + data = getMainViewModel().newsInstance.newsGlobal.data.filter { it.date == today.toEpochMilliseconds() }.size } else { - (getMainViewModel().newsGlobal.data.value ?: NewsCache()).newsListByDate.forEach { - // https://stackoverflow.com/questions/77368433/how-to-get-current-date-with-reset-time-0000-with-kotlinx-localdatetime - if (it.date <= today.toEpochMilliseconds() && it.date >= before7Days.toEpochMilliseconds()) { - data += it.itemList.count() - } - } + data = getMainViewModel().newsInstance.newsGlobal.data.filter { it.date <= today.toEpochMilliseconds() && it.date >= before7Days.toEpochMilliseconds() }.size } return data } @@ -302,7 +292,7 @@ fun MainActivity.MainViewDashboard( clicked = { context.startActivity(Intent(context, NewsActivity::class.java)) }, - isLoading = getMainViewModel().newsGlobal.processState.value == ProcessState.Running, + isLoading = getMainViewModel().newsInstance.newsGlobal.processState.value == ProcessState.Running, opacity = getControlBackgroundAlpha() ) UpdateAvailableSummaryItem( diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt index 468a160..13c628f 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt @@ -44,7 +44,6 @@ import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.NewsActivity import io.zoemeow.dutschedule.model.ProcessState import io.zoemeow.dutschedule.model.news.NewsFetchType -import io.zoemeow.dutschedule.model.news.NewsGroupByDate import io.zoemeow.dutschedule.ui.component.base.ButtonBase import io.zoemeow.dutschedule.ui.component.news.NewsListPage import kotlinx.coroutines.CoroutineScope @@ -158,11 +157,11 @@ fun NewsActivity.MainView( floatingActionButton = { if (when (pagerState.currentPage) { 0 -> { - getMainViewModel().newsGlobal.processState.value != ProcessState.Running + getMainViewModel().newsInstance.newsGlobal.processState.value != ProcessState.Running } 1 -> { - getMainViewModel().newsSubject.processState.value != ProcessState.Running + getMainViewModel().newsInstance.newsSubject.processState.value != ProcessState.Running } else -> false @@ -172,16 +171,16 @@ fun NewsActivity.MainView( onClick = { when (pagerState.currentPage) { 0 -> { - getMainViewModel().newsGlobal.refreshData( - force = true, - args = mapOf("newsfetchtype" to NewsFetchType.ClearAndFirstPage.value.toString()) + getMainViewModel().newsInstance.fetchGlobalNews( + fetchType = NewsFetchType.ClearAndFirstPage, + forceRequest = true ) } 1 -> { - getMainViewModel().newsSubject.refreshData( - force = true, - args = mapOf("newsfetchtype" to NewsFetchType.ClearAndFirstPage.value.toString()) + getMainViewModel().newsInstance.fetchSubjectNews( + fetchType = NewsFetchType.ClearAndFirstPage, + forceRequest = true ) } @@ -202,9 +201,8 @@ fun NewsActivity.MainView( when (pageIndex) { 0 -> { NewsListPage( - newsList = (getMainViewModel().newsGlobal.data.value?.newsListByDate - ?: arrayListOf()), - processState = getMainViewModel().newsGlobal.processState.value, + newsList = getMainViewModel().newsInstance.newsGlobal.data.toList(), + processState = getMainViewModel().newsInstance.newsGlobal.processState.value, opacity = getControlBackgroundAlpha(), itemClicked = { newsItem -> context.startActivity( @@ -220,9 +218,9 @@ fun NewsActivity.MainView( endOfListReached = { CoroutineScope(Dispatchers.Main).launch { withContext(Dispatchers.IO) { - getMainViewModel().newsGlobal.refreshData( - force = true, - args = mapOf("newsfetchtype" to NewsFetchType.NextPage.value.toString()) + getMainViewModel().newsInstance.fetchGlobalNews( + fetchType = NewsFetchType.NextPage, + forceRequest = true ) } } @@ -233,9 +231,8 @@ fun NewsActivity.MainView( 1 -> { @Suppress("UNCHECKED_CAST") (NewsListPage( - newsList = (getMainViewModel().newsSubject.data.value?.newsListByDate - ?: arrayListOf()) as ArrayList>, - processState = getMainViewModel().newsSubject.processState.value, + newsList = getMainViewModel().newsInstance.newsSubject.data.toList() as List, + processState = getMainViewModel().newsInstance.newsSubject.processState.value, opacity = getControlBackgroundAlpha(), itemClicked = { newsItem -> context.startActivity( @@ -251,9 +248,9 @@ fun NewsActivity.MainView( endOfListReached = { CoroutineScope(Dispatchers.Main).launch { withContext(Dispatchers.IO) { - getMainViewModel().newsSubject.refreshData( - force = true, - args = mapOf("newsfetchtype" to NewsFetchType.NextPage.value.toString()) + getMainViewModel().newsInstance.fetchSubjectNews( + fetchType = NewsFetchType.NextPage, + forceRequest = true ) } } diff --git a/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt b/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt index caaa737..6d2c4c2 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt @@ -9,16 +9,13 @@ import com.google.gson.Gson import com.google.gson.reflect.TypeToken import dagger.hilt.android.lifecycle.HiltViewModel import io.dutwrapper.dutwrapper.Utils -import io.dutwrapper.dutwrapper.model.news.NewsGlobalItem -import io.dutwrapper.dutwrapper.model.news.NewsSubjectItem import io.dutwrapper.dutwrapper.model.utils.DutSchoolYearItem -import io.zoemeow.dutschedule.model.DUTAccountSession +import io.zoemeow.dutschedule.model.DUTAccountInstance +import io.zoemeow.dutschedule.model.DUTNewsInstance import io.zoemeow.dutschedule.model.NotificationHistory import io.zoemeow.dutschedule.model.ProcessVariable import io.zoemeow.dutschedule.model.account.AccountSession -import io.zoemeow.dutschedule.model.news.NewsCache import io.zoemeow.dutschedule.model.news.NewsFetchType -import io.zoemeow.dutschedule.model.news.NewsGroupByDate import io.zoemeow.dutschedule.model.settings.AppSettings import io.zoemeow.dutschedule.repository.DutRequestRepository import io.zoemeow.dutschedule.repository.FileModuleRepository @@ -35,7 +32,7 @@ class MainViewModel @Inject constructor( ) : ViewModel() { val appSettings: MutableState = mutableStateOf(AppSettings()) - val accountSession: DUTAccountSession = DUTAccountSession( + val accountSession: DUTAccountInstance = DUTAccountInstance( dutRequestRepository = dutRequestRepository, onEventSent = { eventId -> when (eventId) { @@ -51,210 +48,15 @@ class MainViewModel @Inject constructor( } ) - /** - * Refresh or clear news global. - * - * @param newsfetchtype Following NewsFetchType enum class. - */ - val newsGlobal = ProcessVariable>( - onRefresh = { baseData, arg -> - val newsBase = baseData ?: NewsCache() - val fetchType = NewsFetchType.fromValue(Integer.parseInt(arg?.get("newsfetchtype") ?: "1")) - - // Get news from internet - val newsFromInternet = dutRequestRepository.getNewsGlobal( - page = when (fetchType) { - NewsFetchType.NextPage -> newsBase.pageCurrent - NewsFetchType.FirstPage -> 1 - NewsFetchType.ClearAndFirstPage -> 1 - } - ) - - // If requested, clear cache - if (fetchType == NewsFetchType.ClearAndFirstPage) { - newsBase.newsListByDate.clear() - } - - // Remove duplicate news to new list - val newsFiltered = arrayListOf>() - newsFromInternet.forEach { newsItem -> - val anyMatch = newsBase.newsListByDate.any { newsSourceGroup -> - newsSourceGroup.itemList.any { newsSourceItem -> - newsSourceItem.date == newsItem.date - && newsSourceItem.title == newsItem.title - && newsSourceItem.contentString == newsItem.contentString - } - } - - if (!anyMatch) { - // Check if date group exist - val groupExist = - newsFiltered.any { newsGroupTarget -> newsGroupTarget.date == newsItem.date } - if (!groupExist) { - val newsGroupNew = NewsGroupByDate( - date = newsItem.date, - itemList = arrayListOf(newsItem) - ) - newsFiltered.add(newsGroupNew) - } else { - newsFiltered.first { newsGroupTarget -> newsGroupTarget.date == newsItem.date } - .add(newsItem) - } - } - } - - // Add to current cache - newsFiltered.forEach { newsGroup -> - var itemIndex = 0 - newsGroup.itemList.forEach { newsItem -> - if (newsBase.newsListByDate.any { group -> group.date == newsItem.date }) { - if (fetchType == NewsFetchType.FirstPage) { - newsBase.newsListByDate.first { group -> group.date == newsItem.date } - .itemList.add(itemIndex, newsItem) - itemIndex += 1 - } else { - newsBase.newsListByDate.first { group -> group.date == newsItem.date } - .itemList.add(newsItem) - } - } else { - val newsGroupNew = NewsGroupByDate( - date = newsItem.date, - itemList = arrayListOf(newsItem) - ) - newsBase.newsListByDate.add(newsGroupNew) - } - } - } - newsBase.newsListByDate.sortByDescending { group -> group.date } - - when (fetchType) { - NewsFetchType.NextPage -> { - newsBase.pageCurrent += 1 - } - - NewsFetchType.FirstPage -> { - if (newsBase.pageCurrent <= 1) - newsBase.pageCurrent += 1 - } - - NewsFetchType.ClearAndFirstPage -> { - newsBase.pageCurrent = 2 - } - } - - newsBase.lastModifiedDate = System.currentTimeMillis() - - // TODO: Remove here! - fileModuleRepository.saveCacheNewsGlobal(newsBase) - - return@ProcessVariable newsBase - }, - onAfterRefresh = { - // TODO: Save here! - // fileModuleRepository.saveCacheNewsSubject(newsSubject2) - } - ) - - /** - * Refresh or clear news subject. - * - * @param newsfetchtype Following NewsFetchType enum class. - */ - val newsSubject = ProcessVariable>( - onRefresh = { baseData, arg -> - val newsBase = baseData ?: NewsCache() - val fetchType = NewsFetchType.fromValue(Integer.parseInt(arg?.get("newsfetchtype") ?: "1")) - - // Get news from internet - val newsFromInternet = dutRequestRepository.getNewsSubject( - page = when (fetchType) { - NewsFetchType.NextPage -> newsBase.pageCurrent - NewsFetchType.FirstPage -> 1 - NewsFetchType.ClearAndFirstPage -> 1 - } - ) - - // If requested, clear cache - if (fetchType == NewsFetchType.ClearAndFirstPage) { - newsBase.newsListByDate.clear() - } - - // Remove duplicate news to new list - val newsFiltered = arrayListOf>() - newsFromInternet.forEach { newsItem -> - val anyMatch = newsBase.newsListByDate.any { newsSourceGroup -> - newsSourceGroup.itemList.any { newsSourceItem -> - newsSourceItem.date == newsItem.date - && newsSourceItem.title == newsItem.title - && newsSourceItem.contentString == newsItem.contentString - } - } - - if (!anyMatch) { - // Check if date group exist - val groupExist = - newsFiltered.any { newsGroupTarget -> newsGroupTarget.date == newsItem.date } - if (!groupExist) { - val newsGroupNew = NewsGroupByDate( - date = newsItem.date, - itemList = arrayListOf(newsItem) - ) - newsFiltered.add(newsGroupNew) - } else { - newsFiltered.first { newsGroupTarget -> newsGroupTarget.date == newsItem.date } - .add(newsItem) - } - } - } - - newsFiltered.forEach { newsGroup -> - var itemIndex = 0 - newsGroup.itemList.forEach { newsItem -> - if (newsBase.newsListByDate.any { group -> group.date == newsItem.date }) { - if (fetchType == NewsFetchType.FirstPage) { - newsBase.newsListByDate.first { group -> group.date == newsItem.date } - .itemList.add(itemIndex, newsItem) - itemIndex += 1 - } else { - newsBase.newsListByDate.first { group -> group.date == newsItem.date } - .itemList.add(newsItem) - } - } else { - val newsGroupNew = NewsGroupByDate( - date = newsItem.date, - itemList = arrayListOf(newsItem) - ) - newsBase.newsListByDate.add(newsGroupNew) - } - } - } - newsBase.newsListByDate.sortByDescending { group -> group.date } - - when (fetchType) { - NewsFetchType.NextPage -> { - newsBase.pageCurrent += 1 - } - - NewsFetchType.FirstPage -> { - if (newsBase.pageCurrent <= 1) - newsBase.pageCurrent += 1 - } - - NewsFetchType.ClearAndFirstPage -> { - newsBase.pageCurrent = 2 + val newsInstance: DUTNewsInstance = DUTNewsInstance( + dutRequestRepository = dutRequestRepository, + onEventSent = {eventId -> + when (eventId) { + 1 -> { + Log.d("app", "triggered saved news") + saveSettings() } } - - newsBase.lastModifiedDate = System.currentTimeMillis() - - // TODO: Remove here! - fileModuleRepository.saveCacheNewsSubject(newsBase) - - return@ProcessVariable newsBase - }, - onAfterRefresh = { - // TODO: Save here! - // fileModuleRepository.saveCacheNewsSubject(newsSubject2) } ) @@ -316,13 +118,16 @@ class MainViewModel @Inject constructor( launchOnScope( script = { // Get all news cache - fileModuleRepository.getCacheNewsGlobal().also { - newsGlobal.data.value = it + fileModuleRepository.getCacheNewsGlobal { newsGlobalItems, i, lq -> + newsInstance.loadNewsCache(newsGlobalItems, i, lq, null, null, null) } - fileModuleRepository.getCacheNewsSubject().also { - newsSubject.data.value = it + fileModuleRepository.getCacheNewsSubject { newsSubjectItems, i, lq -> + newsInstance.loadNewsCache(null, null, null, newsSubjectItems, i, lq) } + fileModuleRepository.getAccountSubjectScheduleCache().also { + accountSession.setSubjectScheduleCache(it) + } // Get school year cache fileModuleRepository.getSchoolYearCache().also { if (it != null) { @@ -379,13 +184,13 @@ class MainViewModel @Inject constructor( reloadNotification() accountSession.reLogin(force = true) launchOnScope(script = { - newsGlobal.refreshData( - force = true, - args = mapOf("newsfetchtype" to NewsFetchType.FirstPage.value.toString()) + newsInstance.fetchGlobalNews( + fetchType = NewsFetchType.FirstPage, + forceRequest = true ) - newsSubject.refreshData( - force = true, - args = mapOf("newsfetchtype" to NewsFetchType.FirstPage.value.toString()) + newsInstance.fetchSubjectNews( + fetchType = NewsFetchType.FirstPage, + forceRequest = true ) }) } diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index 2b068d1..0000000 --- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..47d318e --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,611 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index c4a603d..7353dbd 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index c4a603d..7353dbd 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp index 09e02e3ecd477aa3763e2844e97b17d5bdd25cd5..6fd3463c3d1b0d6345b485317845030285c85b59 100644 GIT binary patch literal 2140 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 o0Ff<%4+%&@N_#KY;Q6EnA>UgdF#+DyBecWM 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 GDF6UjUkH`} diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp index 904b62819480d38b5b87a12d235f2df8d3dd577a..eb951cbdc21de8bb9187bcb7cd1acc1e01942914 100644 GIT binary patch literal 1228 zcmV;-1T*_mNk&G*1ONb6MM6+kP&iDu1ONapFTe{BwFW`9&E}8&-60|-fUDMJXt=R$ zE6ZM63@MaANJKI8p_z5y4zaf25ZU`&?>9q!kl)?)4H1qb+cvFd+kWrcQT#uB>3wEk zL}d+i>Y#1gHX^cjs(+BBZQB`1YwcNqZQHhOn`OJ=q-XJZ3`0Dxtq z{QoD1*|t~AwpX`7l>}k{0BPGa?or$$ZMXibZQF*SS8pjtU{@%#!jOH~r%(bexqh1f z6kExZl<4gV5H_bs%2+;0cd}<`{0m`?Pi2(Q3-bjd&3$0$@XHPVNWg*U=Uv9-wG`sR z=pNn1_IdNhk!%Y$%U7-og)vudZXX_%>e;k+k-?!JpwWo+-A5?uI-+zvxN&2KgO_Vl z(K2Qx2S9X+GJ6{zOmUX;N82`3*Ak7d$!zs)q49)P5MbL_0S<9KyV$*O3)E zH5#%0$n0#}6d)p0+U`CswA98I9=pzAf0PjO^N8pPl*AZ-i)3Gi(r)a(ef#?LhP%&q z%1-RRefu`osn2}_;E`wEhFa$EU8tYAl;FJRkDY#68;Q;(cpF&Phf1tgB#wnEOd$pIl&_+=|2dc`ZNIK(!r0<{l&yyk5ikxw%^TYW7i1GBM^ zuRxKaoC?KtasnuEvI~bNo<2aV0l@5_R5@OS<3VDddJ9BlJB|xrZg4a^BoyU+)~qoM z!4arsFq!}k^(xTk$c_*+08hEc1K)faED(yF*tIwUK^V%t*Q3{ZMD;-`WDb8506E5P zSjPb+WgMk?1O_WicHCdC;&F0pMt&D|JF(x5?TfRQmWh)_{kx8t=3|-AhAL%56ihmn zkP(wH)jX`Y1d%Y<)a9?*XX!y^S(Zw$$-x**(n5Os`)`Z>zPyk8Mcb_hsli~pTfMm( zvG&#W$hUkmzT9n({NDfk%}fR>lTbJoj1b-FhxpK>ps5H~>Wz2n5N(|!31Ot(W1WVG zW5J5Soq0Z)41C~c-kpAXkPai#=+QHTb*CQULxaMm zB0SkA)~=JR5Nwg3jL4+8aJK(n$7B%R{r!2f_jgC)8?N_`#Z3S@Hut1&SL*fUFMH(6 zN?~e5ZvOcXO;uV}Q<9qhKVfzN literal 1376 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 literal 2506 zcmV;*2{raoNk&G(2><|BMM6+kP&iDr2><{uFTe{BHHSbTIe+dQB4PqsD!6FrNZY^I zQvRF3*5JATcHj;xAg3mTh$6Dk-Q69=-QC^w>e2502i>R7y*v0)+})iOxHWc`aKese z3HTBwnyN3HSpsXYyo*KX3W&SdMzaBOs9-)3(aCZQsa7X4|%v+57<8 zH|Dc-oil#6-`jij36NB6+wwRLXNzTKW@ct)W@e_)mp{z;AjZFE^xOh(8E1lhI>NxA=pwC%BN+iX|jXa2;`bRgSS+NvrR z4-PHx2@-4nCjfa%6eLO#l>j+|LSUnsC5jSd=q1_`3yH(b-=;(F#MKcy03@!GQpvQ& zW0fnFw;G?{J^7=Ccg}hofD>0)0hlf+)VQPZT>}-&zVPj(5-p%mHFf}7QloO=k}5($ zHtCVP{T3UJT?GLk3Dh`rsUj==k$Z`>L~D1Vn^5)F#3q& z$%q`Rb4$3~8(qYJg9-}Wh`Eyjdp#eb!sUsihf-_jiQ2D3TS_0mi40V6n=tA>#fkL* z@;(ZNgo0c=EDZxGhXaWMkm3$46CDF2s6M`?-EP|IhK&~2!$cE>VT0AE_Go1~?1t4# zvv7M~H1f+N9slrI9K-#`UpQ?fA;3t;73dnR9U6J6#yZ$zLWR0tNY!K`hq>sIBCgIarNmIuG=R zvA#Kfw(LML`v0JIntr8H|EiZfu8xf5*}H$F*3I2S6wnML9K}n4GH?jmNv_N}i7CQ- zz>5AV4TaEO!E$Z2H_>_P!auuaz0x#}fj9`_V2C4C>@DGZ*oZV3$IY4;0G+YQ@W$Le z)O%5t8HyxgBX%V$OF>WhIfNUXK86MNeYgx8tdxe?LoAi>vA{Q?BP-?YCfEb*amw<^ z+$Q6WSq{%uTGdyDqmHv-w7(kq1X7+bG;B1ow~UCE`o*v=(Lb^>KJSP-&;*w}4m-60 zO@`qq#;rq{We93}Bk2KfmDc@M?lk&3HpL6r1KSjD%^7JNtkB5 zhy*GuWF@>?4=11z1_`drz3eWI23~ALf%H2eys7>OzIXGhXMsc*0 zd|7JL9^^p8p&SY{AyF16k76m0-&4UKvZ9zid>>A`WditU!ep0pV_l%)6l%NDKo#Mc zfp8?lMcC+6g5g?ehITo5&mSp}dS9ksf7)vc0XD-s!&gRT3P|nOpufmFPx|O#-iurv zCMym9wP8Aha`Gr`zbWR|;D50>J&^d}B)+Xh5Dr0Eq5y&RSRT?cQzk_3HF~$Rry&2f zJ5Wv4-;oeT$T%6oW;BxbukAXr$IZNV7Az&P*oP+qJ=kz&HKp+q&(oEC)J%MCnKo_G zb3+bUR*#c7k>NSK%vct%Q#;G^Mj%JLym@xc!hI+LKa*uukN*>=t zty|VKgq;}w+Cffx8I%N;t=LF|ME1}BBGD)y3zq1&lb~>ie%Ftxm5OR-d_2KZ*OL|$ z9_PL0^KZNB5Vj{DZRbKE$STWeB{1etC}`mV2Oo_onw3s1)J z<@yt2^FI{=^_miI&A3;^yj|embW&*e6iQkOwrfm1*@U$_!p2&x*?jOcg zFAi&t`bOaX-z$%-jv}ftb_6N{-T!7xnG4^=h+Leghj0FM%8dW+u5n^iv)#}G)w60| U5oif?p1SsxnzJM-4L0%y0Hw{?(f|Me diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 0f9f4ba9ee843cbaae1975feaa94bd10870d4d57..8b30a1a600ff738739c64bc5210ca74079989ef7 100644 GIT binary patch literal 3076 zcmV+f4Eys^Nk&He3jhFDMM6+kP&iEQ3jhEwU%(d-^@f7BZJ2~V?fnsmhzX#mmDsTM z9ulf8snWKoV)Nfcm`UaeIMJNgOf<>(xOfH~#mtUmCN2}j%vjz%zucWUTU5WTVyB|x%3!$@c|1X@W)|0jSButp@J z6nNoSEG&=`7Z#);ng}r{MkOkAe)G@~qN|b?2C(LwJ!UBykwUNQU*bo0z;Ng$P#JHM63o>>U5ZE{WpHkvY{=FTVixPfO zDULS`_B+KuiBq4Zx;qhM`X#bJO$vUPVt=Z5u-13XFw%k$AnMHgu9AUAy63qVAge4& z>A$CV=fwb3WK&B2Bi;|Fy)(B*nx|TEv~ZGFy1U0*cX5)T%C0iiLld;-vGnDRF4_L7 zerS{@Gk@kaDH%l4{vlz4_Cw2$Cl@AhaY;#t~30n zh&met(}blje|)MQYSXW92ZR!r!=g>g8l1Gw8?w6@K0bc8#a78oU!~!*(!Wc?M`ir_ z#0l=Q#95NhBhPwz=V2t5VC7sEBgC#LEqVDX&zZ!)`4POchirK9EC!|ztNG`>z~td1 zKdip9vi0zqHjA-fC!0ws_Dt2danIpX9GQX1flKP8-QraS^R8R0G(u)A+;_ zPdt%>#wVUAC463Xbjv0rga-E7L0?)BMF)Do7XQn9ZbEEEEOczD{?-tgwzvpo z!H-Y_-4!?@GotX21-RmWjuYmHtO%=uU2YmA;E3zXeSIMzaYMxOP1{y33w5iF_#`B1 zM_5kyKrfI0gLBhVZYLok&<$3O)OxV)mzIsdGYAty{ZoW(} z;t-BgeN3cikcE7ZGB$#im#U2PRi$eNYcsjkhrJ~Dp}N;2f%xz5u|*Z|#n{mhY-_pH z5v~A>ow7+0Ge85l2%KHC#kpUqn+Pe4OjuNAa%68&)`h*5T#Sh0ZMS6snkMCgL<^JP z7MWWE;#$!Lwrb!XK?K{2!Xj){GV)S$hbOD`^2RChLfc9rEAz_W#%K)y%9s`B#}d`W zJS*WmV9CSj-^!EGYN*NMj<9xy)lLu+JOp&hmqD3Py(lbwqOQJCmpc6a!0FzK)5+n` zR#O$^!GH*0obDTn-RMZ@&KCC1C{AbP=@dA)f;ODp5|QOaY2rG8gy=vkK-_AS-C>7E zw{g3#coieBdaV1H7>{xEzcr-KDbR|LVU>M?f{WpRT`1 zkOWo=PJyU`H|%Y<;hbZwKk8@2pG5Vm#2s!Ym&K+52f_k0VNwcb!AOfVyCva&C;j(F zvDbjfBUC_`Qn&K;~}G=6cxZo4hcWV-JUr+4YTN{u6x!7VEq@E|Be(^_bG z!=!quodKn#onUp|nENKJF>C{Jg9KAVjisBP4Z=kT+1xEzcAG_IAzS-Zc84XF<>3d{ zGk|8<)>+P>hbwn@8mUOLmqdOvbJF|mSWio_bikHKfMrEjMkJ*y__O%V_wNYihk+Kj z!#%R8QMQ={3)x-liW2#0I+V-H?DiAbzc{^{)4kQt%IVR3-BuC)6dDYO8)sW9xk~dw z^{OJydi9HpQ&jKB4D~H8!hz3*)k~~22^LNdtZP1ffa#C+ql!2mj@));$!r0yFm?9d zb8Vpj4Q>MDF>jHr<-t$RHLEB%rC6q#MzB?pZryLo?#9r)1!qFAaJ;Ek>AYJ$6=!j9 zHXgK}z*1~9vK4GOX}aR)Ip|6>#6&t=4VH*3h}M>}mfS0XD<^$bY3G2Dq@e%-l2FVQFl@+!y zcMavd_(un{Z)SQ5=5|X~&DKpLITz%vm7D?`cM}Kz8wypQdk29?;Cueh`vAc8yd<~f zVP9P;91%%sUL~#m;8c(#DGzjr(s`L1r#mkJ0Ncx|4hvR-kRS}a90V5vfL*rC$NY5N zS`m@)eG!oHVZRe4gzcQ0_hG*7nsNt7kOWqk$2)-Mc>(5LaS=!+9YRB1l-sGMeZRe2 zI>;3bx`=B^zow|Ih$@MFgwl_Z$@LMq$BF{n1$bVX#r0*4hLb~b(dU29W^4$N;UQq2ZYYswkSsgGPPr)$HaZYM26zYgYr|UA6xyW)V|f|aC^+3JV$Rdv-V&0pbod<+>fEQSPOGk z3AP^jFj052O*=yA#&RktY*A+WNV9p(M_+gHgFWK!BwnSc3xf_Hg0R0!v=P02E<~YFECB-%)I^f+e1~q&lXXEnsvsV1kJPL_Fuz$#cchzfItU03TolPZZcO% zaLU~lK@!~#MO}9;izEM!9z>C6qc^}*XE!u-Z~XRK(P|N{){vb^x^H4`mFbte9QF|q zU;j}^_DrVstxUiId1mIVUWA`ZcE=>DA1fb%RPx^8>X|65mIU)n|5@Au0P^FWR!u}^ z0-Bc(LC6jVPlUAUNvoVmlR{T2eh$J_Je9zLo?b*&Uwa z4lI^Ay>S|VP$CUkoY z-kPo_h|t~f79?ojODfUF*j=xBlwJY>09+RRPH#jaKD`!&%hj$4AmQZaSIQ2CA0&I@ zy^7O5;=EHm$>$*9=gumQ^z{Mt__cU_j=hs^hallUl&4yLI9NoUno{jvLH}J!iCrnD z5(6?ajqIqBu2Y;#43X#DINQ1!uwPYtGMYxNqH~!j5lX`Vt+BKVv~Tu5rNmwTAI71y zeDD>C0Xf*+KHCW&_|Jn~f4YCbem*QTEPVYyiN%TmB9siGm{j9p6s8aqXpBMqGCKF$ zkMdlM0PV~MwPz%^(lBkkbUr&Di@Lt>P22TDR@FUv*OBv&Bt8e6i|{U3%|pSl@N`&8 z=T|S8hyUNbc;dMX0wmz=cqR(afB}rUzc6>|+mCsY`y4PpvS8WrZ3I?Aol`fK53M{W S>CevrdPVpD&ON6CO922I$>&)B literal 3696 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 O&~EMj9e&Q^O9235_-pF` diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index 27e50e494036d0e50eaafbe58cc375b7f70c1be3..4240685dd489a18c65f9f5ba2bf9a9dfa5012952 100644 GIT binary patch literal 5604 zcmV-j6CO z>?G_gRL9nf`fCez4SJ=v@;;QHZ98N=?`Br9ZQHh;-M*cvCy&@C<*`&L$EdK zQc~oWVd%zOyZc`M9k`98NY3)^G2at#%0mPpXpmqI3I=_-8bN{wG;|>?5D4Zn=Sw4m zgOAWeL9B=!aawi#-EM!=!L@|ub3a%qG`KE*T_X1;#jVCdSx%ByP?@3x!A!?8Abcw69wK7jYnV&9+pYD7!+lX82H$MXtjpe}XpD;|%%Bjy*Pbx*|? zKqNZulC_s{pz^Ckl~1aGgY<5sOm91n=nw-wsTV;cV(zfDk8xmOEPBbrAQz35>0!$M zhyh7^mp~+QtksMz8Y@ep*hOV}D4C16gmj%j4T+L_&gdbkESc0pWH$TZs0F234Ur^T z8<_8Q+r1 zM1=K*ccuRJDe0WZ-@npM(qBKF{XXMBF-OtRv=EV~q@iZ{F5KcQ$7YS{+_2*xyYQ9n zNAt_#Th^9WE!z*D`Hbq+Vv{C^lNQ^CO#fcR*ZchB= z*DuHS5ZK6sAZdece)Ag#x6(c6B<=JWUga%64F0uD2%I*M+|J$1komCzMnU1Fc{fu9 znQPpt?>D4+vr2FIk+XhHVaRWuFQqZzbg`5=zX$F0&MLi-@+~_x-euKGYIr3QCfYB! zAOxD@83%$E>JFK3-?J(&X8Da&Zo{+ott$Vr_Rbpn=Pd0cUh&I~znuJ!6aTq|w38g4 zlXu<(H`2GR%0f|khBNs0Endm?BcoMMWfZ*w~kTP;MlZ6DuBY=6Fa!`6^B&Sw>x%0XyiVItR5Ffi3CvPcKgfea``PPys z5?~?%&^Rn7WLhMkAQ)yAa+Nej7Qn$uPEGfv=(5_?ZO1%78ME~E{|LqV!n(3fO(Ow4 zx2G--)fqpk8;$Ea6#)P|f?7w{;p1F%U6+qw*JRZL%3M#N8IcE>AcM_BUkT!v#9ekY zkX}40NTY*z+gx=pF2R-)jley83mzb9%GI-}3HsaStAJ^AkZwB~Oarm20&yH-u`Z4Y zhtW?On#(e)mg}KrRuFs0rXBesw3J!KF?@U3ZY+i^M1Q0bB3p=Ual9@UVvli&Z9{(X zbdlL&R3XylcN1BDv=ZBkc4IO8<2YSPY+-ac1G%M{#+X?)dGBxz7=pX- z8&T&4mLN5(356rIgaSna{8S6_w;c!Don;g72nzJT1{n5H3VoE$FzR(9;9|@~YOYIQ z38?`3W4|1m^RR$bE>}3}^cVoLA;UaYI@T57_aX257EgYlad|X?5De(ys3}3wbh7RN z?J+6g+#kM&3H}zW#;11Z?stLJlSSD)IXwmuJCN9k$+2LzC>{MH^ysX^jF>Im2I0^K zJp+ZQ$g#2PJRb8b4+q3xu(F|$?x>+Tec^@hxMCV36sIRU%Jzb7RNI6-1N)USy>T|Y zeVuF^hq^ix))m8Fh}<5}o^B)T?n$s+KbyAV?Pk26mu{mc=U{4iwg6O&JYiWY5)3-? zXP-iV@rO+&WLwCt1^cB_?GO}8gl{7vl;Re73|T)L+pu3boA3bguz;o~dowtXmX(Jy z|4syw5|=n)kxH{U$leO$iKZk=;m>D>MDvV*lUD|y;g7|9ZH%5aps+mCUdj|FmRGsi z#gC|yOs^VOIBL)qSznFfVOr#^%Ewx8a+Gk6TIqr3mfpPbEjx0)wM;nVwPad!Oo%)l zAo(|sQC|V(^EtNDskkPl@WH;nFk2@f)=ap#6NT;5@c1x3FWQ@pTY4!^4K8um;%VqH z4c!v~0GwQrq-}YJDWf|fu&S>P*^y_*AJ?c&I>=SLDLMBL4OuRMiCa2VV;`8q z3)`4thRy-rwz+%~;@8PEu22XU(KL?`Ch1(3cWoaGXztrNs}mOP&YM=`PP80gThY{l z$k#mZ@9RrT6(j1%>RMRWhT2A!y#)K`0%tIBrd4p0?*e`PjLiHwtQdw)n6R0<(3Y`jIS zmuJ@SDlhMm5Ug5V99^tTu~c5b0G zzRMq-(T?k-B2ZX1fTT^?EN(}|iOq={6oLh(@V0zT<%2}^@k6u(Sd&i!aF8VG?%2F8 z<+6NC{K_4IJmKKnWSbP*aDb%gllJxOZX_l6OgL4iubjeeJ4<>jo^JZ@5y}5--$P82 za+omXnc>&&XqLwf+%Ox)rq<#Kz~Neql-!4=N_r4(_pZ_#Ju}5Fd^Fd4$!5toC~U~b zY?P5+(X+wO3ylEZlX(Ywg)`t_PubRh>nL9RfKz<`?QiVDN7PE5xhl(aQ3c~_d<=s^ zdThc;@%*ug8y7og%H@ZSo&Wre^Z$+Jzp9HPn$psSSziaz*n5mRm#|@wz9 z7E-xHuV!Ad_H?5kfA@t0V+o)p~qLY1F%14*|RIB^vHD1lxWZWJg_bIR@F$!H61ql3FWgm>0sQ!V^z6qx(vI! zhuXc5*Mfj!=6sQ6&;U-bK`m zE|pO=HWycq_YBhaU`^l&YAmhHB(JxEn@$5J7S1)v=atkUHibT_wrj3>meV_qH} zk7E%G1;n&`N7=2JjT`gfNG&`bbw}AubejlU3IgNRH*9HWcKX#>+NgyU!3nyoM=23N zkM~!eDc0xxU~DRY-{kN_3YTlxa%*YTtFtQcmB5cjpIgXQX($Abg=Y_;y5innF<*J*Yf%3!a!zRa?F`7n}}AttDlGzZehneS)p?DKrI(y^H}5< zh|SufcX*t4u&f()*Q`M?Nnb~v3XefuxdBleYvd?J%Ogh++9}mC=bQ8x3M&wuU2yVo?O5oPv zebB?x2z9&cG-7 z0;dfzwWBl8bprBy8O%l3elRkr(XKb2E^N(hqPSi^64%Rm5SZdEYw;l)Wv7uy?eZ36 z7Eb46l(m7dK)R7k*wM5bpQg+sYDGmC_`kd7_?c*jo0DC~nHTYT)))63B@Kc$6+@rrSW6jE++7 z8+5dyqE*Vfw)dU_f~>!+Ac{_pD_|N8BnCQ8Bb&pMRuGm?HIVL0ssDpSX$7^>3J$zU z?ujE4Wt;M@0>;JIOx8zM;A0K&W|spYSeZyj_#F7p_z*EE0rY5lt#IzSr;G=?Bk!1y zJd+V1>7@)}SIfq9$cnHBG310nPab>>+5yf8a~bcuMz}RKK(|abJ%JRNXRXLgM>k(U zP=r_XEYR)Bulpc*-%TLf_cC`=l_~zXt5hw>Zo<201WAxvIwjr4BZtH?D2A$!-`i-& z5xmR?1jw^ziUeSN-nT%ra5e`58WLbyK0e(Wpj9{q#i42P!R(Ih(5?;o&*By3NRi(G(gNFvm+;3J~vwi z<73%}bo8FLZfZ&E{)FbrVzaggralF9zTv}eIegv)aD^Q3wb@~lU;61c&{Y|q+bkFDWJduI*u=YRjI z=GVWzg(`xpkJ?<5=XK8@U$s!682cF|{U!?~0xkR(+DUQq75WkA{}@=87xcYr)offe z)%kW6%H&8afGDB-W^j!f&AC;dIM;9#wmvD1$y1>5%rMM*P@aemv_gWYzeO@rnrVyN zqao`-7>Xx(=Q7R-6fD89=H`*o$o8RqOVv5;9R0bN_55Ve-nv(99*5?^Md+p|mR2*M|Da;q6lJN`&u|*@O$?_70kC1 zSwH;bRON*@9Q+JAssVa~#K+=PSB2%PXXeud7cl>&j(=xI?!17NPiQG{t4@2XzfIs> zNOr-q@{twCM-%^mGCq$Fe4y&`k+x8OKmV#=!lqf@BTH#zvp}C0H+USKu<`Eo`eti$ z8EkqV=tAA$!@W7!R3dStC}&?0+{5rNQk3N2nN&!3r2GeBMqd;{HS_rzk+4wl#@s5UvNpi5~h6lQaY6yDYnHSiU#-}kCzhrr(NZ zq_AuROfN$oV9WqdFn{!BQiuE`kictV|%>H?a! zeDr-}DbLp@Jb_&dB+mJj%+TxAn}0LVxe|~dDZ+WYGTE#b$_-TK3r&lLqfS1Ub+s-#FjXLr*WXVx&5YBv2weS|l;4rqHUYG|{l{7a2D%d9g-bw} z@XZV!q1e7!nx=`csVG2pxW29@v=E25&GYwwpWay(aCC`L2MwySea5VbTT~$Whl!>+ zW&b(wEd?5fo_Y;WBSmSh^s~q^!=iCX>&ghYuWFuwt_5i2Vz6v`$)tK34f&Du@JZ9k zvq#ne=ps40sNPufVd#JD{yY`hm&OPP7Zy&Idl3#X#<5xDPHg+ zxQz6335J=Dl~1z6O?B~j^{emAE(e;XIaTl9l#mnLaVcP4Uo7tycXw7UQk48? z?IXuD<}v7BaQ*QU1%=9AhIna{`v^-SX1(uqe2Zx2U1L90R!wN zuciO?qhJic%aMb+ifoYeb=OO94ePi5l3%yNwh=VVFaIxWne*kRYhcZv4n)1d9ao&K zcX?p$Fk$P^-%WT%pa;|QC~TTnN%r>wr`Wyk=cuFv_0#bNib!{S3L5YT`IQ-1v>9aO zv0TnlyyZM=Ygmc?%@dq4{3p&y=d614qp`WXIuuBGu{OrnUD{Hk+5H3k1@HYny~Ac= z(h;RAK?`mJy_^`BMIXn)L)@ZlLg?cSxG%8!5jGQxmm} zqODQXiEgj*m>0z(m zK#8OPrx;~{x(d>2>JEko!8z6WRbDrw9`FCB{2WFJEsT3x$A0m(g3`&j1Nb#0XSq6> zKBfqF`5~|I_$J&Dn*LQje0=F;uBFYL9Rc52m_$)u7c4JI19rbv6x+>(M8y+8PGAJ4 zdqsW4VvwEw_mky4UQu6}PS-=>mV>&!SV8=6FMN3B*Ss)kEAid}l_hFlSli^=!shRKAn~E#Ky*|sx8j5l`U461Cv0ix_N#w`K|rJoL6|`4RnJPAiQ7fy3->j zw~``S+}7vgk+U;(PR#CobtK44=ZVQTgu#WAvZHm3lKtJj%RK^4JMH>~_YU5i?x_HM zTVdvTX#+2qZcXg)!7a{+W1-lJ)z3pHDHNlGm3V3I3QH1VNus`)Tle@_)rijq;W2#N5WAX{NzEea<%w4Dc z2F_<*`rQ@<%RgMfZ=joHDDf+J^AZ=Wdbnuy2B6#N1+YJJ^UXIu^M#$>*oZQMZ-VNRJXaS)~NjlCS#OQ1Nmp82~BlyDC=c#9ND&Mb^dX@_ifGo z)J6)n4F{4WnRYav`E47I(r8;d-e^9i{o)i!9o4pNjcwbuZQHi(U$s59ZG5;8BuQ@D z%mO;#?hqs!pu-Qm{r|r@akgy_W!!*qSKO`c?(R0Ob$54n_m=gKtd&4KB*d8nl8Lgi zAih@Uwr!BWKM$eZ?yI8pNPA=^ zvKP69)FUsD&&W^Y_fc0KhdduQ_wxDk{?t!Iej#dF47*00kuJzqq#F5wKm$Ukz)C`h z{~}fG9oHM)c@t*nEkucQKn^2sr4XE}2r7Iom&vGzx-y0>7*hXCQw~q>ujyKd8JUaR zOM5bzqKd?A)mF@8(vBNuBIc%P_yr=wf^0y(NTDLB{I`nX47#d~i@`aFg{a`Wy$BJa zMHV7orO4z=9tVzGhKuX|KcA@Js<)X4>54p(BA4N=D3jwNM7j|*yXTx15u!(qN^w<@ zyQbn*Tn3XUn&#GOL`dFYlIOUq%5ibJM-;g=hsrG7+s4^wMZ`J4U))tfG%_0?sNhPC zRQ5}uz;iZzx@ne}ZwBqj-tbumV*f?lmZQ07;6MV}h0;uiYh05>ra9vXp~Q3bpLJ(1 zVdF#izr?GjeAAGNZ{ETWNz2wh^}g97R$$Co7DWTs_drd}j@yVvqeQZt1=ir8^qu`@ zZF?85UV_G#+PE|~t80=46EBichK07TN^!_V)sw2;(lu*(7=IJdH0}`AH6rRM-47iV z9M+L2HHxKu+KYk2Yy5etPhMp6stSIJx19cAa!s>HP7~+DA#N5y<9Ahix(TZnzx0-_ zOT$g?8mEurj3sI)NuY655Q-v=s|+=gGv&#PUv(%r`SsIqH!v(|66!KPanQ< z*7TdwlLtfNkn%VSaexp0-}xt%Zf!jOn9((l>muxsu^F!8tP?Zl=c~g~JRK06f4#mDQT+fU8h2uNd zo=u`<%dcF5=Ak^^{b7tVP=i-MDF*?9rm@>bW+Gs@$KqD(5J3jZ*t}QUBEHx{Gv63u`w~`c!!z@rp6tjm}XPOxcTg%heDY zz)>ZR*0>ita^oUxoVg$YspHb((;m}pL@ z=3#xVApr;I2^LZ3MM2O&DK9q0m1zQm@MDD|3^Vu<_L9@~(d(qqbn%IGXRL9W-XGjf z8moKFt*x_8g^iAUcWsggfX5M&4({C0g8PwnI0z612qD;!A{(C)S6fE&l|{w@!njBE z4do<2`aX-Tr;FtFNsEvabD18x(M$}Rn%qzq{ff*VS2$Obrh3%xF`np2R2A@TpQO9B zNW6&SfFO)WZM`$od@H_WQ#FC8k-u*2*kw~)#P(9Q7IHp`sF6gE4Q1cZz3_fOod66u z2&%y@^ky&!MJ1>;9tZ`2+d>1blwyZ?K`y*B&ks`F_Hx`hUUrO^{gdG^)lF0F)TG@_ zPNT`zZzfj=ZD2(*HL1sut@m6PMEim9I6tjtrp;kVe?UC!6wTX4%dYV{Q?=LS*?mPc zg{e2(cj!|Aiz~&WZ(>i8EdWu^d;|;?;(+#3OP*9dAkablfUnITxoqn@*L|rP%lR}_ ztDM*2mPkxEes16QSV93T6KDu^V0V@SL}Y8rFl&6)fP%=f!}Ig1Ke=?)OIS)2;U6pj z0B`_C$ENH05Y?)!A4W*pVjV~MajKS@z-p--na+B*=gDT#G?a*$x(xtMzyi9@h7;H}iSyHSDL1~& zQir1&>tC0r13oMr zhVnyd!!{eYj)yENwMAgMU&;FCSK``D1-E#`EE?wBUc`GR<2Y_C=;2Y2WChrS?iOK$ z@By-xgUnB2_~WQh&5s)|oPy4IY3!V%`#2x~%6aL; zo|_hIL8SYa_Hy?ObL(So|EMHOhN>UFFafa*`qA^ktipZKXaKu%0-$`~+}{C%%1aS4 z(sf~0j3t&baU3C-_?*Kb=_^*Pa!31qQ|X3L>UtuvY8%8I zaERjo-u{#P=a0DZ^^UUYWmkK=at;{4TgLl#l7|2QbH>AX{a%dp1t`;n9jEBX-hXYr z8uFPns>t@zM>7hnAq}ss(Epmx7xU97tlr^-aflY^ZVFchWDrI;R1OtPn@OfQ&})FR zc+po)`ahlTwc&Q6mR-z{D4?U$u~cX$QtZz4=@ah&YZ?uZ)ofz6uyU3f3YpPNO5Q*J zebW@+TlG&v!*geQ@A&0+F!tA;l(v(Y(K-*7A1H@Ssi^yDyZtn71qAEd!)HW!me+x7 zT(O+Q0A}mHgsVT{J|F7A4(tvhQ~&NT4V1GJ2XtEAjw6RCehU4chRf-v0Pt7ksgka% zSVq2gcrxd6?LY$2zhFk%Wrj`}J_QRu>z}&z$Icq*o{{ee&cbK*h?gxQ?gtE*oqTVq zSB0bh8b|Mpx5^jJ3_NV(A>sRqq|cH4j%sRo>jaI<`qFg!5N9&Eub83; z-ElxSnKW(wy(6wPN4u!zoIm+b9*<~hxE}@0%l=WcB3g)aED^I**M&=t;OnOei8qHa zav<#_FEY~tP%LzDbU04a{hNylt-Q_17psR|c|J2-_CG;$^zL&9r zxPXk~cAH4MMI>uU!N0d^N7SD3njKqJ{phYHw!kc)GqNmgrfN(8xE-<_{Fq*G!5ArF zng*QwZgHQ9hca_mGTXJ^_*p9Vxnx_*+uS}ZmU4jZD&p~E-WPe^u*yepi4GS*-g(DivTif&U?9Rk4BhKMy_GZq>Gz7Nn9?9wj{ zd;x%y^XEWf_K}0!1%5*V8H-vR23X|Cg;_qHiUKMG{a)BQBpcFrH3+>!b2NK2n~^qY zb9Y!WXq@jaXfUHuhnF`vdX|0;Cwp2bs!V{t!Wu$7*p@>8|Sz`iIlhWk?e?|@HhnPYsvfXG~aeD4xg@zp`jYg z?5@c)M_2Q*YF{-@WU^nVE21?mlow{tI3WDb5pQSi0vt`#iHD%5qlUAy_VPU6BVK64 z-_tqo6kTq{*BrJ@VWOCe{gUZ{B4QASGe)gi#r`Xrkyb#CUdi)r&0QNs6a`Q@|G2cS z6k8VGr!Za;i9?g2%0znf@}xRQ&0zp+y?k#P?u#zg2S)J+(Kqne0n7K5ODUCZhXJ~p z5L+zOThD6 zne;W1@CCYZAOV1`saZFQIJYAy7|z#s5f&KuE)xI1^jT$%7bx12|gsV~BG*nt~Dhco)$I0P33r zJ0XCuQ_L#`rx5`B06VB5o$3m6aKUpOa-DW7*REHppOl$K>Tf;sd;C#P<+O0B|ZDZzaOW2k>g- ztpmC)%FE%@@=XLcnt0G$%}t9Iz`&#n@;rmP({-(~{^U^4R~-u|1qbr}#RMC95w3MS zaWE0ctUrXfQ`5?dBsW@1#L@zQdrLV`55t8$JuQdN%tJ*4Da>duo(;h|HUX`lYk<5x zx@u~i5C$lVctOO^*ZBy-Z!QwRjS1N~d9jsPrx57{lEUu~#q!m2=H~0F z3mXHlUs7d}O>I4Vo`z5lHs&ax{JT#?o5P&D8?%Lq)P|1(bY+qD2<;qQvw*Ny{dN-# zp7S%)Y$-kk36ZM6cUgY!g_H)cS&wdlJNX;R@;#Gb1X1@C2`M@&n5AE z!;23vfI|i4&VXv`Pa#_-->Xc}g+c0qQn`!{_XmqG4gjg(+ffU+be&FVy#pDb-G^<$ z8DKG9Dcc5dJ}ay3NIGlr0Y1wX)3u zDU2yo;Qfs3{U|m@z5oT-J{i9no;&HXmWWzk{q?SCOVzHnebtY8@2z^O-^^yQeVS@+ zHE|tMA3_iy49JgQ26j&oFG$)_&ihln^|wOh%QJpxJ4$mT-J^o<|B7gq%8*@o^=QJb ze+@Jl^;l$p1^*ERsFbng`n~=ewKuZj*xvm-G>l8i0ENdj-h-k zNL@NPGO6B2PWNQ7;%=6#8G7~*x4zbwL4UPLH!No`{#B>J*MgsL7SLa8{=MKe zuz==k^!A)|9lfe%<@Wx`$>4{(S15Ot`FemTAd6!>6J3$zC#q^{+#VHo^Hh70E*~ip zz4d|sY=PB>M%R8djWd4mr)iqXHZSA#97jl@dn9-p!~dyAOkIFm151iVCs$w05a<6T zYX}p@v7GqB#6M4<<8Tf)es(K^A~fGE@3g)3Ei6psrne3ER>AjNbDwB!xQUyx#4B2x zfbiYJ%sqNM;U1;!@s1OQhtZ5cE26|D(bL#HOus@;63*g8LE(9ZscVqAhxt!93!Y)N z!Zk`=f6}(qON@Jvg{0SU6A{b67g;}XZ8)G6S#$x_(}=3zt}NItbqpz9jzE z5T2>oT)pF{-A|-j{B8fMZt*0R*M;mG>8_ISNfoZ6P4`Z#ZMwnz4fh6E?$g0 z%k%8Qm%78THN*<=HTIn}1Vv0%IvCJCg}5@_SsUX+yAkV6N*7U)FRd)ye7|CmlC;;lqebWJ!E~lz{8Htf~V=JN#h)058UJ>>bC7 z!-XiBC!dsTZXv}IiP!cdmbZooED98=2!{EIze;tN{n}N+1`i0Y(>Whf3O>u`j@j0h zXb3EzTZ4f8XyFRH4Sp6vLO2Vx-GK8^)1LBTqIqHV#0wFvu_e&}q> zlTZRKA7XeVKu}}?nuRZ1kx#CbZY`VvY#f5_YCyhOD{P3)ga{4cL;$@y zI>53irzU6!)_gum96)h86TVqRE$sop@ae9`&UqTbiQnYHe~z0)w!}|_0}OW|a&I1c zDm)WlxNqM?Sm*RC?969V;N`q6d3r};r#u9Oy(I$32cQMq-%Q_;@EQo2(BE}LU{{e1 zPKP1lUa07a(vWu2%sKA#Z}=LDD}^WqTsZ`ctSZZSOh2hfI&{M&#*FIeOWP?ZPP&ukp4m zjb;Q(UPCAc-{UqAG`&9{Rsqx<(H^@1A)8U87h{JGgI0(B_~O?hnm9+P^k^XhO)P3+ zQOS1aa7qDyCK11;QAs0Di(%X1w6_20S8JlWww zEEu$4zW3ylPyYGiRf|TjEZl>{`~cdieMT~Dk&HB0KmCCLFO(_pZEgb*XAN}e)ky)^ zJD>;5K=5}sJ{-tBCPzLQ3`j&eB%&=kl92|#=t#y}tdnZ6QRR)TYZn|aoulCw!s6c> z=BG-$wlDtev(Huqj=!W--Km-TU_$u@*tK&(GBzR+8?ZtChpKZ5!+DcK!529NgaGD~ z&eyBMatj>J0s=qA%Dwqtc~4Qgs)W?K5=X5oq}CO5m2OIu?F&?0Q`CP$a4U8H!Z(g> zABFj`S@>2uM=8UDn8A?j?CNWVl2wJ&x<=WqK;t)n?$r>y-TD7M)&hZN9bvAHf~#S- z6ehs`WenVs$K^n(@SY0XlF|FOO?dJ=hIId?GaYrIEQ9(c7y;i~k?<|f9snAEhNvL8 z*6?}{@%95>=KL*5IyJX6nsjxe02CmpJ3zmye$O+kyM1xVY(M)=EyRz9dAFv=3gFs;o#JO z+*8~IC~t;)3lR7~vOmY3?Rhh$5l9WbH_zF{#KZK#w|e9D;V=N&TPZ8j!Mucr94DK$S4TCy5W%wadO0V$qZFu^#84D0S zuIH#Lv$JAq0tEn*9lQy~7QlZ2f^SXgD;fVq4sJ;0E_@h;(u}fbbjEAn6`tK8(UA(&pmM|Exjc4S$?G8whB<;>Ocf#-aE4u`xBV;$YVC z*`?6l1jEg7y9vgLfTg7e@M%5u)Kgn~TK+RJ*FC8#d;F}x)EW&!=L5Fc>sCj%D)xhu gbPbL@?z-&t=Uc``lTaK405x`&uCZf-Aa*ZC0HBt&00000 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp index dfb188dc54d640c390d7ec81cd83d0c55faf9a83..4b87a9023a5b6cbcad21956e066904988e4f2556 100644 GIT binary patch literal 5264 zcmV;B6mRQNNk&G96aWBMMM6+kP&iC`6aWA(kH8}k6^DYhZJ35X?Cy_1L`(qd8mKES zXp?;9J_Am1BSuZ$+P}8#lq}~@Id-E?A6sMFwr$(CZQH1-T}Io+sV>`9ou8_zk#)Y{ z*HKS)c2LP1+o@SQW@qA5P&2WUL4{|np4i5z*tXTNZDVZD^jfjK!$C{5Z98ov|Nmcf zI1MLFnK3CdGfW!Wda@*2EIo(;0HkeI1&cdz#YrG-r~RJ*y88c_wzGc|+jj0CRk~b( zZKqFVr?h?Qpie5>wq4uVYfYRp`&koXy4brv7ZBU2v{!7~wvF;6t!rBoPgJGKK4n}x zuirW*0FJe7$DFZk+qSJG%*3ivSu1d=WG^`>rw>cp>Bryy|L+9QX4E2Ngo02d|7R~H z>V?pLQX>9b)k{fMQA!aOqDRb#N#c+GIFC}gN-3_;RmdE2iaaA7WP}VQevebMf^5heePhK>QyGPjM)O^c@VZ0W+=;iGs;?3xa_omN1Ee z$kkT?7MZ>_7!_gQL~y=@&k>-JM|n46@*eLkd7_qzI6U;7PvCO~U?`G|<1KdCFn<3s ze*H53{Jwzx>+c28OE&#vHHyd?0rH#)P*3ud{e#u`(0~5XfBmvr@6^Lx_SRSxdHOx$ zw*=$w&y$|S?>rgDo9v@H1YtId3g4T|la)37#rXA0|NYDDJ*fwq{C{QD*lv;CA-~^m zbJ(hh^0P|aKlt5;{`2pKSp6&WWL2k`-9aRoWGH!W;)f@D_GUCc^uIsCxjW@>*JPPR zl6U)y`EH90Rg)rRbc*I2{m&1p>o|iee&;NFudN0nN0sHg1VRe?9W$5?& z{ZChZRfR9K@TvRY2R|r%rrI>OS?u@w{gx_}e^spR&20ORV;#4<4>i)yn%VWtp2zyG z;{BpEm2ol4-y6wOmpWwPIVb2m*qvWaB)zY^* zX|#)$6|>_%&Yrnrx0vLm0n|udn~H?dsmh9;uWj5ens4F46}_rc zkGFysD*dQs?{)q|pSUT+bC;m^uxj7*pZ|=%UmyRta7C5Nze-(^I069VBp+=RocZe> zFZN5`nu`19-uLqFQpaO!fOZ5P%?7JUCi|?v{c(2xwptA5y;5S9 z7?&04mvdD~jf${(_ay;6JtNKz#Dp;YYW}a}tCEe256A5>f6m=dk@rkXWHN*|XFl%9 zfV3@j#2P{IOcCy= zidBYs{l!}rk&7(>OrI?-`{CFb9A9jf1fap3fWw0&Yz2C9v2J8)3qlutglp(_T)szv z36`*1p|laA!GXav(2RAWsvXs2Cs2JCawx_|RS|jCe8G|0*18ur26e7@JtEz82 zO>sW{T$1waHOKekD(~d4vBZT6c5D0xdqa4Q)`>>1+ z!}yqDpS2peE-@gC4at0Rup&SO>}0d@N;r5f*|#rL7}zLkx=`7Tsg8wuN(7h@_+EWL zF+R5Y*nDf0-*vMmJa?_h)rQ#c?5>yjX5^Z%oYMk5L5+sM{fS3>u%Gv!dyl6G- zpz=%`Ni0b+@y4nii|@^;X zO8o-mL|PoOSajBhtO4T2fYy?8j%q zNeWHck*aou7W>3aPsIH*(Iofl%JxG!*8{KTvD&AS+kLVwC!;1fzN0RxdVtj+LdJU| z!3@Kh)ROe$6>EIFysi_@U0D6&<@O%gXGvaaB}vQ25WAbjHLaKXImB$TIAN1lR>p6L zm{rZ1etWO4WeIKxXwj;#J$Wx9dsI}b`F*El zF)4LqXvSMoE%0$s5&&otGys#WiH&#hSsCX`((u%YSTQbhZsy;=t1@O;nH|Pe0f*%-xDs%4#+-;~b z?P$aC6%{2mG8H1M2wKwnO?gGi>pA|6Q$tF+{vWGvPc6Mol+}%o% z<<~viY_ZF$5_g!l;ECe+6nv|fy|ZJ82uBi3S#C7&OjBf>FJFsZ(JGRe4Kh1qGI(!x zR&$ZEYC^ZY;)?DoosC-e<rL3=$}}g*OdIvfWNXlv%Vh5*qwY^gJDD9GWCYTgs>tldOl~$f^GUFaYYuZZU+4)-6gZR;p7 zO*1zkKep9{!ee7VCJzfedF?o2YnI)QdemyB22@nX`dASWq=aa)a%n;JQ@S%X5aZ|e z!{qnM2jpT?Qz=Ev>DlLIU5<8#{8~uQeUm0nZ3*D`WGHsl%Y1{(cco?!#`f<@b;I1( z&-`Faj3buX=`)E~4?u-9;trseRvmx|0n4Y8SMzZE9GHuKnHZe&(ZsQisU;DEJ;1oF z^SRlVgWoUvsIazUww#tE04hW~x@=6b^;f8l#B>HTK%w^8#^yd9!lwDvo@y;ZRBU7N zX5i+p25>bZkVTXmLtiym5Tu20N1H8pe{R=yoUSz#u|*Q$lG>%=X5Y8t7wZC&@?9)J z44^`o9Z6oq8idsVHVjUr<+l3)f51(HT&6p;h-a{)!OBKa7&{pn<4t2=pR1%ispldI^)gDHWVnR1BYtZGSQ zH^wI@=M`1Wf{H(+*Hq1j&&yQXrG{B z0Xce-M?3~!t6G2@M=U$I^h1F_K$E3^IwAMID@s5h5C|N-h@!2j`1auZhzU`(%iPsi zd-CDi3>Ay1fm~j-Zs;!ST8OF<`OftT1YB<{d1-Fy3?Se>H{!9x+?>cxlx=Pcm)Xks z`f)x%#rmcp=a+-a_BMOcUbDzShbZ9jJQxH6p6y96zoKoaXbXH3TL9P-FcLfKPTrD8 zP$BjSIZHKv3_#@Ku9UY1v689n7z9f94IA3B9TnXfj}9?d5k!IwC$|a_DvSujz;&WZ z6CubWx^EPb3#jgZKxrSam}>tyjMy(-5OIU_8M+YfM8#7^;w%jAR3E}piqeQ0O>BCE zfzsC}aQt%dEvnkJO?CbG%NbAsOR0`rQqlvu6`}*>m`?&_8RMhhbmM0~Ok7{=QgkHv zC=#y07@w;ePM^6NpiCekL~EI|oR#Xan94K2Gf|;tJPWoA1 zQb5QSridac46YH_u0vQx$uVjJik4qWL@0rg`Cc`iSe!{vHt!s-oU0a=9UvvnM;$@5 z1+jxo%cT0o&50beIQK+L5e93kswK#Q7|RKyEM9lR_8_<}0knV>!7Sz2oTwq#ksnE^ z!45X$4<8ns+^_33w@c#5Dzbcs>P-tVXG zm-3xdVb&qo9JX457`sP;xp(Rzmwz;@f4`D98gn%7TD!V`>L`%IifwgM6+~=A-~oP~ z6~m=;Ae?`R7yU`t-@oKNS@6&>{#_dl z&u_LOk_M_h%Ar&qZ>y(N+T z6nr7s@m(VSDo#j0nKfC?X?&*)g(fZjlXJbGMm4Tj?ce;zG5zS|w!Xe7US#|k>F@vm zvI1FKR|j2t-l~87!m?XdwHujIiWmSg&&Lx~X(oN4pL;;sA!E0HRNob9oDt;TPw%!_ ze3DN&&I%qHCjxNmOn@^9s(s*A@m0j{oHtdC%lK7ek)+^ZOgmy;)Rd(o0Ps3I1g|py z=;hfgidjE+v$B}{Igxuy7319Qp(o{cCYa^?fvEY;r#(qts+yXb#NFk{-tik7e;`X- z_*zMjMPf$X(rZ_-84W91V$!$r*Y2dRC8K5uf_R{(jFX-qh#H$gMiKIQlJV~}^Bo&1~ zxX3Ar;&fFhu^QTr6&&RrWXIeEv(-|&@ zd5}C;#pCwnE6}u?%aJ8ZJ01yw5X|!4NIE2LbEhbZ-O~V|nq;h|CI~|ERC!7s%>+S2 zuO3GtN&TI&+M1%Mw3874de;yHk$p)$92vX569kbmn!G47vhYn5MPUu2PE!Ez-IpK; z-ayy`bf?W@X5A13k#*SvLiTXu48Y-l^rd_RMNw4B{sgYyAP9mnZ7+0F0PvYBELvm) zL44^wC^;+htk1seE_Si(i!Nb%Wi~D4DqLV@Mcf$6kk=OkK@c?;%YSH|o_;(834$P! zc4u$#1OsvPUR&{_R}KS@!2LC`x+Yw|M7ZVkg+*g1-rG$jLB;;LH|@2+qqpZ%eU^*+ zPv{^9RP+|FoXm!;__=FE1-qkT){exz^?7DVd{9iEk%uo8&NJq;aXDPHz@Bck2#R|U zt7Wi;eSGZt@#|g|yh6_+LlGHRe8H&LA|iyKVDdPEpncU66m`i{NYA&-SD)zG=K}SsLz*f+4hwLyXbxxbW1sD- z12pXe6#LjfT3Y+C0!9`QRtY3=4rHhIVeo(pq^WIJ0|HEzWtkW%bupl2ucZq-41jDG z6Ns&{7x(r>fMjy2M5qc#>e~|cNt;+41EAX@1NsOSivrb56)9A@EE5Q1b?uHjH~5VdQr{0hJjp;5RQF-f?_4sr={A&kxHBNqjz7F8$R0$lx0VWUu?J?lfC!|lrj*2r)cP3nzka*W7oNC!n(ITRd*KHrg$^rGO z9?%$Iv%qIW00i_25%B*}liK#x`#Ol@p?uI+AOLcp17-u9M!8M#N&XuH91E`>o%MAP zr?~kbzyO?CErH?@Q19$3={t8m8rc#Emu(uvDSple-RTC{%K%sBlcekDEW6O{&LGaI WQgXg;-yj0++s8RoVopHXv{C@^mLfm^ literal 6868 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 OJSNgb2x2=a#Q*>dB1|{{ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index fed21bc8410461a4e5349c9ade60c976c75875e3..b75745ebbb2de750b4c788cf630ea313da2ef1e0 100644 GIT binary patch literal 9176 zcmV;}BPZNaNk&G{BLDzbMM6+kP&iD(BLDy|kH8}kH3x&XjU=V~VSl@OM<605fM2T) z%B(j?{*L*c-;=X}I$0xQ;=^_z08|~hc>#r)nVFfHnVFfHnVFdx%*;&3%*@QpchY|| zU;1DAPuf8DbF-C-y)MkjzxiF5nR6L4D6G)c@{GbeX11%Hk-}zIKJ6$Ko>O_Y%E(O^ z%o9C*RaXU6 zgkjGmfuxQ7tDZ;rQP{R^i<#LWfwpbatw!vmw%gcA^=;d>ZQHhO+ouy)raCZ~FV0-`nQ<9uNWo%DRk8M7D*0${(tZm!Y=vlL@R<%{tu4;FetGatO zIOmj3{s<=>oF~s3y*Izuwz2NJ6JFU##dg-}*v^&hip|c%p8MFivTfT}wvC$Dw(Vqj z7uz;^W83x}uWZ|i!~g)(w(;yVL+IE2#X@W-x$REMMKz*XrtOUqkJ!ZV^EAaqN#5IB7t~A zf*~r15n_4dt^U{KsTZX7i*^L5kTF^{2JwVMLqG2=`j} z+x`AHc#xm`>CvPAMKu&{3__0oe~__|V~{LJnV=K_)JY)qY)B^L=#u~Q2MxpTAoDH= z=?2*g$^W)WEdix~uqhxp1(Z(#O_9&XaW7 z(#H8*3ZS7}KF-ErK@g;mLOM8fIbHd2nm|@U-V1_2Kobz0%feVL&bBBP1oO;ISk@S= zt1Cz=$VEt_AmC;`3o`j^7z?kwaD81|DEmM|BZ~gVMep;2n6}+`)I2TK>=A5t#u$AmQ}D3<7RRwUI*m1VSEAdgJ}EA z%R)j4Qb?EA5s~I6zt3v*tn6o5IYS9*aWgxwGqT(8lj|^yW&PUhp?VB4i^{B3QsU<=#!K`I|ELMfmW1o-A{&ch0@Q|_s1p>#4<%DQ!2zEsic9^#N~c8TRthZuSGq^WZ`nWntVS~ z5$7QRqA==$(mVh#StBA(tj8AIjAw{|gxtr@@<@`pH?i{&et(l{AIn|i_S6QWXfm~3 z*>94%JdBo`JK@(isr8WBH@SP+oG$K9wIU#`hpiSU&TbKnb#qqE0F3Pn@p>*P3m38b z=yW=p({-^5izkCytnYilf3En$8j3%vy+>7W6(etl+>+obVy-z-`@Z$9$?ePLWC<&P z)-z;W735_EdvhcmDiK0Z#r&Sn^?&=>&ysq#PDfuotWKoos1j2yM3HB|bvTynb#JWD# zx#D;)1sdyy6{t>z0zm^KE6PbhaOrk_7<8=7{jBpa-#5^Kgde{f-tdMAU;hX{e~7hj z*79k4EFCTvr_06hd}VL#)VZ(cZy1VLe?zCB^e)@;#r1#VKZ{YB3)<>{;$#9?%yS%K zJvJ~;0-*`mUCuhjx)-2O;jl~*5#{Vdr6V?9%*B(vXPoXv{(CMGOsuHXVtGW01Qi*6 zQ|6IG5wW0wm2iFRtoMz@_D=3xw;soT8ow|!0)SG@s)4-h1TY+oJJckGc@h9(!Kh28 zPRCq#fj~?)nOncnneCZi8ir(`wr|>BPo(zESaE1B({Xu_j`m`&uo-#)rkrdPH(H#( zJ+bqc?ikDCO=r4eC4x?N2%Y2YP(y=wuQ{cAQhFyqG24_yDFAR-BQviq^BH@Nkqu@u zbID5?3rBF|sGpV?#tQpxK&50O%8o3=;}tY(Z{3xe@5t##3HNnMaO?D1fGnrvkyIg4>%f~ z0u4$~Etg2UulhcBd_Nn9dn5?b0uJxv_GH%}U(!IQ?@gDCA1*8jl>ENCL?b(pcNo+1+!;Rpc0(yOBWE*3%ZD7(2 zYw#lJQtFx+b?YlabzN-h)0B0vP2aVu5tNjH z>Xr7Im-wCaxQC#oPhIHir7HmMVft`z8&|MdjfyrK!p0+dVG<(y$ebc&gs8Gjd)H;% zbiK4b8!~N*z7y4!B2uqkOuM4%&lH3poFend*dZ{M4~KDa6`RzkQDbnMnEFs$Kp@Vr zDL(6oUhoJmS9y45(sUawR-ywYpn1mZJ~eXb&<@o09(Les)I*h^st;2JXLuo?F1JX& zsW%HD&s>X^2EEc-Tw2pv&Lb>`WDYmd+{Eg=GY0CrV;8E)OUypnSDkAlK!Yp;Klx&; zBS-P1(;z2tCclPrd7>e}cII5@So$7xr9@E#UJ1D+EYCq$5ddV00!TAZbK^t>^h{Wk z!G$0P($~+|z>JWbj7x&<=%CpY?X1g50l^GFFQ(ELqS_@<077$MpIDzJuazx%n8{8| zHez!#QJ(1Ex8pC~AAhl}q(m+}D>3C& zD=gs0Roc~XKB|dsiK;U;c*QU8#O0k7T#3OQ4?JKLCfoujfskP@Wfm(V#fXT|rJ|lFoz{?ohPIUEgR7hc! zKq}S)nCaoDzha62c0KY1jz=V?L`(B+>3ON?N0nZVNCT8QB=txZAdUwFe1uX|h3BGn zXcsG!WzddllT~Np@@|FAj*dwql1B(94$#r_w)vO@TnVeIGgZG9oxM_S-U0ZcQGL8D ztp%)rM#P0$mc!EXQ$uhu9sq^I#cX@L*`2H>5@`bM*i(IM4KKp9Bm<>KcyKRLRL9f0 z7F}>aGKXCMw)SW1TJ6tQ-g7An2*V^1_9&Wo<&zZL!?*;U)eGSlUDMsEHhBsFt0udJ zFbg7f^6R^r*-#{@?de9Tzsa=^6uJJfJ6#2g;kdJ7`KSqRO%Crta7`pV-k;XBtmOf^ zKQ*-GL!SGO z-8;f$>2SWCJO(v;$1>-yXh!GazZMuK32e`x(ZO|;i*Vb_O>xy^Zd8`P*RJ8f<1%xRxsx82*=CmP`HmLb+awJ1Kq%UGb&Bh z#``fjCN|XhW_0<}NgWQtk;A!k`_~6ng1v2{8NFMgF@_DGi}>JGj2cM+(O?lufzIix zZo4#qU7f80hl-9=pBmi_Fook1i|38YJmlO?)cOyz_db&MXZG58_=6b7ds9_--E4u4 z=))w{v2@o!F>JGY!+0+h`S%~?_pjxaTibIYIQIjFIRuuY@`=D5oDnL*+NR`ifF7d= z;MOG_8Mi7UrX5z$`7?I;3>-hDN55gTKIC72=BsBQiY#|b1phV1aXZVxOH%2d5u~z9 z;)5$UHGD_hpK9OF6vHryC3eT^2=wMx5ELtj-olqKaYUuCiBrCbAwoy1?BoFiqLKcR znF%>T9d;o?oIx{u-66{e&rwBdJwD?Uhw_^y%-TRlNS@7%8q#vul^S+S;j^Bt#;`lnbD4EyHFd$9#7cnDP%bjwsz2XJX408Ul* z3UWwPe7vljcG$ga-TKXIXAm2LBN7N^vT{7i=QG7|vWvrOV_RquKn8bld~jvL)>TCR zjQ07=Fl3y&A=>_Qm3f2(1yr2aII}l5QB+vM?wW#AUb=vB*RTM*+9aR@bg7#i7N<_A zF|G5w*6C_`24bhnzb?XjbB8XfEzu`OzCJz!2swl)E1w9S){dVj`t3r_5CWjMI;HmZ zc&ags;X>o;Oj=#S4AH^0%0z}|*RYIqkrUw|=O6YVgP(9(bmt%k-Nv57irRX8;vicW zYjUk16mlQ>PrTM+P(w4~;#A$eS5v@58PrL)SmmpI*$6oQ{G_bK^Kv3Vza(JX`#ry2 z^xMCLVcqCDVR<*o7I_Bf@Zr1^?~wqfpJs2$f(7ElE@Xf@nb~1WY!Mjjd^bi!?ACJ6 zSnX;??~0hz{&aZ;j`yJI6v$^M+9eUW#SRyv|1<`mCsW=h7v60ge;~d6L?6|BeYZJ~ z&zUruwQvSj5r+8$Y=qtahjap-Y)iDh2?1FFC_buN zEXH_WdN`aB-b+zg$)%zUgHiOK7^{PAvdJbBF-~q@F|L0b@NsU$Wo4Hc?dQEu{OV2w z&II6!1D^Qc&WL>bs@>`z_uYlAk>Tbu^9CRpM^xo#FazsCT9?lV5hmCepOeGG+;af6 z)oxAInj9Jza|3*7RScc$W{npDAglm(XH$&Hsufcyg;FNzKc1SOO^TjC;04 z>vE`whfm(6(z1JItT_uZhF(1Ek_V|9229->MIumVTJryl>Y4;kMt$Se~0 z_zzAh%y6DzIRL;b5mFGn>!P_=Rbl@8uEXUR;*sAtA-Ap(8fTgFqX1Sl@ugc`P(sMh z7yx+zwsgmnDhdFg`gd9E*-Q6%RQQBE6z`ePz!xZZPeYiTN`$;O6Lx8lvkYA2#~7!` z+7nI9bWMY7Dri$f)6|_Gv3VcY_akNR%9uX3jvT-mK{I12WQ~!ogUwy(!au>8fKs-J z#hJZwV{b%9L8=de4#1%@emkfXG+-Ap-gUw(WVee<1&hebiq2p#7-3yIpUcR$KU6a| zbxh2hFX&^ONGrOeb6qtj`ixl3m5LSRB&swyYZGp0kj0Y3I-a!w@v8U5hn!7!vR`j~aQJ~mZz5ETVZ%8jH) zQ`E$kG|=p!I9-@AlotTu=ZV0T6&yvsKu1yR$Dqad6iyEZ!0P}Lfde5HAxw!*WzLZ{ z!v}+*6kDggP!uBX_bkSA!`vbYnO{c7$$>XR9xD5`8PcLMS6AUJ&`}KH7_>4R4K9^- zF{8vb@=VyLL7p8s0@Z~%ur{Q}x<8vgJ4!h*XJH9MsmLm_I{2I#*>q5Mq$>Cm-<#u> zubk+-0y>J~q~QyOvtuA=Mu=;qg{V!A4#e45o);n8jHTH+ypMJjrDLn zn@<41pe~6$`xGMk=h0CF+u>*rfCbPi_)0AQh|$Z9CTR*T4@ zv`A3Ec2%7k+8jiYLj*v3x{f{j)W%kH=RKhy4TmI(j$rVbk)Na03-!$ zm3JSyDmyX|rA}$hLrTYMfM*M*D&p4Q16(U| zUr0bm-=;_bj||uqp>iv&s7{ta6bjKoXbg|Iv$uVq0Ns5~ZO|KW*0HGU2O+?v6i1Qb zkw{Uh#vk7pnZdn4V^U<$yzJwbui``l)bIr4UAy5*++reygChXQ@~~1sx%qe(vPy~e z@$*m>m=PMxB2SM^D&Q-c6h#D71<*db8qxufCS!!4dLBlkac^fRK&8?gSj!7F)I?W7 zhA4v2&p8z{!f7}#Jb>g3v{Yb2`J4s7P%cgfjS({4nKC2?TDw&l)ucft3(_``0rc2~ z0@x2rx2V0rr!o#ev;~4|^BcQSa-p_G8<_=btH6PlLUYis%`#w}2HL~PGeD6WXQpr? zD6hXsI22d_Yqf{~-j#^M2`B;H3D-&&LjfW;ryVe)7g?|}v>*obCSFNRL&MvVQ{53_b7`WS0in50w#}Uq%2`J|PFSd!kLlu|yiiT?JBre~` zNOMPy@qWdeOqJD*b)=FEZl&tod{T!Y?1uqT|Qi-?Z#3PgW#zkrT zdh~nio&8?i+u8RcIVpQ@akH$lP3oje9;^%}zv8?yVV#$%0z~E~GCNjjq50WXewK%* zI_O-b9P>;|&PQ#e-G}$oS<|*gK0%V{MQd&ige10TV&4IZdz@bZ7t?ZdU{y z6L;0$dQ|r6k~fGOAmEtvuL5DwaPsUIqriBh%;z;hCkOyYe+t!D>SJVJg#9HwML}cJ@XaW;LEUcOWrM&t% zb6LYTgIM>xmrOg#nQv^cdueqk+`Jv|{H1{ilLoL(0C!Ee^?4wAl%vUFM?uvXNEl(Q zD)@Gotf=LpCukOGwaCO7Xxrf^(oov8^5>DO%qj=BB>q}65hw&U0_erg4ZxdkGn2NPU*&I z)#n=UpmtW@`1%94ZkLsf^hh|d$Dn|jZEV#vH#=%1eLip^wzQ_s8inJQPar^*k<=D) z2qcIpg_*1=g-3JogCzw-qt3|%s2>nVjce3oZTMVu-}Ej8etESTcphSrUmMF zzolq#^J%c{ow!(>${9Ml-4x%+*@D3N@5z$w}P^|8af zd_f?Ss4E{gL(WfV0Nd&>XZo(hq0E__^5E>Oi%}B=GWIs zXhYQ+CYA@_z9u4q&&{q9Kml!MhD>79b@@vOeZfd2hWWfD4}?(KyO>iYVHwz+4KTCy z4WQx11avNN3JwKN9^z5;m|26Ygb0AH8*Y{c3F_`ER15WB{$?B7RUi*sN>~Q44=@(= zC{Wf1liVPr4a27##cPnELE;L5v+*!;#z80^Ln~?p_yObqeNQBn0~{^SM_2{IdEa1j zG?H8HmvGyGPYnRDJsx4BSZuCB%HWk!x5tv!wWC08B%%}r5;siyqyvm&E+79W6_4rU zG{mip;I9Kr$tY-KljzYPlT|5Z6d*|ip?A$Fh)6X@>zD_Qg3O~|0JV;<$qabpPI(8g zemykK;8dL%=U7)u*xc))MM}Os%qB5mu2zThNtAEYcr#L4b`;gUWVWwrPqI}2UGZ}r zw4U2sYJN4cF@U+GJ%@$r4_MwWDRV)qc3PRnKm!^~-SBtQbRKC< z$YUdvNoWr5^#Puf2R|$sUug;gO2Jq(q=sKRsp%1|oz!rY*1pw6*`N1sd4;-9fzxpx zYK!fa+;j*Jq;>3Ufn`(;sOE6qlN@xILeDoKk>vB@}Z)MCk}$4nA7oqCMhfmDnq|<%zsI}`nl(z__F_42p~Tv zP?y5Ox#qe;r9M1f&8u?7Ku5e?J8goEgT#Bu4hWB^q&1=dXk}QsBC!ZiK>H+}d+2m8 zS^%^|s*K3_Kb92bb&3cP$=8cn6^dXPruMm&57Y&mq+^4#_m@)q>%Oh;sc)Ig1(t_*9_PIK=0R59YAz{ASKmnmE!zSef?U1}S+L_mkuFu%( zQ!0=zy#QedAaHeP06H0_Q?N4~jUzAWmUQCPW=clm3D_G6(0%T)ArY!W8;gJk;M#b_ zC{~Vs{+-jWbJBBz7MxGg_KFL=cKc-Rkb(1m`3sGkdjia(jP6szlK>&BMQ4^_K0fPu z=l>I1adK!aE+ji=DGGX`=;&-T1N0-eObS=I57pu^etgK=R*Z6m=;vQEZO+D}G}M;_ zyv;3XE5n9=k&61$b7f7mn}5y~qk3CN{P^@kJ1snMF{CNOw0-)YlZP3xzk}!85^_T# z`{1%51NO7=czX;`x-oGcY7ecNhN#%LEf>XVF#Ju1d5?@dH&4^iG*2i2O}0kdTi>cU zNK4Z>dA55-oB#I0+1?f#WuUrRHvzJwK8Icn#t)c>ITM$9r!YUL3b0=9;w?zT+kJYW zIw%M}e+Dk^fI;RDj5;(RIH{ppSGz5*-#q3QD?+_;^nWw2hE~UA^F1@t^z#;io`Ce( zdB(9Yz0jZp$VAgUkHwZ%)7t#EFMsJ5G0m?}(^DtR0Du>7eR|8GU--938P;7g;#8dI zmbaO)&C}MR43Vk4+IBq8+uh0-9&ebe*xvHrT`=+kk$-#P&@G>CjW+~TTpe4){9?A% zqF*6K8AkKOdtl7TInpT^DJh%pIc+6DfKXv!VLrE;+^OXGxUjH$2?9S}Y0&>Hoss^S zJu*&4lzIO87hEn1KyZvTK9RE0u|;Bh^FiJQ2wM@F7cK`wq*;b#Lv%d^b5G03j!5d2 z9~r6Z|K~HcZ6H|$im(*gw#_gv%S_$WmwM>InYTf@eZU+!@C#?V>e~isPLrX73jjAF zJof2*ww~>U_2(x;GGez$|AR5-caO3uQWKgd{cYv*~h?uL$*pU${n0qt>6FR3xeSN!1_qN-w4m0OC{SWWg?~W21HTYxhw#cVtf$* literal 11428 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⋙CI!f z=gi1yjqA}oanhG27zT@OhH@F@G2!x>|L#$I zvHZYe`h+q$ZL->0Y>>CE`L~Kby2T?$?6vv=gMBA<{o5NQ9lz}9h3}$qZqI}~&n~+f zMSg!mcnlCXJ)8jFB#1W8iJU+N>BSX(1qs_mym;_~{`!0I|6j$fyIPG4PV*>~%c(Ap zJm^Yd%Wflj?aW|%z?ayy)@4b>b99*F_1GA-nE`eg1?Dg8$l4$O3a-Eo&K;tDR?r}4 zCPAzByd@4Gu$k-#u@mP5sHcw8a}tCb;fKs1iZA>s64sA+;Uiu=xYCun`GPNafQnF- zynD)(%n}XzK4kw1Dj8t9Rm2JZ(qWg<20yo5Yg`9A&1oo?pFJK?*-5RT9Q zZv_6v*c3YqIs8-qj`oFM zeT_Y?3W!?|l_--ia+eB6QZV3x5f`u0Gn6AhT6GeU*#ZY5sy{GJaiKm&x#y&@Ocfs> zxMn7`$(wzi+=~+s*u$y0`Hk7nVcv(YiX2^y-41?1Ae}tA6^d5P^oU1H8tXkrp}PJe zZSZyYO&ed;8#cl5e^dZwK|PE!bLl_>oSHEORcCj4d zgMZbtKQpd|!gHt!kDyV!2blt{$q|T{0eL&-iwkjSMG}!Hc2aF8DnuiD-eBIOGYAn1dgZgv3tybJ6sdXTu|{5CPm|D4{)=P4_W2?KMb|C~L@zH?39c8(3-|Sn&uo`37t28Zq0ZAiz!c}BG^^T? zzO}=nCy(dLvHsH}XH#rynkz|Z9`@^>wmcoM{x#_+B4UW?pLPW-tO2=WpkRsMI(aa=E<@|xIJk12`2 zBW-mSGgFOChUfoABsIPm93h2u*^Jf8Wbx^j0|BB*fbRF71_=F*k+5aG2)uvFW;Re7Atl_BTH{4@FI*K4llc3$g=(lEOZ<_5p;Ijt! zs~Ik~&i#bEY?q=WR3xW5CAF!jO+j^1D%z*0OlfH4$#$%zViNu`(n>u`qE<_ z^QABK{Ket+f92?!b;Ivzh#Vl0I^z)%dY28rvigovBhujQxMx>!;e^F?bx()4A+p9O yzE-WxrKiexoDv~**@Cn&R%@VwS*7u z=)`Vmd+)6wCpJA)XHl;0@Fahbt^|OJB<11mo)c+G>hJ~RE~&dm?(XjHZs+dq?(R0z zLHG8|y}f=oJRHt4s?H*|_Uhs8FcD6~xw|5RI~l|}wJEX@vK!-0=&<1wp-a^G7Ve(9 zJ4DBQz-72iMC{OL{E*1@a$J{)FSshr@W|aVs%CMQ3}P4n1e^T-Nio~DjoP;Du#F@| z4*U84nECFRrQEi4Y}>GH+qP{pzs*dx-#?&kr}u5PZ6$sF^5f@O6X5y&|6{V<|NB$6 z58K_l_i^oG+qP}nwr$()LRU4btM;yHRNJoVE?4)-sIIQ=-C24416djA3a&vXE2D~D zv29z&uVgtHIu+ZgIN22^BaO0_%_~kiPQ|0MmO8d=dt}?rUS}5jiaHpvW7|$%#FCf!l%n;A8%>btygkECrGB3tDsB{?1 z=#?aiK_*8gLxzAdO z0-!Z8vFOq&*;hlc^=1PQhb}FXvssg9Lm+Mq(CL?WbZL=X)tusBpgB>ddGb{Cq5}b) zQ`Hv<2m{dFp2CoSh@;E`C4dwA6!9a!>u(2o3pD@H1?^iw5eD>K5Je0+nnL)wYoW1cGUp5zQJR{t#2>FjU6@$pxa08M1zvj(S0 zUvk-LEjD9cS6W8VlPdDA4ZrTguKM$aUv&{TUGzhp^qOi6!7!T_msSC9))lfne|Dfr zI^!lx_RPuOl97*PY8j?}M6th$Z%R&Pui@q1c(cEKnV)}dpa0!H{p&M7{a9cBIE^o_ z`x5V{$lEqkFzRhfW-Fr{3#f`siDs344;>PPgS>L~V(ti?fbaH35!oos!|L|He zZtrJ&#|uJt4n&-;A!s5#nM~~QnNB^k+K@}czoy=6#>DL33oyTZJq8zlLko6f#AB6$ z0vX}yf+-DA2c%0t?XX-j8Szu=AmJqy?=SqN&2x0Knjco@E-d6NxZh19o=THM#83)? z#$cZ{Z8CW*x!8Z1)?>3d`cGkE4llnQkz9pxK{XNTGRUSjLWQEK$Pc>c4qJF5xJ)Yu z@o%ZgWHPZwN8(#-Hpl+WO`A|C@}wNq1jrfgg;Bo>V2QKV z*cBRs%@LEzjCNq=aA76S6C+k5{~D085gj1G2Wrk(hSKHb%rSs`B-oHiImZBbvyhUGTvb44I`pvd&=3 zgK+|Di!6y*0ZedWz%Z)LNwO!=^(cAin>k=d`C=-oC? zS90-$gn_}s`+q`K4qR6}*6^`$O?I&!MBfANx)=RF^e)>$RK1>YOg5p-M%Beos_IR& z9kS60S(0=C)3fNq0u2ak0NKYBH0AN1B|Ft^fn5hO7f?bpe1unSmo%ayVT=%q5n9S*tP#h-_UIi6-xLN(LyB ze0JAjV1^(zk<9#nSuU{z#O|0+Bm-**3`^9BHQB8giZQYq7+JTx{}Ym&{~vsVrzrr; z$scQ-Eo4(ffCGFJ*(_O;GR_gG$Y2tiwn*1@*u)F|yI{@=17ABix+wtSZlAjl%LiCX zBxDqth+p8`;QL1wm2g^ySUD)m@J5nED@E#`Xk@|(2^B+lvsyvBfMtddAW{;l12owY z8-EN!sLX&Hynb}u025AF+hJ=9c8T&>wa%vzO%|7NM)AmjJBCIwtjSy9lRYvuBRB87 zWZg{lt1FK89t`L7!PGXoBS64@M02l`o(UHF2G*7s?ZMc9!*N@1c3^jl$?AUL-Y%Vg>6*cHBwAr&#V}V#`*is+uE}SzAygJ{B%9p_ z$L+v%$%bKNIiAj^)X4-Y8%kC!&=tpRv1^%6F$rhGE3gKcC|O-4%Deh{Z;~v-s$yK7 zOlG2SO)`p3+u>8qwaYJEU3CyKj!VD-LZ~c2zp8WqgS%imvMdukRtBgJP+#GpzvG>< zu>h}!YLDO>#0kC&Hu)vXyAwK9_FNGWp|6Z6KHwo` zM<|W6#E&>>8YwhJ2%&V3Ce~SbZ19Gulq6;VKd{<}Dp_7-t7~jk$|E8Un$DRN`xhq& zf>1{&a9)c<Ktyjdzj9YYCkv0y`cj81Ov;`T4knM1U@#=j*9p%2{n zh2Hac?!$$J9FcxirdyFM6-J-D7z22G<{Z&ubc z)gcZXB;*BbpHKL6J&dn(IQXJ-@7?Uy3)8)O2cC`(MMxe{|DxHgJ=h6cn#Fi7t)boz zFq$ff_~+DXW@k5F*glIcwe|^u2z0z|Y)ek>Hi!g?*?cg6!6>R!dl-7t8jYT_N5J6} zH7OzsKx zDD<}FFB_xPb9AJjfFMpjhR#cuSGn@(pBO+4q8fWRnxHz?6IDRL7yTbEM@BhP8Z0B; zL#4zM?X5=Rjnp0@G@;6N>jY*NA7C(}|Ci;eVsiqhhj@>A=4siCwMugTc#U4T^;ArJ`KY|taGKInX zRb%uRA976*3Ua(2qVa)DjnRW__qP3rKnYCA`ON8xb=s;S-;832P@2rA(HNE{o=C6QeA ziSLn~%At^pk=^(eNzSMBb6QVh$aRMh>WU9Ig9^PJZ~f98@KI&3@1o_jpDK$9f*|CA z`OKe1fhQ|*c%1am+FwE`(SJ)llbhlyHZDaJB-SrS!c2Vf%u>Ok} zCFeatsAtjPwPB@~o?#6W?P!LhVrWQhzL*1miJxV zg9i+p5Y&pdBe~fq{AX?a9l53$2iTHa(G$Mj81Yykgl>b^g0-G9gQlq{QQhtG@xP4L z;L9FEKj3(>3fGm&V}kH>ewp$vTVCOb07G;l^%8fau~qrr*S|jOzQI5U#r(;Q#tR@M z^QbL4;~j`m?`fwgQQl3^<=KKYZS`B#Du=UpD6fB7MY}6O5JRe>)bv?neW(fu48=`R zeWbRTOjFc%$r@iNr5;u~rwbVc8%k+0LcY#-1!IHVm?qPMeaiaMuE+>Ie}dH*^Cv$j zR^Z8GG!_saVG2cO{AiMJ#5!bgQe|9=tVMFeX~w7-h?7Qu%8hVjQvcU^>n9IKCNym>aOGtdPX z{glhFmstRql3}xK&3`~*z^UN>vDNCc;BFakI91w~Rjt;lsSpsvy8Ds!Z5(U&EP#OB zi^&0w+VD23iBuj%Pl`V_>c8G&e69i^lon9;qQ{-VOz%RiL*m1c**>`=m+%~CW&o#j zarO1_!huAC-usJNwd!y6fmc7-LHwML_v&S`WO>(yOmLz=0Vi|u%J9FK$wbNkQP$Fc`xNch@ehUw z-*-*YG-t50x(ChfA5oKhiKv8|U`xsJy3G)(N{fgfh{q86st9Ma0Rzjuy>L96L9lq* zcs6ms7RNhhqo?&%0b)o!8b{?7smAg8sqz*QxY0h7cNBO zS+~sBSzY*#P_VVqbi`w2Hk-}j{gI{V^52{h@R}inP@bVMu-Qm5$+Dwa-6Q^AV%AzL z+*;{by89seu5B_5YjSS%7J?AM)Qln$4oT!k;uFd!9uPt(jZrunOqH+D34 z=tlY9wn*nj=~4?wN6-}Ij8!XqoYckA3=Q~?CWtCEeGV*ZfM}gBr~@`Z12q}^eH(1D zN1Ad#s#Yu;V{@OXn$$(Jc`u54ej9?O>0QPBOQGe0Z<)$Por9_1L%%Lw_}Vow@#OBw2cKojA$y<6cvdHVR?9ufd%!>=IIod zu*Fb0 zWU!3lQ6*zD?n*Y*sCGp^4}>rZam zSgNcvqrP=n?Va7qMwtPG!pLf@jxy!*b^sv=Uai>KpnW2nLI_nE{43>*)bJ+TrKL*C zGVDlnRJ9RJrppgFo=MA;cg^KhDN=yY7N7xYP={=U@M%Nqf};9S)sI=Kd_*FRu$iih z2(ReDU)_dtbuJemVCQ^9jWU^#5EMt|3W{o+EjP8~#yPy_iY3*)=tClDg34`l$z})= zQ`p1?c6->|eDIImijJx_wF@n|vv*ZFfpW#7Ez#9bpJk`dsZ9bD;=t-82KB3&;7H3O zNv%bjK`!~DzEstTZ0l$1{%WtcgoJ?tcEBg(47LFxs2}G9-;!$&!2pxC@oo=6ioPf$aVVVAt4zOZzz8QUfmKv3QAE(UVIfI#abg4$1|$YAcc z9)qk@y+r;xL672TY;+ z9FY1#pQ2taL=pIpy@-aQ&XJN}6?;|}qPaHJo#^r*n82rX#k((17y6gX{HP)@Hsf|g z{{^aZ*5yM`f$%RWN2C~h9nogTt^&K9!3T|!g`zrUJzORf5E=72|5z!iJeKWyL~zHMTpN&J zWh;_a(doVBp6>l`I`$Fd%ly@Y-T8%G53!Sq-S}~ue+tSaXU#Qd>DVq_mnT#2bOO~7 zUs`6v;JTPS_{`|>g7=Ic6h-D8_<3h=!Ae@>V*_;k@xxF~pnJS#G96{#FkHBN`U*3zi)?;q_!~XG$eCU$jgKM(eF8VDJ1m3G?nxM-U zDC*yT{Ks9ftP_g|Fo^g`XOPw2Nl{dzbnTXRp273j5VX&V$_OC(sVTbm+(9Sne+f5T zMt}$cli!p7`mfUqS6z{KTPYajjr29+ekTx~U^W-@E$f!ZR6h}Se+Gvn#cVvIgLNjre1|;d$?xo zuN3MRhea}H2xhRsWHy=5q~n@&0+UW^lj&SIhY#+UQ2+=blvBnXI%elg`X7Cnzjz!q z=CM{A!tH-vodyAr%pL!?@SJ_B6?s;|-&|ToF!_JrF{2j1$U&7RUQ60oa zIeKXwRvYR$`x5PGT|hvR8+8F{J%Ui{&{H7P7@QFyT-KMFKpN(0z&Dsq*}7!lVICH? zza!FYg%Vb-DGu4yau=3K}7S3+;9q_=QxgU-t78dlpD6cliW`tyd&Ejh1QEn84f z;6Bi@n#0YrT8`Cpf0L6-e;l+NATZ*?-mST1zrbToMNP8I0vM2y0K09OO_!IK_ppZe|5eqyJxY!>OL|u z#G~3##K`Aw!S^mtXG}~?T%Apr*jstRVLSB3+0hwS=M!ubR~Hjux;q5PcO)mu9Ne3i zmR|T>;W;NUzUsZOh8u3-I9VBPZw6R*1ovc7&{Rm!~&I$Qs6BZ7epAZ zA%qw~F&BDR=3&`hdJ(L8f)MgQ6beB>Ah`mOrz>2Z&#-J>Cxj5L0O=TB?(TXx@*+l% zLVyB~O{tKrzzZeJbQB*2r{EE3vck#11=hJiD*-%q2$+rFH6?3@AjJTri7?zSNbm#8 z(e9O}c-RiVNY2N|^~DIwPWZ_Ld@ck6K?)%XtZTz|ezK5=wF+y87e4TQZ0D~|tU44;$ZupRGRh%ni}M@}hdSoruYv0>Xm0-}~c@y1b!+!5_!fD}P^9C!Zr zr-z3QBVIeAfZ}WV{41~^25zwiN1VKu?X2KsdD!BHgm;MA5d^uyZz9Er@XVKh%TT+k z4_+tygI7-=$8gufpo$qIja@6fGNPyI~Nf^ zhJ2$pkFrD0B3-2@2q-+@bJ>fgrET- zC%?4Af}OCo&!lBxW2P0M0H~>~%EKAp0r2mNIcMMx3P+C~{ngxp|LUvJqaP3eY(u*& z0|K7;!b)|Eh#{ov4*&)L%d&9?co#!JlQj0u^Mc+X1ni)Z1pk1WLV+)E4Bhk65Y3N{ zR5tDpCj3B6VZZ?8TT}oOfNd#wmW1D<#SqpgZJo2$JMRPW-zj1j$ebe;c<0ERA{mtb zz46{D>kZRU4B?NSu++`5Rkw&3N~#8E0Q3&owm96&CE$)?2r7o)qKT@WlG-V&lS-kl zgQ}dffBw__cdU5qwuj_OrlxRUs{g?3kZl33#o|*Af#ncX41x3ign;w!bK!-ga5hmsS`^q+Pn09Yg|`fzv8$t)#x_yy!Hsk=w+?(Xis-Lo^Z&)wbk z;j;34r;F~{p2xTQeMJO~J1Mz5$#EMzB64@vZQM7)T^fsQNgGLZa3Y_A>{*06^Ke~4 z%8A@v&vFQV#~k+%R+WC6z1X8UO-to^9Ko?XKdSw;ThX`ja9p` zZQEGcSlPDC=Ga)<(_D>h+iJ$xUfDKt)!0xQ06=Y8-Twc#_i-E0Mw+yxE$%wJY5!8B@^9yW_2t_82r9=);pN=Ov`CEylgQX~JOWo-jaY zI68)vGBJ!JFOd>5*bf^K|iis>(f z;*foNWfZzHDG1I)L4J|eRtP<#RU(2vX!TR%Y7WF<(T85*s zSC6n&CO8|mzadLH^`a?4sac`p9E~u`ToX|c$cvi0XNJS2>JYvjw}od8`Ow6Thy0J+D9~{VKy)q{h9|wA%{Y~%SMV>7KkelMi=8iT{qpVK zVCReG*2r(uxVwAW?LBIDk?F zDn=G}KFIq0@nH8u7H#C&nDJ6+^56~X>)$|=P-zSr(xQ|IdW%^4HR;A-um$Nhws1T z>ni^LHZRC^rT>^vLo6CzX2HiP=|>Z~#0AfugS0kc*%O6tPh;-yr8G8j*JgkiY0zXi z?d}}DH3&nFJdHXQbyhNKRGT#U@>vKoeeGEdfLg_-orf(9vy{tc`1&O4a>0_sme^Hf zR=s%YWvxx{xf@4(R78<-g(KJeC~_5cCH9nRWhr>ejE&m>0N}DIU>3+wdYU*a4YQ)R zvf#DKf2<0gs-oAT1oo;%!ft{Ik>ya)@As~lz3aG zzf}2QRuY9}2`-JI!Z68xDaPL3V^90Abs}^%Z@n8y>;2`(S&?J;&We1i+Mvl-&w?;D zawJk;%X+xs&PE&)v(;V^J{bczA2%tsx6M?z|oMbS{n}Q-drT+Kl69j%d}u zBvE-5X1^B6*LCFnh`Q>OFjLxn6b zH3@ARxS-~^8cv#ebLag{Imh+P2M{Tb8TVu4y@Pmb*c_F+ihQ+-S!Jj^Xox+TkiSPC zj1~!@mdOuu?5~QRR7asGF&L0*(1}1Dhh(%NZ%@0p8%r<$DHm)*rftsS8BQ9p$9Qi~ zysWRjwr)_?F)~{hy{C?u0Vp@n2!JRs180LK@0v`r?4NVQDy zj2(M*cNr^JxvY85j{Q~HfKy+~x}a-?9K1113SG~#8nY*9^}zE&2c1vp|r z3t<2BMF%T|EK6k&Ui!Z9c^{Y2c{WZ2aY?fC7TadD9(?l})8J8Iwcfs7ZO3l-uimjf z?c2l$2^^|$nsLUFh=`$l^3L0uA)C^P{6fBk$R$M~I8CMKc0x+a} zP4H(u7^X6)=P`5$6c!h~Ti|kYu6w7B=K8B<-?x z4UuO7*9riD8Gt11E>jilqLxLJ3B~QPq2ZvGAR#4`ql}@FA&N!vTNub0NGH-zP)N~G zN)Qi-=BZe-N?FdpS{fPOgq8s$1{xROcN%ZrDakg-SpuM7@Mbbwj90VO_nC@%rlOg( zc+FqeEAq=ayjWl4oAPWsk!~-NO`2@E>O3H?{b;tfTM>&M3YG+N?x z+!5(+;Smh1I`lGE$_UOk;a_H zf+e&!du<}J-)@3hq*TMBrXv^H?rbmvM-+ZYt|Ka1k3hFAMe2mwq$$I=E7oNY9caiY zB51#l?~Q)MdlO*Ism6F1`2NNQkrAD>ONF&z*Cn2r~|-a90y-2 z22jvATO%hx4GYUhC^PV>KdT7ZE43gd$)aBjehDbeOew~b|C*Q*-SG}=SN z%a)f}vU-NBoj!dHW%bhKd1sj<8rJ|Es9hc*MtR$dbcy0RguSNAx_6k3Jyx#D{Zy0= z&_qbq{=ll8gJ1Ljh(z6$p^&sL;3{L4yKI$rNnY-l&``NSJ0;R*a6^WH z%AK%wNjEpI!i|5GwQ%RW3T7`ZZK7U+83px}$Q}WkDUqQ<%|PizXgAibDDx!aK1mrs z8UVziZglRT(tu(b&^(I_gvVT0q^*;NhUB3+#C#}e8xP&GxkpAj#d9EC+%aQA@hyRA zx4Ovl0dm&Vx=aJ-uS=h3ywzuvVCokw?US)PTY9FXeLVXTDuLoK8qpk6dFHU89?O*~p57ro{BB4_{r=lk|z z-A}twy)?g{Lo^Im*n3c|5ZbJo*;|_aSUlaQvtq*B`1aA-9q&Y8zML___GjKpv zQ^c@|Sxww508}r|7Lgf)qy*|Yidt)+O@iXrQ}oUx>ltDlWaje}LU)VYpNhlSrX2uh zqV{^_{pU$y#hX|x6av_by|_?yibj};_I zZDr*HK)mrf=aU;*TQVN4aW&Yu{>dN-b>Flv(#XP5(%FRlA7p=T7PmfR<{&d~VQMGS z8}69Yo2yA60O#%dB!t4|D8-}!P$Xmj+Jr};@ z*?+Dnr?1d~5T-XgvqkPs#>NF`+FIt)NKeK%=eD{Bf6`S`|Sp+5kPhl zjN2|7IA{m3m(~E#GaPxKVosMbl%BMFlBFJuS$B`?Ec};;4&^DaZ*!hY7eEZ~y%J90 zb*`#58h{1y+VzS!2kk<|^RvLj-6E80bL;{`;m5C&hS_BwVvXU{x@2W5NIZN51@pEocpmir#ZY*W6RY z#%!!z00RKiH!Y2~P8rPw9v1AOQVwrYWOm}E4LB|r06=+|%B_QZ{^GCS$W>992Kg@~ z>KwnD^;o$8_S30ntTi34&+y9in1i-nPE*Y|WMmkJ|Sg-{zSn~G8M$RJ(hq;f};vlaFfR`(HDkGNoqX;vf z+}oR>@??uEY47o;txW@fFoT0q&JwdYuc{aUm?OVaaP3G$rt&Lb8rpm3{JAw$UDRiUalQ@-u@sf2<;SlyzGR zGyeqlQvy@_R^&Wh!j@IpBr|h2bj=WFkJ;_55&*>Lpk*Xkyv@@*su%#6sd%`Bx(lWG z!cRI}gsDGycY7bOyXf-3*m$62Sg~LQcT2y!xd3?JbJ()j43jAq7Te04!Mg;lqj*;I zUdWuU(E6m^J?vZI`W2TZiOB3h+{Noa#8OVmCV6av&NaECIQ_kws0+a&KiS?2|O`EnA24aD8*0Q1C+aJG?$n`9USflvsPNvuB&Av2@O|{!- zYFurM$KKwB1?VHuT4C16E!GnDgl71nOzP*eTvBq9-Zl$u0l19?~TUBtDsT*S&Igi_gB?2(b53GxsMJw%q4+XyD6IRaK44;(2Hscy=Tgo zQtg(bIBbzVANjh;JQK0ZqP9r*I+ZVKzPI15FjP6es|J9VShy}Y$h;BTm1i~pv~iFy zeilwrs|H{O&N{NZ=pSCbY^??qycWuy#9wwYwJEzga+O_n)(cP^04>6oTnzylKivsN{YttYh`)mMWCni$6&^fFhU5VzjQI(X*@c8}ox7P8F> z-cB|R0L%dFB%=V@b0O~^w}r|rgRyFbV9UsQBI~g{E&N4~Vx+M3jcu?@F0nLBqca8?f3og&L`M?2M@OJgSiaF4FEa-)(|O&7A|Bf)x~RzMcp2&{kGXiV;z);9YtXP zz#?HWOq2hw!?vhmNh-H^)cKJ4T2+?bxf=)ILx-ujO)_whw<61^CMgNikY$G46GIDvr64#MxnJs%9J{?0AOlR!P_Z*+^N=>Vp)dnu$Z4+?xgr40J!t5MB+yW zHl$s$I>#Uo3r*Xa*qhB1)_xIPmlqWP+JFNmHE`D~VT0Y)|f;Wfd%yetotGc<0OVlBR+VZR>>YrLvcR`foKh=(sKba29B|VpDj> z0|P0G=UrtiBI~VJxuH%ylIa&&ET%6TUI*Gwun7RbsWm`QcyRgPC})v$$9%=&njVz$ zHO>1g-_t2)&&IshlhEbliy>__?&5mdH(Izk(?~EAgi)9sv%G(WSU5;wg$|p*2ApK$%~M~! z>GF;=)HD<{<}_v$VQy4W*wA>Th~;87V}6a(UQ=O^lySWlu3vHQn zTsJ_Ipo9L^lZ(W^|2ReDjlDiaI7JkVEQ2gXbU-{UT~%Qk_sB;#qMK)n&H#}RtOH3T zJESDTcmQB9&R;CUt3Ip?ptCzB9AX^e5~BR5=+5Rvs*7CVmjJl=$lVEdjC{n(aR>lL z?uGAAVq2jjbN}W50BQh(b+|x%?aD@9I!iJ6omY|)6%g^Gux^c}j8mQwI9IutxVn*z zi)4Zc#KAchs?XHq?T1CY^6kHLQ_;#FEyG=Tx(m-nia0{!e1nX}0RWqEGzy0Qx*=cO za^%ineO1Z#SVf+LDoRt9N7&lbmVItm{XS?zAKoQ02pv1r@ z0dVfBPgBGjP3$n0h=Z>D2h4le-L?ggQKN2 zB%YyQL(K-74WtBR*R^?ZxC}?aOa~ggI6s;BjhqMNK z%Fw*>qdglE28w(0qO?;oPRIHMSQGqzgM5t~8!|^w-|(J|J7^0t9Ll5Jn(X5GXLV~b zxI_9a3}n9XcJ67IQ8c^M5?WUuStfnzxF!QAI4EbXp z8I=Ke^-vXcipiqjearW3=I!~N!9F?w4phN0imMlz*a@F_#G;Ngfz#d}1+j$-wt+=o z37|MUX^}7r9@DT+4L~(|0G3lHk7T3?V<_pDHzQ^ESx_ym+>heG(H?u;n;QfU@)G;0 zs7wB$L-w2jY`9nZOO|o{5-thWp~dSe#o;4e3rfLv4>nZ$NW~{OGrux)p$YX)rd4?U zkl---%CG4dsR9G%Z7P<`4=qWTopfSX?j~tZ8VSk7E?wT6#NmeqmHkb$$)-#L?P!2A zlen;iGK6+!?Fbbf(qX9qU|pF==}aT#o{-&SQgsQFK0_oMm<@R)63fl``O^)(y<_$l zRVqJ$#bw43!eY|9Y*gAm+raw#m-!(}8Ab;}m2pzuZ! zy{C+w03W)oiQLKjJ0008MEi9yCK`dWn#v@Ra3kxH%YK~kpmE9i!h|bFWUTQ;f*fEj zQf|nuP*+a^&S>MdO<$6L`^V!jjXxCXBELOXb7iPzBehh0`>t{Wr)M1-*k&<#NRdzi zVDogLpq(yne*r20n7t{vkV`OKRb+KFAImVPd)ZUeG)vgQGD=cjYqD??bBy988g=`z ztA>U)AfjP1a1PUfcRWf$ew5;Q5l~mzL&e@1rDHvF)o$8k(ojhPwJp!;#lkKkl|scp zwPsV58hGsD%0lK%=TSO}v;anHPtg9`bAcX^HyRr9U2SJk&A~Uq|NCtN(Lfx-1x;$l{XZY(ul3k%MYzHYT(SmODQB$OqZyu&k`9R z)3{AK(4q8?$Z!##qmCGWv#N+a@F)%FRC%3c5QvR`-9;u|bfE*)GFk)`)y-EXZX#oE zGDrf}A?b<20(OAWfVD+NBgRZ*>v)o>u&FMtTdBmIj$A+k$+(}4nlJ@@cVG#2q^X?+~3cwM#!d2@0%NaA0jQ3IIBU6~Y+c4Xqz6 zWen#2bRH(6CJ-lJ24Ks&^dZYTQ`ypozYf;;Ne%&>adtLQ$Eu?v$r9Oy%tS^_AVzcy zm!Zt2fmOpX7q9s7a8Q*T2@TWS$?5M@U^>n!E+NLh%368}UI&AD**5Omeu$tz9xEDC zBsSE31Dv~YOtpJCZdfFY-r+*;wt z{+HG~Y%Rd~qsN)KyalqpC7w3f5PJ>PZkK(r^a|h4%7u6C)74duv#=$b>}th=8{r$8 zxBeG@MvS#5^gg)#lXhL}3tzdwiDlQ^VSb1*p<+VmOXjIqy{0&w=8V_n*N&Tr_#YN-6^J%+7s8KxD-aScknBW#rXO9pvhEERZL0N} zY+Ih}A>#dIw5N!-<>^{YzF>2mp=diKvlo^OT<@60s(DQ2$ew$Bo3Lj?7I3uNepnPBkiBqyfND!A`E+tfo8HWW!)Z0>crSZxk?Mp--Hmp`x5fbYZir!gp zXOG(-tqSBm-%#hD;2+u~DuKDs`^x;Suh-86+|cELe10R&H}j^}L=m!oN$XScFE0yI z!P`k^EqprxKrukU3a?sMgILoc83Uummr`%cJTdh|)Dv+pEPMzLidg4#9+JN{tV_8H zUll;laOm9nftmOBcTOA=CajYXnyHyw6*!NP}hcaGA!vO0Y}eb0s)`^3J;)CRZM{ zVUYE2%=fM#W=rArZ{kt6A%0GGD&j=7q052{bQ#Y1bNP2Do}8k#shY-6>!@*}92^OI zlT|{if^oY-=zM+;5)X*Hzm>uSN2iM|(s)(~U`gWn~Wx8IX%kM0@JURPY#*vR`=-L!_+GITz z>J*>WF^hoFD?`XERSfARDTX{`AW~0XjzygziU4BwCPh!m z2SPqzvLVyMkS#W1!?;83_c(WL4qGF4=Ja!enhEu1~Xsv-$_ z-ym1M?(5=?ly`Y)7z0FZE!Z;HR@f0Aa zyI1US*(@oA5kTY)A(p|m{$uR$0OnICQ^JJjd6CXh%j8aSJ% zdWao5KFDFpg`p^n>#KP*%tWF|85tQFfy^>ZWKow7=3~eA)$gC{m#{}=gSixi0Fj%N zSSztKw&g8<_Nb#orYVXdiUFVf>`}Mvlsm=!1S%dGkg6!kOLeV8*G`_(c0$f45_*hu z`IFr=as+a?-2-8EDGU-q-9S%|v4XA<(*%V3Yl2IF{%$D`ag@}89UxonncyP2*V zw$!%6hmGn|$7axX?sT1G;f1%!R|v^Xi@vtuI&8Rq9m(eQt?_}ER7bc3 zCOJ=e{Ml1BTyFXepZ+!Pb;3uH0aZsqHDM-`amWbDrDP*SS=nQ!PVwEK4m*4j_7`C^ zmEsWKxY{K6Y(wdekNdZg{y%r(9xdgjS#|9j=Z7!yGC5)F0s5^B|N79PCb^!lu>70W zS6=Gn6?Yl3`Jof;^D1rSVEh2#(_X;>AlD`tr(U*kC!`L5n8c zCBB(pJ?*P6oTReR{Pc%Tm@wg9bDvHYPm9bZ3@68WfALI{WhzZyUZBX%NPIPA5!f3U zkVC&}!Y>hrhHrN1ehxt8{PY#ss6rLhsxK~F1`R|hiEpMXCZ0MV&gqIn6hs7GRW;3| z?>A${+b~_q3b8k%x7pKQzoW^6eGEk%Bhkf#1t#aGZ>|be)k~f73uN7B%2W|nj)=(Y zimF80*f`8+0gXs=DlTwgn%HM6M*qzt%9`cBwV|%u(7O0}TCC zU|bqyb%sH{B23PBzp5BW6V>}Jyr05n7$^d|^aRW;Je-ZZRANW^y~>aZ&Pe(GRj z`9_Q^lzK4$;`_3_{96NtR3MwmOu@VAdu)>DBzQPnQx;^6*m z2piikLtIkR%>{?;4u>O;PMUm zMm#Kt$QT*FOUI1djsNj{xA9lxlQSlNzn<1k5lm1=`ii`2O?)+BVPPm;*&IFm)Tf?= zy(RVHV!?)Ay8g_+g~NfWWq#cG6ZPrcOuD@@qArmSMl}tp6#>FLy?+v(A0te8*6;g{ z#q%Zcq@Q=>vr|u+bn&FO6uhTGX1rM*B=O3ri93(IKEHE=T^`fKktPk&9xCP9$amS@ z+|8sjo^f*eRrStbpOl-s?<6{*&o;(;uU6>?FOOO8xXj>Sr&KKYA1Ua4GEc*;RLQH{-9QzvMoi01Ms4 z`iZdfFUGhm>=5D*VpC!>Vsq?>KmMkVy6Hnvhkeyo-Ob(H4e!%ejQ-Y|Y6K3l9N7#G z_2T54Zo27JuM$IlVYeH9Utc)!U*Eg$KA!J#<@S{OFF!8S$oavXWBNzde-QEQEF$}&D>9cs3pp>B{rj49-X@Kk z#P3b4gDuZVy?Bhhxu^fRBSId+4ln;jiy z`X=)FL7dB_LX?;!azV}9zqS%zti?Xqi*>N~|G39nNgvFm|1;n{Z+G@Kx#+qlh5&2UU?N3%p1O$My>3oaG|X!Ma*2mQ{&Oir0g?gvNqCYD9t>p1&iKVX z(KT-YMS&#|{bv~hKoB6JY*GO^EwN~|XJ3Myy$oaywtY@ThBKZWe)!?P@)G3pm9Gpx{CT}Yi^haeV3WJCz3PX_?(D1)&KDWi?I@nSW ztslR1)i;l9G{PAGVSpHaYax`DOx?Q+qM;xf3#6kG7F5Hc>R5_;s%6>2Q~&1gOTBxm zi>>}In!(cuPw=bAnKR;I|j1U8F$_}*j-kCG5F6i2aAnvfq=}1 z{Q%_!Q&TMUCDK?LO{LL{cz!Z=vDD?g-0$J}0_n2J9K<%mlYj(3Y6u_?P;i$YpcMJ| z-L=b)LRPuv$r+3aV)M!I(idG5xC$U+z7XUy-!ZEMUh|@t=J4@UFgJ4qo1YZ30^);$ zu0c3KK|xu?C(Xe*jCUb7+2-@-x96B)hG4`q!wfmz{%0SX%#>G5Y&IVZ#Aah=UKbey I;RfL%9J<@I7ytkO diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp index a2f14f446149a3da5130c93f67e00b7fec1b9f3f..5e5574ba76b31bb3f85a5b6211adc5c3b6433ce4 100644 GIT binary patch literal 13266 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 Qu#zK+2rCifdbG?b04%LJy8r+H literal 15358 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 diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 913292d..2f56918 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Oct 04 23:40:00 ICT 2023 +#Sun Mar 24 10:24:05 ICT 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From d5c412013eebd67a807ad1e41e5633eb95e060af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BA=BF=20T=C3=B9ng?= <47247560+ZoeMeow1027@users.noreply.github.com> Date: Thu, 4 Apr 2024 12:24:45 +0700 Subject: [PATCH 08/21] Update build.gradle - Updated dutwrapper-java dependency to latest to fix login issue. --- app/build.gradle | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2caee8d..68388f5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,20 +56,20 @@ dependencies { implementation 'androidx.core:core-ktx:1.12.0' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' implementation 'androidx.activity:activity-compose:1.8.2' - implementation platform('androidx.compose:compose-bom:2024.02.02') - implementation "androidx.compose.ui:ui:1.6.3" - implementation "androidx.compose.ui:ui-tooling-preview:1.6.3" + implementation platform('androidx.compose:compose-bom:2024.04.00') + implementation "androidx.compose.ui:ui:1.6.5" + implementation "androidx.compose.ui:ui-tooling-preview:1.6.5" implementation 'androidx.compose.material3:material3' - implementation platform('androidx.compose:compose-bom:2024.02.02') + implementation platform('androidx.compose:compose-bom:2024.04.00') implementation 'androidx.compose.ui:ui-graphics' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - androidTestImplementation platform('androidx.compose:compose-bom:2024.02.02') - androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.6.3" - androidTestImplementation platform('androidx.compose:compose-bom:2024.02.02') - debugImplementation "androidx.compose.ui:ui-tooling:1.6.3" - debugImplementation "androidx.compose.ui:ui-test-manifest:1.6.3" + androidTestImplementation platform('androidx.compose:compose-bom:2024.04.00') + androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.6.5" + androidTestImplementation platform('androidx.compose:compose-bom:2024.04.00') + debugImplementation "androidx.compose.ui:ui-tooling:1.6.5" + debugImplementation "androidx.compose.ui:ui-test-manifest:1.6.5" implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.appcompat:appcompat-resources:1.6.1" @@ -82,7 +82,7 @@ dependencies { runtimeOnly 'androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0' // https://mvnrepository.com/artifact/androidx.fragment/fragment-ktx - runtimeOnly 'androidx.fragment:fragment-ktx:1.7.0-alpha10' + runtimeOnly 'androidx.fragment:fragment-ktx:1.7.0-rc01' // https://mvnrepository.com/artifact/androidx.compose.material3/material3 runtimeOnly 'androidx.compose.material3:material3:1.2.1' @@ -124,7 +124,7 @@ dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.5.0' - implementation 'com.github.dutwrapper:dutwrapper-java:238ab7398f' + implementation 'com.github.dutwrapper:dutwrapper-java:v1.9.0' implementation 'com.google.android.material:material:1.11.0' From 40c5dc4d3098a3294454668b7fc07d0b024355b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BA=BF=20T=C3=B9ng?= <47247560+ZoeMeow1027@users.noreply.github.com> Date: Thu, 11 Apr 2024 02:17:46 +0700 Subject: [PATCH 09/21] Optimize and Main View navigation bar. --- CREDIT.md | 16 +- README.md | 8 +- .../dutschedule/activity/BaseActivity.kt | 6 + .../dutschedule/activity/HelpActivity.kt | 2 +- .../dutschedule/activity/MainActivity.kt | 81 +++++-- .../dutschedule/activity/SettingsActivity.kt | 19 +- .../{helpandexternallink => }/HelpLinkInfo.kt | 2 +- .../zoemeow/dutschedule/model/NavBarItem.kt | 45 ++++ .../model/{ => account}/DUTAccountInstance.kt | 8 +- .../model/{ => news}/DUTNewsInstance.kt | 8 +- .../dutschedule/model/settings/AppSettings.kt | 5 + .../service/NewsBackgroundUpdateService.kt | 2 +- .../helpandexternallink/HelpLinkClickable.kt | 2 +- .../notification/NotificationDialogBox.kt | 173 +++++++------- .../main/notification/NotificationItem.kt | 6 +- .../dialog/DialogAppBackgroundSettings.kt | 2 +- .../settings/dialog/DialogAppThemeSettings.kt | 2 +- .../dialog/DialogSchoolYearSettings.kt | 2 +- .../dutschedule/ui/view/account/MainView.kt | 130 ++++++---- .../ui/view/main/MainViewDashboard.kt | 94 ++++---- .../ui/view/main/MainViewTabbed.kt | 207 ++++++++++++---- .../dutschedule/ui/view/news/MainView.kt | 207 ++++++++-------- .../ui/view/settings/ExperimentSettings.kt | 43 ++++ .../dutschedule/ui/view/settings/MainView.kt | 224 +++++++++++------- .../dutschedule/utils/FunctionExtension.kt | 30 +++ .../dutschedule/viewmodel/MainViewModel.kt | 18 +- app/src/main/res/drawable/blank_24.xml | 5 + app/src/main/res/drawable/github_mark_24.xml | 5 + .../drawable/google_fonts_device_reset_24.xml | 9 + .../main/res/drawable/ic_baseline_info_24.xml | 4 +- build.gradle | 2 +- 31 files changed, 908 insertions(+), 459 deletions(-) rename app/src/main/java/io/zoemeow/dutschedule/model/{helpandexternallink => }/HelpLinkInfo.kt (97%) create mode 100644 app/src/main/java/io/zoemeow/dutschedule/model/NavBarItem.kt rename app/src/main/java/io/zoemeow/dutschedule/model/{ => account}/DUTAccountInstance.kt (98%) rename app/src/main/java/io/zoemeow/dutschedule/model/{ => news}/DUTNewsInstance.kt (97%) create mode 100644 app/src/main/java/io/zoemeow/dutschedule/utils/FunctionExtension.kt create mode 100644 app/src/main/res/drawable/blank_24.xml create mode 100644 app/src/main/res/drawable/github_mark_24.xml create mode 100644 app/src/main/res/drawable/google_fonts_device_reset_24.xml diff --git a/CREDIT.md b/CREDIT.md index 591582f..3e89518 100644 --- a/CREDIT.md +++ b/CREDIT.md @@ -1,19 +1,25 @@ # DutSchedule - CREDIT AND COPYRIGHT -- DISCLAIMER: This project - DutSchedule - is not affiliated with Da Nang University of Technology. +## DISCLAIMER +- This project - DutSchedule - is not affiliated with Da Nang University of Technology. - DUT, Da Nang University of Technology, web materials and web contents are trademarks and copyrights of Da Nang University of Technology school. +- GitHub, GitHub mark and icon are trademarks and copyrights of GitHub, Inc. ## Google and Android -- Google and Android are trademarks of Google LLC. +- Google, Android and its icon are trademarks and copyrights of Google LLC. ## Google Accompanist - https://github.com/google/accompanist -- Google Accompanist is licensed under the (Apache License 2.0)[https://github.com/google/accompanist/blob/main/LICENSE]. +- Licensed under the [Apache License 2.0](https://github.com/google/accompanist/blob/main/LICENSE). ## Google Gson - https://github.com/google/gson -- Google Gson is licensed under the (Apache License 2.0)[https://github.com/google/gson/blob/main/LICENSE]. +- Licensed under the [Apache License 2.0](https://github.com/google/gson/blob/main/LICENSE). ## Jsoup - https://github.com/jhy/jsoup/ -- Google Gson is licensed under the (MIT license)[https://github.com/jhy/jsoup/blob/master/LICENSE]. +- Licensed under the [MIT license](https://github.com/jhy/jsoup/blob/master/LICENSE). + +## timeago +- https://github.com/marlonlom/timeago +- Licensed under the [Apache License 2.0](https://github.com/marlonlom/timeago/blob/master/LICENSE). \ No newline at end of file diff --git a/README.md b/README.md index 069920d..82474d3 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ A unofficial Android app to provide better UI from [sv.dut.udn.vn](http://sv.dut # Build app yourself - Required Gradle: 8.5 - - Older version of Gradle will be failed while building. + - Older version of Gradle may be failed while building. - If you open project with Android Studio, make sure your IDE support Gradle [Gradle](https://gradle.org/releases/) above, which can be fixed by upgrading your IDE. After that, just build and run app normally as you do with another Android project. -- If you want to build app without IDE, just type command as you build another gradle project (note that you need to [Gradle](https://gradle.org/releases/) installed first): +- If you want to build app without IDE, just type command as you build another gradle project (note that you still need to [Gradle](https://gradle.org/releases/) installed first): ``` Build: gradlew build @@ -47,10 +47,6 @@ If you want to: ### I'm got issue with this app. Which place can I reproduce issue for you? -- You can report issue via [Issue tab](https://github.com/ZoeMeow1027/DutSchedule/issues) on this repository. - -# Known issues - If you found a issue, you can report this via [issue tab](https://github.com/ZoeMeow1027/DutSchedule/issues) on this repository. - Global news and subject news were shown not correctly. - You just need to refresh news and this will clear old and get latest one automatically. diff --git a/app/src/main/java/io/zoemeow/dutschedule/activity/BaseActivity.kt b/app/src/main/java/io/zoemeow/dutschedule/activity/BaseActivity.kt index 3529c5b..d80cb98 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/activity/BaseActivity.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/activity/BaseActivity.kt @@ -212,6 +212,12 @@ abstract class BaseActivity: ComponentActivity() { } } + fun clearSnackBar() { + snackBarScope.launch { + snackBarHostState.currentSnackbarData?.dismiss() + } + } + fun openLink( url: String, context: Context, diff --git a/app/src/main/java/io/zoemeow/dutschedule/activity/HelpActivity.kt b/app/src/main/java/io/zoemeow/dutschedule/activity/HelpActivity.kt index 38f4e4e..862ce87 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/activity/HelpActivity.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/activity/HelpActivity.kt @@ -38,7 +38,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import dagger.hilt.android.AndroidEntryPoint -import io.zoemeow.dutschedule.model.helpandexternallink.HelpLinkInfo +import io.zoemeow.dutschedule.model.HelpLinkInfo import io.zoemeow.dutschedule.ui.component.helpandexternallink.HelpLinkClickable @AndroidEntryPoint diff --git a/app/src/main/java/io/zoemeow/dutschedule/activity/MainActivity.kt b/app/src/main/java/io/zoemeow/dutschedule/activity/MainActivity.kt index c6aef47..9037b98 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/activity/MainActivity.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/activity/MainActivity.kt @@ -3,13 +3,17 @@ package io.zoemeow.dutschedule.activity import android.content.Context import android.content.Intent import android.util.Log +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import dagger.hilt.android.AndroidEntryPoint +import io.zoemeow.dutschedule.model.settings.BackgroundImageOption import io.zoemeow.dutschedule.service.BaseService import io.zoemeow.dutschedule.service.NewsBackgroundUpdateService import io.zoemeow.dutschedule.ui.view.main.MainViewDashboard +import io.zoemeow.dutschedule.ui.view.main.MainViewTabbed +import io.zoemeow.dutschedule.utils.BackgroundImageUtil import io.zoemeow.dutschedule.utils.NotificationsUtil @AndroidEntryPoint @@ -25,6 +29,34 @@ class MainActivity : BaseActivity() { NotificationsUtil.initializeNotificationChannel(this) } + // When active + val pickMedia = + registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> + // Callback is invoked after the user selects a media item or closes the photo picker. + if (uri != null) { + Log.d("PhotoPicker", "Selected URI: $uri") + getMainViewModel().appSettings.value = getMainViewModel().appSettings.value.clone( + backgroundImage = BackgroundImageOption.None + ) + getMainViewModel().saveSettings( + onCompleted = { + BackgroundImageUtil.saveImageToAppData(this, uri) + Log.d("PhotoPicker", "Copied!") + getMainViewModel().appSettings.value = getMainViewModel().appSettings.value.clone( + backgroundImage = BackgroundImageOption.PickFileFromMedia + ) + getMainViewModel().saveSettings( + onCompleted = { + Log.d("PhotoPicker", "Copied!") + } + ) + } + ) + } else { + Log.d("PhotoPicker", "No media selected") + } + } + @Composable override fun OnMainView( context: Context, @@ -32,26 +64,35 @@ class MainActivity : BaseActivity() { containerColor: Color, contentColor: Color ) { - MainViewDashboard( - context = context, - snackBarHostState = snackBarHostState, - containerColor = containerColor, - contentColor = contentColor, - newsClicked = { - context.startActivity(Intent(context, NewsActivity::class.java)) - }, - accountClicked = { - context.startActivity(Intent(context, AccountActivity::class.java)) - }, - settingsClicked = { - context.startActivity(Intent(context, SettingsActivity::class.java)) - }, - externalLinkClicked = { - val intent = Intent(context, HelpActivity::class.java) - intent.action = "view_externallink" - context.startActivity(intent) - } - ) + if (getMainViewModel().appSettings.value.mainScreenDashboardView) { + MainViewDashboard( + context = context, + snackBarHostState = snackBarHostState, + containerColor = containerColor, + contentColor = contentColor, + newsClicked = { + context.startActivity(Intent(context, NewsActivity::class.java)) + }, + accountClicked = { + context.startActivity(Intent(context, AccountActivity::class.java)) + }, + settingsClicked = { + context.startActivity(Intent(context, SettingsActivity::class.java)) + }, + externalLinkClicked = { + val intent = Intent(context, HelpActivity::class.java) + intent.action = "view_externallink" + context.startActivity(intent) + } + ) + } else { + MainViewTabbed( + context = context, + snackBarHostState = snackBarHostState, + containerColor = containerColor, + contentColor = contentColor + ) + } } override fun onStop() { diff --git a/app/src/main/java/io/zoemeow/dutschedule/activity/SettingsActivity.kt b/app/src/main/java/io/zoemeow/dutschedule/activity/SettingsActivity.kt index 379efd5..7e20a9b 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/activity/SettingsActivity.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/activity/SettingsActivity.kt @@ -27,12 +27,23 @@ class SettingsActivity : BaseActivity() { // Callback is invoked after the user selects a media item or closes the photo picker. if (uri != null) { Log.d("PhotoPicker", "Selected URI: $uri") - BackgroundImageUtil.saveImageToAppData(this, uri) getMainViewModel().appSettings.value = getMainViewModel().appSettings.value.clone( - backgroundImage = BackgroundImageOption.PickFileFromMedia + backgroundImage = BackgroundImageOption.None + ) + getMainViewModel().saveSettings( + onCompleted = { + BackgroundImageUtil.saveImageToAppData(this, uri) + Log.d("PhotoPicker", "Copied!") + getMainViewModel().appSettings.value = getMainViewModel().appSettings.value.clone( + backgroundImage = BackgroundImageOption.PickFileFromMedia + ) + getMainViewModel().saveSettings( + onCompleted = { + Log.d("PhotoPicker", "Copied!") + } + ) + } ) - getMainViewModel().saveSettings() - Log.d("PhotoPicker", "Copied!") } else { Log.d("PhotoPicker", "No media selected") } diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/helpandexternallink/HelpLinkInfo.kt b/app/src/main/java/io/zoemeow/dutschedule/model/HelpLinkInfo.kt similarity index 97% rename from app/src/main/java/io/zoemeow/dutschedule/model/helpandexternallink/HelpLinkInfo.kt rename to app/src/main/java/io/zoemeow/dutschedule/model/HelpLinkInfo.kt index f4a4f7d..bc56ecb 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/helpandexternallink/HelpLinkInfo.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/HelpLinkInfo.kt @@ -1,4 +1,4 @@ -package io.zoemeow.dutschedule.model.helpandexternallink +package io.zoemeow.dutschedule.model data class HelpLinkInfo( val title: String, diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/NavBarItem.kt b/app/src/main/java/io/zoemeow/dutschedule/model/NavBarItem.kt new file mode 100644 index 0000000..82c34f0 --- /dev/null +++ b/app/src/main/java/io/zoemeow/dutschedule/model/NavBarItem.kt @@ -0,0 +1,45 @@ +package io.zoemeow.dutschedule.model + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material.icons.filled.Settings +import androidx.compose.ui.graphics.vector.ImageVector +import io.zoemeow.dutschedule.R + +data class NavBarItem( + val title: String, + val icon: ImageVector? = null, + val resourceIconId: Int? = null, + val route: String +) { + companion object { + val news = NavBarItem( + title = "News", + resourceIconId = R.drawable.ic_baseline_newspaper_24, + route = "news" + ) + + val account = NavBarItem( + title = "Account", + icon = Icons.Default.AccountCircle, + route = "account" + ) + + val notification = NavBarItem( + title = "Notifications", + icon = Icons.Default.Notifications, + route = "notifications" + ) + + val settings = NavBarItem( + title = "Settings", + icon = Icons.Default.Settings, + route = "settings" + ) + + fun getAll(): List { + return listOf(news, account, notification, settings) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/DUTAccountInstance.kt b/app/src/main/java/io/zoemeow/dutschedule/model/account/DUTAccountInstance.kt similarity index 98% rename from app/src/main/java/io/zoemeow/dutschedule/model/DUTAccountInstance.kt rename to app/src/main/java/io/zoemeow/dutschedule/model/account/DUTAccountInstance.kt index 0d18798..fe4dbbd 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/DUTAccountInstance.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/account/DUTAccountInstance.kt @@ -1,4 +1,4 @@ -package io.zoemeow.dutschedule.model +package io.zoemeow.dutschedule.model.account import android.util.Log import androidx.compose.runtime.MutableState @@ -7,9 +7,9 @@ import io.dutwrapper.dutwrapper.model.accounts.AccountInformation import io.dutwrapper.dutwrapper.model.accounts.SubjectFeeItem import io.dutwrapper.dutwrapper.model.accounts.SubjectScheduleItem import io.dutwrapper.dutwrapper.model.accounts.trainingresult.AccountTrainingStatus -import io.zoemeow.dutschedule.model.account.AccountAuth -import io.zoemeow.dutschedule.model.account.AccountSession -import io.zoemeow.dutschedule.model.account.SchoolYearItem +import io.zoemeow.dutschedule.model.ProcessState +import io.zoemeow.dutschedule.model.VariableListState +import io.zoemeow.dutschedule.model.VariableState import io.zoemeow.dutschedule.repository.DutRequestRepository import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/DUTNewsInstance.kt b/app/src/main/java/io/zoemeow/dutschedule/model/news/DUTNewsInstance.kt similarity index 97% rename from app/src/main/java/io/zoemeow/dutschedule/model/DUTNewsInstance.kt rename to app/src/main/java/io/zoemeow/dutschedule/model/news/DUTNewsInstance.kt index d513f15..3a50467 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/DUTNewsInstance.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/news/DUTNewsInstance.kt @@ -1,9 +1,7 @@ -package io.zoemeow.dutschedule.model +package io.zoemeow.dutschedule.model.news -import androidx.compose.runtime.referentialEqualityPolicy -import io.zoemeow.dutschedule.model.news.NewsFetchType -import io.zoemeow.dutschedule.model.news.NewsGlobalItem -import io.zoemeow.dutschedule.model.news.NewsSubjectItem +import io.zoemeow.dutschedule.model.ProcessState +import io.zoemeow.dutschedule.model.VariableListState import io.zoemeow.dutschedule.repository.DutRequestRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/settings/AppSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/model/settings/AppSettings.kt index b21ad3d..c4cac11 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/settings/AppSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/settings/AppSettings.kt @@ -5,6 +5,9 @@ import io.zoemeow.dutschedule.model.account.SchoolYearItem import java.io.Serializable data class AppSettings( + @SerializedName("appsettings.layout.mainview.dashboardview") + val mainScreenDashboardView: Boolean = true, + @SerializedName("appsettings.appearance.thememode") val themeMode: ThemeMode = ThemeMode.FollowDeviceTheme, @@ -54,6 +57,7 @@ data class AppSettings( val currentSchoolYear: SchoolYearItem = SchoolYearItem(), ): Serializable { fun clone( + mainScreenDashboardView: Boolean? = null, themeMode: ThemeMode? = null, dynamicColor: Boolean? = null, blackBackground: Boolean? = null, @@ -68,6 +72,7 @@ data class AppSettings( currentSchoolYear: SchoolYearItem? = null ): AppSettings { return AppSettings( + mainScreenDashboardView = mainScreenDashboardView ?: this.mainScreenDashboardView, themeMode = themeMode ?: this.themeMode, dynamicColor = dynamicColor ?: this.dynamicColor, blackBackground = blackBackground ?: this.blackBackground, diff --git a/app/src/main/java/io/zoemeow/dutschedule/service/NewsBackgroundUpdateService.kt b/app/src/main/java/io/zoemeow/dutschedule/service/NewsBackgroundUpdateService.kt index d9d8f47..e0ba264 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/service/NewsBackgroundUpdateService.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/service/NewsBackgroundUpdateService.kt @@ -13,7 +13,7 @@ import io.dutwrapper.dutwrapper.model.news.NewsGlobalItem import io.dutwrapper.dutwrapper.model.news.NewsSubjectItem import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.PermissionRequestActivity -import io.zoemeow.dutschedule.model.DUTNewsInstance +import io.zoemeow.dutschedule.model.news.DUTNewsInstance import io.zoemeow.dutschedule.model.NotificationHistory import io.zoemeow.dutschedule.model.ProcessState import io.zoemeow.dutschedule.model.news.NewsFetchType diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/helpandexternallink/HelpLinkClickable.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/helpandexternallink/HelpLinkClickable.kt index 470a5be..7be17d2 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/helpandexternallink/HelpLinkClickable.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/helpandexternallink/HelpLinkClickable.kt @@ -10,7 +10,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import io.zoemeow.dutschedule.model.helpandexternallink.HelpLinkInfo +import io.zoemeow.dutschedule.model.HelpLinkInfo @Composable fun HelpLinkClickable( diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationDialogBox.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationDialogBox.kt index ef752bd..5acb354 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationDialogBox.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationDialogBox.kt @@ -1,8 +1,10 @@ package io.zoemeow.dutschedule.ui.component.main.notification +import android.graphics.Bitmap import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -17,33 +19,45 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Clear +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.model.NotificationHistory import io.zoemeow.dutschedule.utils.CustomDateUtil +@OptIn(ExperimentalMaterial3Api::class) @Composable fun NotificationDialogBox( - modifier: Modifier = Modifier, isVisible: Boolean = false, itemList: List, - snackbarHost: (@Composable () -> Unit)? = null, + containerColor: Color, + contentColor: Color, + backgroundImage: Bitmap? = null, + snackBarHostState: SnackbarHostState? = null, onDismiss: (() -> Unit)? = null, onClick: ((NotificationHistory) -> Unit)? = null, onClear: ((NotificationHistory) -> Unit)? = null, onClearAll: (() -> Unit)? = null, height: Float = 0.7f, - opacity: Float = 1f + opacity: Float = 1f, ) { AnimatedVisibility( visible = isVisible, @@ -58,97 +72,86 @@ fun NotificationDialogBox( }, ), content = { - Column( - modifier = modifier.fillMaxSize(), - verticalArrangement = Arrangement.Bottom, + backgroundImage?.let { + Image( + modifier = Modifier.fillMaxSize(), + bitmap = it.asImageBitmap(), + contentDescription = "background_image", + contentScale = ContentScale.Crop + ) + } + Scaffold( + containerColor = containerColor, + contentColor = contentColor, + topBar = { + TopAppBar( + title = { Text(text = "Notifications") }, + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), + actions = { + if (itemList.isNotEmpty()) { + IconButton( + onClick = { onClearAll?.let { it() } }, + content = { + Icon(ImageVector.vectorResource(id = R.drawable.ic_baseline_clear_all_24), "") + } + ) + Spacer(modifier = Modifier.size(3.dp)) + } + if (onDismiss != null) { + IconButton( + onClick = { onDismiss() }, + content = { + Icon(Icons.Default.Clear, "Close") + } + ) + } + } + ) + }, + snackbarHost = { snackBarHostState?.let { SnackbarHost(hostState = it) } }, content = { - Surface( + Column( modifier = Modifier .fillMaxWidth() - .fillMaxHeight(height), - color = MaterialTheme.colorScheme.surface.copy(alpha = opacity), - content = { - Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(height) - .padding(top = 5.dp) - .padding(horizontal = 15.dp) - ) { - Row( + .fillMaxHeight(height) + .padding(it) + .padding(horizontal = 15.dp) + ) { + Column( + modifier = Modifier.verticalScroll(rememberScrollState()), + ) { + if (itemList.isEmpty()) { + Box( modifier = Modifier - .fillMaxWidth() - .padding(vertical = 3.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + .fillMaxSize() + .padding(top = 15.dp), + contentAlignment = Alignment.Center ) { - Text( - "Notifications", - style = MaterialTheme.typography.headlineSmall, - ) - Row { - if (itemList.isNotEmpty()) { - IconButton( - onClick = { onClearAll?.let { it() } }, - content = { - Icon(ImageVector.vectorResource(id = R.drawable.ic_baseline_clear_all_24), "") - } - ) - Spacer(modifier = Modifier.size(3.dp)) - } - IconButton( - onClick = { onDismiss?.let { it() } }, - content = { - Icon(Icons.Default.Clear, "Close") - } - ) - } + Text("No notifications") } - Column( - modifier = Modifier.verticalScroll(rememberScrollState()), - ) { - if (itemList.isEmpty()) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(top = 15.dp), - contentAlignment = Alignment.Center - ) { - Text("No notifications") + } else { + itemList.groupBy { p -> p.timestamp } + .toSortedMap(compareByDescending { it }) + .forEach(action = { group -> + Text( + CustomDateUtil.unixToDuration(group.key), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(top = 5.dp, bottom = 4.dp) + ) + group.value.forEach { item -> + NotificationItem( + modifier = Modifier.padding(top = 2.dp, bottom = 5.dp), + opacity = opacity, + onClick = { onClick?.let { it(item) } }, + onClear = { onClear?.let { it(item) } }, + item = item + ) } - } else { - itemList.groupBy { p -> p.timestamp } - .toSortedMap(compareByDescending { it }) - .forEach(action = { group -> - Text( - CustomDateUtil.unixToDuration(group.key), - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(top = 5.dp, bottom = 4.dp) - ) - group.value.forEach { item -> - NotificationItem( - modifier = Modifier.padding(bottom = 5.dp), - opacity = opacity, - onClick = { onClick?.let { it(item) } }, - onClear = { onClear?.let { it(item) } }, - item = item - ) - } - }) - } - Spacer(modifier = Modifier.size(9.dp)) - } - } - Column( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(height), - verticalArrangement = Arrangement.Bottom - ) { - snackbarHost?.let { it() } + }) } + Spacer(modifier = Modifier.size(12.dp)) } - ) + } } ) } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationItem.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationItem.kt index 151c7eb..014c169 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationItem.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationItem.kt @@ -41,7 +41,7 @@ fun NotificationItem( color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = opacity), content = { Row( - modifier = Modifier.fillMaxWidth().padding(horizontal = 6.dp, vertical = 3.dp), + modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 3.dp), verticalAlignment = Alignment.CenterVertically, content = { Column( @@ -61,14 +61,14 @@ fun NotificationItem( } Text( item.title, - style = MaterialTheme.typography.titleSmall, + style = MaterialTheme.typography.titleMedium, overflow = TextOverflow.Ellipsis, maxLines = 2 ) Spacer(modifier = Modifier.size(3.dp)) Text( item.description, - style = MaterialTheme.typography.bodySmall, + style = MaterialTheme.typography.bodyMedium, overflow = TextOverflow.Ellipsis, maxLines = 3 ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogAppBackgroundSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogAppBackgroundSettings.kt index 55ffebe..01ffc57 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogAppBackgroundSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogAppBackgroundSettings.kt @@ -19,7 +19,7 @@ import io.zoemeow.dutschedule.ui.component.base.DialogBase import io.zoemeow.dutschedule.ui.component.base.DialogRadioButton @Composable -fun SettingsActivity.DialogAppBackgroundSettings( +fun DialogAppBackgroundSettings( context: Context, isVisible: Boolean = false, value: BackgroundImageOption, diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogAppThemeSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogAppThemeSettings.kt index ccd2268..44c2fdb 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogAppThemeSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogAppThemeSettings.kt @@ -23,7 +23,7 @@ import io.zoemeow.dutschedule.ui.component.base.DialogCheckboxButton import io.zoemeow.dutschedule.ui.component.base.DialogRadioButton @Composable -fun SettingsActivity.DialogAppThemeSettings( +fun DialogAppThemeSettings( isVisible: Boolean = false, themeModeValue: ThemeMode, dynamicColorEnabled: Boolean, diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogSchoolYearSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogSchoolYearSettings.kt index e5cab59..c284807 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogSchoolYearSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogSchoolYearSettings.kt @@ -24,7 +24,7 @@ import io.zoemeow.dutschedule.ui.component.base.OutlinedTextBox @OptIn(ExperimentalMaterial3Api::class) @Composable -fun SettingsActivity.DialogSchoolYearSettings( +fun DialogSchoolYearSettings( isVisible: Boolean = false, dismissRequested: (() -> Unit)? = null, currentSchoolYearItem: SchoolYearItem, diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt index 4977f31..72008df 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt @@ -37,17 +37,47 @@ import io.zoemeow.dutschedule.ui.component.account.AccountInfoBanner import io.zoemeow.dutschedule.ui.component.account.LoginBox import io.zoemeow.dutschedule.ui.component.account.LogoutDialog import io.zoemeow.dutschedule.ui.component.base.ButtonBase +import io.zoemeow.dutschedule.utils.openLink +import io.zoemeow.dutschedule.viewmodel.MainViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -@OptIn(ExperimentalMaterial3Api::class) @Composable fun AccountActivity.MainView( context: Context, snackBarHostState: SnackbarHostState, containerColor: Color, contentColor: Color +) { + AccountMainView( + context = context, + snackBarHostState = snackBarHostState, + containerColor = containerColor, + contentColor = contentColor, + componentBackgroundAlpha = getControlBackgroundAlpha(), + mainViewModel = getMainViewModel(), + onShowSnackBar = { text, clearPrevious, actionText, action -> + showSnackBar(text = text, clearPrevious = clearPrevious, actionText = actionText, action = action) + }, + onBack = { + setResult(ComponentActivity.RESULT_OK) + finish() + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AccountMainView( + context: Context, + snackBarHostState: SnackbarHostState? = null, + containerColor: Color, + contentColor: Color, + componentBackgroundAlpha: Float = 1f, + mainViewModel: MainViewModel, + onShowSnackBar: ((String, Boolean, String?, (() -> Unit)?) -> Unit)? = null, + onBack: (() -> Unit)? = null ) { val loginDialogVisible = remember { mutableStateOf(false) } val loginDialogEnabled = remember { mutableStateOf(true) } @@ -55,7 +85,7 @@ fun AccountActivity.MainView( Scaffold( modifier = Modifier.fillMaxSize(), - snackbarHost = { SnackbarHost(hostState = snackBarHostState) }, + snackbarHost = { snackBarHostState?.let { SnackbarHost(hostState = it) } }, containerColor = containerColor, contentColor = contentColor, topBar = { @@ -63,24 +93,25 @@ fun AccountActivity.MainView( title = { Text("Account") }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), navigationIcon = { - IconButton( - onClick = { - setResult(ComponentActivity.RESULT_OK) - finish() - }, - content = { - Icon( - Icons.AutoMirrored.Filled.ArrowBack, - "", - modifier = Modifier.size(25.dp) - ) - } - ) + if (onBack != null) { + IconButton( + onClick = { + onBack() + }, + content = { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + "", + modifier = Modifier.size(25.dp) + ) + } + ) + } } ) }, content = { - getMainViewModel().accountSession.accountSession.processState.value.let { state -> + mainViewModel.accountSession.accountSession.processState.value.let { state -> LoginBox( modifier = Modifier .padding(it) @@ -90,12 +121,11 @@ fun AccountActivity.MainView( isControlEnabled = state != ProcessState.Running, isLoggedInBefore = state == ProcessState.Failed, clearOnInvisible = true, - opacity = getControlBackgroundAlpha(), + opacity = componentBackgroundAlpha, onForgotPass = { - openLink( + context.openLink( url = "https://www.facebook.com/ctsvdhbkdhdn/posts/pfbid02G5sza1p8x7tEJ7S1Cac6a66EW3exgxLNmR9L26RZ8sX8xjhbEnguoeAXms31i7oxl", - context = context, - customTab = getMainViewModel().appSettings.value.openLinkInsideApp + customTab = mainViewModel.appSettings.value.openLinkInsideApp ) }, onClearLogin = { }, @@ -103,12 +133,14 @@ fun AccountActivity.MainView( run { CoroutineScope(Dispatchers.IO).launch { loginDialogEnabled.value = false - showSnackBar( - text = "Logging you in...", - clearPrevious = true, - ) + onShowSnackBar?.let { it( + "Logging you in...", + true, + null, + null + ) } } - getMainViewModel().accountSession.login( + mainViewModel.accountSession.login( accountAuth = AccountAuth( username = username, password = password, @@ -119,18 +151,22 @@ fun AccountActivity.MainView( true -> { loginDialogEnabled.value = true loginDialogVisible.value = false - getMainViewModel().accountSession.reLogin() - showSnackBar( - text = "Successfully logged in!", - clearPrevious = true, - ) + mainViewModel.accountSession.reLogin() + onShowSnackBar?.let { it( + "Successfully logged in!", + true, + null, + null + ) } } false -> { loginDialogEnabled.value = true - showSnackBar( - text = "Login failed! Please check your login information and try again.", - clearPrevious = true, - ) + onShowSnackBar?.let { it( + "Login failed! Please check your login information and try again.", + true, + null, + null + ) } } } } @@ -145,9 +181,9 @@ fun AccountActivity.MainView( .padding(it) .verticalScroll(rememberScrollState()), content = { - getMainViewModel().accountSession.accountInformation.let { accInfo -> + mainViewModel.accountSession.accountInformation.let { accInfo -> AccountInfoBanner( - opacity = getControlBackgroundAlpha(), + opacity = componentBackgroundAlpha, padding = PaddingValues(10.dp), isLoading = accInfo.processState.value == ProcessState.Running, username = accInfo.data.value?.studentId ?: "(unknown)", @@ -162,7 +198,7 @@ fun AccountActivity.MainView( modifierInside = Modifier.padding(vertical = 7.dp), content = { Text("Subject Information") }, horizontalArrangement = Arrangement.Start, - opacity = getControlBackgroundAlpha(), + opacity = componentBackgroundAlpha, clicked = { val intent = Intent(context, AccountActivity::class.java) intent.action = "subject_information" @@ -176,7 +212,7 @@ fun AccountActivity.MainView( modifierInside = Modifier.padding(vertical = 7.dp), content = { Text("Subject Fee") }, horizontalArrangement = Arrangement.Start, - opacity = getControlBackgroundAlpha(), + opacity = componentBackgroundAlpha, clicked = { val intent = Intent(context, AccountActivity::class.java) intent.action = "subject_fee" @@ -190,7 +226,7 @@ fun AccountActivity.MainView( modifierInside = Modifier.padding(vertical = 7.dp), content = { Text("Account Information") }, horizontalArrangement = Arrangement.Start, - opacity = getControlBackgroundAlpha(), + opacity = componentBackgroundAlpha, clicked = { val intent = Intent(context, AccountActivity::class.java) intent.action = "acc_info" @@ -204,7 +240,7 @@ fun AccountActivity.MainView( modifierInside = Modifier.padding(vertical = 7.dp), content = { Text("Account Training Result") }, horizontalArrangement = Arrangement.Start, - opacity = getControlBackgroundAlpha(), + opacity = componentBackgroundAlpha, clicked = { val intent = Intent(context, AccountActivity::class.java) intent.action = "acc_training_result" @@ -218,7 +254,7 @@ fun AccountActivity.MainView( modifierInside = Modifier.padding(vertical = 7.dp), content = { Text("Logout") }, horizontalArrangement = Arrangement.Start, - opacity = getControlBackgroundAlpha(), + opacity = componentBackgroundAlpha, clicked = { logoutDialogVisible.value = true } @@ -235,12 +271,14 @@ fun AccountActivity.MainView( logoutClicked = { run { logoutDialogVisible.value = false - getMainViewModel().accountSession.logout( + mainViewModel.accountSession.logout( onCompleted = { - showSnackBar( - text = "Successfully logout!", - clearPrevious = true, - ) + onShowSnackBar?.let { it( + "Successfully logout!", + true, + null, + null + ) } } ) } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt index b14288c..d8310a3 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt @@ -44,15 +44,18 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.AccountActivity +import io.zoemeow.dutschedule.activity.BaseActivity import io.zoemeow.dutschedule.activity.MainActivity import io.zoemeow.dutschedule.activity.NewsActivity import io.zoemeow.dutschedule.model.CustomClock import io.zoemeow.dutschedule.model.ProcessState +import io.zoemeow.dutschedule.model.settings.BackgroundImageOption import io.zoemeow.dutschedule.ui.component.main.DateAndTimeSummaryItem import io.zoemeow.dutschedule.ui.component.main.LessonTodaySummaryItem import io.zoemeow.dutschedule.ui.component.main.SchoolNewsSummaryItem import io.zoemeow.dutschedule.ui.component.main.UpdateAvailableSummaryItem import io.zoemeow.dutschedule.ui.component.main.notification.NotificationDialogBox +import io.zoemeow.dutschedule.utils.BackgroundImageUtil import io.zoemeow.dutschedule.utils.CustomDateUtil import kotlinx.datetime.Clock import kotlinx.datetime.LocalDateTime @@ -313,53 +316,64 @@ fun MainActivity.MainViewDashboard( ) } ) - NotificationDialogBox( - modifier = Modifier.padding(padding), - itemList = getMainViewModel().notificationHistory, - isVisible = isNotificationOpened.value, - onDismiss = { isNotificationOpened.value = false }, - onClick = { item -> - if (listOf(1, 2).contains(item.tag)) { - Intent(context, NewsActivity::class.java).also { - it.action = "activity_detail" - for (map1 in item.parameters) { - it.putExtra(map1.key, map1.value) - } - context.startActivity(it) - } + } + ) + NotificationDialogBox( + itemList = getMainViewModel().notificationHistory, + snackBarHostState = snackBarHostState, + isVisible = isNotificationOpened.value, + containerColor = containerColor, + contentColor = contentColor, + backgroundImage = when (getMainViewModel().appSettings.value.backgroundImage) { + BackgroundImageOption.None -> null + BackgroundImageOption.YourCurrentWallpaper -> BackgroundImageUtil.getCurrentWallpaperBackground(context) + BackgroundImageOption.PickFileFromMedia -> BackgroundImageUtil.getImageFromAppData(context) + }, + onDismiss = { + clearSnackBar() + isNotificationOpened.value = false + }, + onClick = { item -> + if (listOf(1, 2).contains(item.tag)) { + Intent(context, NewsActivity::class.java).also { + it.action = "activity_detail" + for (map1 in item.parameters) { + it.putExtra(map1.key, map1.value) } - }, - onClear = { - val itemTemp = it.clone() - getMainViewModel().notificationHistory.remove(it) + context.startActivity(it) + } + } + }, + onClear = { + val itemTemp = it.clone() + getMainViewModel().notificationHistory.remove(it) + getMainViewModel().saveSettings() + showSnackBar( + text = "Deleted notifications!", + actionText = "Undo", + action = { + getMainViewModel().notificationHistory.add(itemTemp) + getMainViewModel().saveSettings() + } + ) + }, + onClearAll = { + showSnackBar( + text = "This action is undone! To confirm, click \"Confirm\" to clear all.", + actionText = "Confirm", + action = { + getMainViewModel().notificationHistory.clear() getMainViewModel().saveSettings() showSnackBar( - text = "Deleted notifications!", - actionText = "Undo", - action = { - getMainViewModel().notificationHistory.add(itemTemp) - getMainViewModel().saveSettings() - } - ) - }, - onClearAll = { - showSnackBar( - text = "This action is undone! To confirm, click \"Confirm\" to clear all.", - actionText = "Confirm", - action = { - getMainViewModel().notificationHistory.clear() - getMainViewModel().saveSettings() - showSnackBar( - text = "Successfully cleared all notifications!", - clearPrevious = true - ) - }, + text = "Successfully cleared all notifications!", clearPrevious = true ) }, - height = 1f + clearPrevious = true ) - } + }, + height = 1f, + opacity = getControlBackgroundAlpha() ) BackHandler(isNotificationOpened.value) { diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewTabbed.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewTabbed.kt index 2dab3a0..715e479 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewTabbed.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewTabbed.kt @@ -1,10 +1,20 @@ package io.zoemeow.dutschedule.ui.view.main +import android.content.Context +import android.content.Intent +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.AccountCircle -import androidx.compose.material.icons.filled.Notifications -import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Info import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.NavigationBar @@ -16,79 +26,184 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource -import io.zoemeow.dutschedule.R +import androidx.navigation.NavGraph.Companion.findStartDestination +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState +import androidx.navigation.compose.rememberNavController import io.zoemeow.dutschedule.activity.MainActivity +import io.zoemeow.dutschedule.activity.NewsActivity +import io.zoemeow.dutschedule.model.NavBarItem +import io.zoemeow.dutschedule.ui.component.main.notification.NotificationDialogBox +import io.zoemeow.dutschedule.ui.view.account.AccountMainView +import io.zoemeow.dutschedule.ui.view.news.NewsMainView +import io.zoemeow.dutschedule.ui.view.settings.SettingsMainView -@OptIn(ExperimentalMaterial3Api::class) @Composable fun MainActivity.MainViewTabbed( + context: Context, snackBarHostState: SnackbarHostState, containerColor: Color, contentColor: Color, ) { - val selectedTab = remember { mutableStateOf("News") } + // Initialize for NavController for main activity + val navController = rememberNavController() + // Nav Route + val backStackEntry by navController.currentBackStackEntryAsState() + val currentRoute = backStackEntry?.destination?.route Scaffold( modifier = Modifier.fillMaxSize(), snackbarHost = { SnackbarHost(hostState = snackBarHostState) }, - containerColor = containerColor, + containerColor = Color.Transparent, contentColor = contentColor, - topBar = { - TopAppBar( - title = { Text("DutSchedule") }, - colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent, scrolledContainerColor = Color.Transparent), - ) - }, + // https://stackoverflow.com/questions/75328833/compose-scaffold-unnecessary-systembar-padding-due-to-windowcompat-setdecorfi + contentWindowInsets = WindowInsets.navigationBars, bottomBar = { NavigationBar( containerColor = containerColor, contentColor = contentColor, content = { - mapOf( - "News" to painterResource(id = R.drawable.ic_baseline_newspaper_24), - "Account" to Icons.Default.AccountCircle, - "Notifications" to Icons.Default.Notifications, - "Settings" to Icons.Default.Settings - ).forEach( + NavBarItem.getAll().forEach( action = { - when (it.value) { - is Painter -> { - NavigationBarItem( - selected = selectedTab.value == it.key, - onClick = { selectedTab.value = it.key }, - icon = { - Icon(painter = it.value as Painter, it.key) - }, - label = { Text(it.key) } - ) - } - is ImageVector -> { - NavigationBarItem( - selected = selectedTab.value == it.key, - onClick = { selectedTab.value = it.key }, - icon = { - Icon(imageVector = it.value as ImageVector, it.key) - }, - label = { Text(it.key) } - ) - } - else -> { } - } + NavigationBarItem( + selected = currentRoute == it.route, + onClick = { + navController.navigate(it.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + }, + icon = { + if (it.resourceIconId != null) { + Icon(painter = painterResource(id = it.resourceIconId), it.title) + } else if (it.icon != null) { + Icon(imageVector = it.icon, it.title) + } else { + Icon(imageVector = Icons.Default.Info, it.title) + } + }, + label = { Text(it.title) } + ) } ) } ) }, content = { - // TODO: Avoid error, must be changed - val d = it + NavHost( + navController = navController, + startDestination = NavBarItem.news.route, + enterTransition = { fadeIn(animationSpec = tween(300)) }, + exitTransition = { fadeOut(animationSpec = tween(300)) }, + popEnterTransition = { fadeIn(animationSpec = tween(300)) }, + popExitTransition = { fadeOut(animationSpec = tween(300)) }, + modifier = Modifier.padding(it) + ) { + // TODO: Add view here! + composable(NavBarItem.news.route) { + NewsMainView( + context = context, + containerColor = containerColor, + contentColor = contentColor, + componentBackgroundAlpha = getControlBackgroundAlpha(), + mainViewModel = getMainViewModel(), + searchRequested = { + val intent = Intent(context, NewsActivity::class.java) + intent.action = "activity_search" + context.startActivity(intent) + } + ) + } + + composable(NavBarItem.account.route) { + AccountMainView( + context = context, + containerColor = containerColor, + contentColor = contentColor, + componentBackgroundAlpha = getControlBackgroundAlpha(), + mainViewModel = getMainViewModel(), + onShowSnackBar = { text, clearPrevious, actionText, action -> + showSnackBar(text = text, clearPrevious = clearPrevious, actionText = actionText, action = action) + } + ) + } + + composable(NavBarItem.notification.route) { + NotificationDialogBox( + itemList = getMainViewModel().notificationHistory.toList(), + snackBarHostState = snackBarHostState, + isVisible = true, + containerColor = containerColor, + contentColor = contentColor, + onClick = { item -> + if (listOf(1, 2).contains(item.tag)) { + Intent(context, NewsActivity::class.java).also { + it.action = "activity_detail" + for (map1 in item.parameters) { + it.putExtra(map1.key, map1.value) + } + context.startActivity(it) + } + } + }, + onClear = { + val itemTemp = it.clone() + getMainViewModel().notificationHistory.remove(it) + getMainViewModel().saveSettings() + showSnackBar( + text = "Deleted notifications!", + actionText = "Undo", + action = { + getMainViewModel().notificationHistory.add(itemTemp) + getMainViewModel().saveSettings() + } + ) + }, + onClearAll = { + showSnackBar( + text = "This action is undone! To confirm, click \"Confirm\" to clear all.", + actionText = "Confirm", + action = { + getMainViewModel().notificationHistory.clear() + getMainViewModel().saveSettings() + showSnackBar( + text = "Successfully cleared all notifications!", + clearPrevious = true + ) + }, + clearPrevious = true + ) + }, + opacity = getControlBackgroundAlpha() + ) + } + + composable(NavBarItem.settings.route) { + SettingsMainView( + context = context, + containerColor = containerColor, + contentColor = contentColor, + componentBackgroundAlpha = getControlBackgroundAlpha(), + mainViewModel = getMainViewModel(), + mediaRequest = { + pickMedia.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) + }, + onShowSnackBar = { text, clearPrevious, actionText, action -> + showSnackBar(text = text, clearPrevious = clearPrevious, actionText = actionText, action = action) + } + ) + } + } } ) } \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt index 13c628f..1667cb8 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt @@ -6,8 +6,10 @@ import androidx.activity.ComponentActivity import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight @@ -24,6 +26,10 @@ import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold +import androidx.compose.material3.ScaffoldDefaults +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text @@ -39,6 +45,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import com.google.gson.Gson +import io.dutwrapper.dutwrapper.model.enums.NewsSearchType import io.dutwrapper.dutwrapper.model.news.NewsGlobalItem import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.NewsActivity @@ -46,12 +53,12 @@ import io.zoemeow.dutschedule.model.ProcessState import io.zoemeow.dutschedule.model.news.NewsFetchType import io.zoemeow.dutschedule.ui.component.base.ButtonBase import io.zoemeow.dutschedule.ui.component.news.NewsListPage +import io.zoemeow.dutschedule.viewmodel.MainViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @Composable fun NewsActivity.MainView( context: Context, @@ -59,13 +66,40 @@ fun NewsActivity.MainView( containerColor: Color, contentColor: Color, searchRequested: (() -> Unit)? = null +) { + NewsMainView( + context = context, + snackBarHostState = snackBarHostState, + containerColor = containerColor, + contentColor = contentColor, + searchRequested = searchRequested, + componentBackgroundAlpha = getControlBackgroundAlpha(), + mainViewModel = getMainViewModel(), + onBack = { + setResult(ComponentActivity.RESULT_OK) + finish() + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) +@Composable +fun NewsMainView( + context: Context, + snackBarHostState: SnackbarHostState? = null, + containerColor: Color, + contentColor: Color, + componentBackgroundAlpha: Float = 1f, + mainViewModel: MainViewModel, + searchRequested: (() -> Unit)? = null, + onBack: (() -> Unit)? = null ) { val pagerState = rememberPagerState(initialPage = 0, pageCount = { 2 }) val scope = rememberCoroutineScope() Scaffold( modifier = Modifier.fillMaxSize(), - snackbarHost = { SnackbarHost(hostState = snackBarHostState) }, + snackbarHost = { snackBarHostState?.let { SnackbarHost(hostState = it) } }, containerColor = containerColor, contentColor = contentColor, topBar = { @@ -73,19 +107,20 @@ fun NewsActivity.MainView( title = { Text(text = "News") }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), navigationIcon = { - IconButton( - onClick = { - setResult(ComponentActivity.RESULT_OK) - finish() - }, - content = { - Icon( - Icons.AutoMirrored.Filled.ArrowBack, - "", - modifier = Modifier.size(25.dp) - ) - } - ) + if (onBack != null) { + IconButton( + onClick = { + onBack() + }, + content = { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + "", + modifier = Modifier.size(25.dp) + ) + } + ) + } }, actions = { IconButton( @@ -102,96 +137,78 @@ fun NewsActivity.MainView( bottomBar = { BottomAppBar( containerColor = BottomAppBarDefaults.containerColor.copy( - alpha = getControlBackgroundAlpha() + alpha = 0f ), actions = { Row( + modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - content = { - ButtonBase( - clicked = { - scope.launch { - pagerState.animateScrollToPage(0) - } - }, - isOutlinedButton = pagerState.currentPage != 0, - content = { - Icon( - imageVector = ImageVector.vectorResource(id = R.drawable.ic_baseline_newspaper_24), - "News global", - modifier = Modifier - .size(30.dp) - .padding(end = 7.dp), - ) - Text("News global") + verticalAlignment = Alignment.CenterVertically + ) { + SingleChoiceSegmentedButtonRow { + SegmentedButton( + shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2), + onClick = { scope.launch { + pagerState.animateScrollToPage(0) + } }, + selected = pagerState.currentPage == 0, + label = { + Text("Global") } ) - ButtonBase( - modifier = Modifier.padding(start = 12.dp), - isOutlinedButton = pagerState.currentPage != 1, - clicked = { - scope.launch { - pagerState.animateScrollToPage(1) - } - }, - content = { - Icon( - painter = painterResource(id = R.drawable.ic_baseline_newspaper_24), - "News subject", - modifier = Modifier - .size(30.dp) - .padding(end = 7.dp), - ) - Text("News subject") + SegmentedButton( + shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2), + onClick = { scope.launch { + pagerState.animateScrollToPage(1) + } }, + selected = pagerState.currentPage == 1, + label = { + Text("Subject") } ) } - ) - } - ) - }, - floatingActionButton = { - if (when (pagerState.currentPage) { - 0 -> { - getMainViewModel().newsInstance.newsGlobal.processState.value != ProcessState.Running } - - 1 -> { - getMainViewModel().newsInstance.newsSubject.processState.value != ProcessState.Running - } - - else -> false - } - ) { - FloatingActionButton( - onClick = { - when (pagerState.currentPage) { + }, + floatingActionButton = { + if (when (pagerState.currentPage) { 0 -> { - getMainViewModel().newsInstance.fetchGlobalNews( - fetchType = NewsFetchType.ClearAndFirstPage, - forceRequest = true - ) + mainViewModel.newsInstance.newsGlobal.processState.value != ProcessState.Running } 1 -> { - getMainViewModel().newsInstance.fetchSubjectNews( - fetchType = NewsFetchType.ClearAndFirstPage, - forceRequest = true - ) + mainViewModel.newsInstance.newsSubject.processState.value != ProcessState.Running } - else -> {} + else -> false } - }, - content = { - Icon(Icons.Default.Refresh, "Refresh") + ) { + FloatingActionButton( + onClick = { + when (pagerState.currentPage) { + 0 -> { + mainViewModel.newsInstance.fetchGlobalNews( + fetchType = NewsFetchType.ClearAndFirstPage, + forceRequest = true + ) + } + + 1 -> { + mainViewModel.newsInstance.fetchSubjectNews( + fetchType = NewsFetchType.ClearAndFirstPage, + forceRequest = true + ) + } + + else -> {} + } + }, + content = { + Icon(Icons.Default.Refresh, "Refresh") + } + ) } - ) - } + } + ) }, content = { padding -> HorizontalPager( @@ -201,9 +218,9 @@ fun NewsActivity.MainView( when (pageIndex) { 0 -> { NewsListPage( - newsList = getMainViewModel().newsInstance.newsGlobal.data.toList(), - processState = getMainViewModel().newsInstance.newsGlobal.processState.value, - opacity = getControlBackgroundAlpha(), + newsList = mainViewModel.newsInstance.newsGlobal.data.toList(), + processState = mainViewModel.newsInstance.newsGlobal.processState.value, + opacity = componentBackgroundAlpha, itemClicked = { newsItem -> context.startActivity( Intent( @@ -218,7 +235,7 @@ fun NewsActivity.MainView( endOfListReached = { CoroutineScope(Dispatchers.Main).launch { withContext(Dispatchers.IO) { - getMainViewModel().newsInstance.fetchGlobalNews( + mainViewModel.newsInstance.fetchGlobalNews( fetchType = NewsFetchType.NextPage, forceRequest = true ) @@ -231,9 +248,9 @@ fun NewsActivity.MainView( 1 -> { @Suppress("UNCHECKED_CAST") (NewsListPage( - newsList = getMainViewModel().newsInstance.newsSubject.data.toList() as List, - processState = getMainViewModel().newsInstance.newsSubject.processState.value, - opacity = getControlBackgroundAlpha(), + newsList = mainViewModel.newsInstance.newsSubject.data.toList() as List, + processState = mainViewModel.newsInstance.newsSubject.processState.value, + opacity = componentBackgroundAlpha, itemClicked = { newsItem -> context.startActivity( Intent( @@ -248,7 +265,7 @@ fun NewsActivity.MainView( endOfListReached = { CoroutineScope(Dispatchers.Main).launch { withContext(Dispatchers.IO) { - getMainViewModel().newsInstance.fetchSubjectNews( + mainViewModel.newsInstance.fetchSubjectNews( fetchType = NewsFetchType.NextPage, forceRequest = true ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt index eb9085f..46cc0cc 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt @@ -1,6 +1,9 @@ package io.zoemeow.dutschedule.ui.view.settings +import android.content.ComponentName import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -26,13 +29,16 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat import io.zoemeow.dutschedule.activity.SettingsActivity import io.zoemeow.dutschedule.model.settings.BackgroundImageOption import io.zoemeow.dutschedule.ui.component.base.DividerItem import io.zoemeow.dutschedule.ui.component.base.OptionItem +import io.zoemeow.dutschedule.ui.component.base.OptionSwitchItem import io.zoemeow.dutschedule.ui.component.settings.ContentRegion import io.zoemeow.dutschedule.ui.component.settings.dialog.DialogSchoolYearSettings + @OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsActivity.ExperimentSettings( @@ -136,6 +142,43 @@ fun SettingsActivity.ExperimentSettings( /* TODO: Implement here: Component opacity */ } ) + // https://stackoverflow.com/questions/72932093/jetpack-compose-is-there-a-way-to-restart-whole-app-programmatically + OptionSwitchItem( + modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), + title = "Main screen dashboard view", + isVisible = true, + isEnabled = true, + isChecked = getMainViewModel().appSettings.value.mainScreenDashboardView, + description = String.format( + "%s", + if (getMainViewModel().appSettings.value.mainScreenDashboardView) "Enabled" else "Disabled (tab view)" + ), + onValueChanged = { + showSnackBar( + text = String.format( + "This will %s your dashboard view. Application will restart. To confirm, click Confirm button.", + if (getMainViewModel().appSettings.value.mainScreenDashboardView) "disable" else "enable", + ), + clearPrevious = true, + actionText = "Confirm", + action = { + getMainViewModel().appSettings.value = getMainViewModel().appSettings.value.clone( + mainScreenDashboardView = !getMainViewModel().appSettings.value.mainScreenDashboardView + ) + getMainViewModel().saveSettings( + onCompleted = { + val packageManager: PackageManager = context.packageManager + val intent: Intent = packageManager.getLaunchIntentForPackage(context.packageName)!! + val componentName: ComponentName = intent.component!! + val restartIntent: Intent = Intent.makeRestartActivityTask(componentName) + context.startActivity(restartIntent) + Runtime.getRuntime().exit(0) + } + ) + } + ) + } + ) } ) DividerItem(padding = PaddingValues(top = 5.dp, bottom = 15.dp)) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt index 86d7219..add82e9 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt @@ -47,9 +47,10 @@ import io.zoemeow.dutschedule.ui.component.base.OptionSwitchItem import io.zoemeow.dutschedule.ui.component.settings.ContentRegion import io.zoemeow.dutschedule.ui.component.settings.dialog.DialogAppBackgroundSettings import io.zoemeow.dutschedule.ui.component.settings.dialog.DialogAppThemeSettings +import io.zoemeow.dutschedule.utils.openLink +import io.zoemeow.dutschedule.viewmodel.MainViewModel import java.util.Locale -@OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsActivity.MainView( context: Context, @@ -57,33 +58,65 @@ fun SettingsActivity.MainView( containerColor: Color, contentColor: Color, mediaRequest: () -> Unit +) { + SettingsMainView( + context = context, + snackBarHostState = snackBarHostState, + containerColor = containerColor, + contentColor = contentColor, + componentBackgroundAlpha = getControlBackgroundAlpha(), + mainViewModel = getMainViewModel(), + mediaRequest = mediaRequest, + onShowSnackBar = { text, clearPrevious, actionText, action -> + showSnackBar(text = text, clearPrevious = clearPrevious, actionText = actionText, action = action) + }, + onBack = { + setResult(ComponentActivity.RESULT_OK) + finish() + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsMainView( + context: Context, + snackBarHostState: SnackbarHostState? = null, + containerColor: Color, + contentColor: Color, + componentBackgroundAlpha: Float = 1f, + mainViewModel: MainViewModel, + mediaRequest: () -> Unit, + onShowSnackBar: ((String, Boolean, String?, (() -> Unit)?) -> Unit)? = null, + onBack: (() -> Unit)? = null ) { val dialogAppTheme: MutableState = remember { mutableStateOf(false) } val dialogBackground: MutableState = remember { mutableStateOf(false) } Scaffold( modifier = Modifier.fillMaxSize(), - snackbarHost = { SnackbarHost(hostState = snackBarHostState) }, + snackbarHost = { snackBarHostState?.let { SnackbarHost(hostState = it) } }, containerColor = containerColor, contentColor = contentColor, topBar = { TopAppBar( - title = { Text(getString(R.string.settings_name)) }, + title = { Text(context.getString(R.string.settings_name)) }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), navigationIcon = { - IconButton( - onClick = { - setResult(ComponentActivity.RESULT_OK) - finish() - }, - content = { - Icon( - Icons.AutoMirrored.Filled.ArrowBack, - "", - modifier = Modifier.size(25.dp) - ) - } - ) + if (onBack != null) { + IconButton( + onClick = { + onBack() + }, + content = { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + "", + modifier = Modifier.size(25.dp) + ) + } + ) + } } ) }, @@ -97,19 +130,19 @@ fun SettingsActivity.MainView( modifier = Modifier .padding(top = 10.dp), textModifier = Modifier.padding(horizontal = 20.dp), - text = getString(R.string.settings_category_notifications), + text = context.getString(R.string.settings_category_notifications), content = { OptionItem( modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), leadingIcon = { Icon( imageVector = ImageVector.vectorResource(R.drawable.ic_outline_calendar_clock_24), - getString(R.string.settings_option_newsschedule), + context.getString(R.string.settings_option_newsschedule), modifier = Modifier.padding(end = 15.dp) ) }, - title = getString(R.string.settings_option_newsschedule), - description = getString(R.string.settings_option_newsschedule_description), + title = context.getString(R.string.settings_option_newsschedule), + description = context.getString(R.string.settings_option_newsschedule_description), onClick = { Intent(context, SettingsActivity::class.java).apply { action = "settings_newsnotificaitonsettings" @@ -122,15 +155,15 @@ fun SettingsActivity.MainView( leadingIcon = { Icon( Icons.Default.Notifications, - getString(R.string.settings_option_notificationoutside), + context.getString(R.string.settings_option_notificationoutside), modifier = Modifier.padding(end = 15.dp) ) }, - title = getString(R.string.settings_option_notificationoutside), - description = getString(R.string.settings_option_notificationoutside_description), + title = context.getString(R.string.settings_option_notificationoutside), + description = context.getString(R.string.settings_option_notificationoutside_description), onClick = { context.startActivity(Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).also { intent -> - intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName) + intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) }) } ) @@ -142,26 +175,26 @@ fun SettingsActivity.MainView( modifier = Modifier .padding(top = 10.dp), textModifier = Modifier.padding(horizontal = 20.dp), - text = getString(R.string.settings_category_appearance), + text = context.getString(R.string.settings_category_appearance), content = { OptionItem( modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), leadingIcon = { Icon( imageVector = ImageVector.vectorResource(R.drawable.ic_baseline_dark_mode_24), - getString(R.string.settings_option_apptheme), + context.getString(R.string.settings_option_apptheme), modifier = Modifier.padding(end = 15.dp) ) }, - title = getString(R.string.settings_option_apptheme), + title = context.getString(R.string.settings_option_apptheme), description = String.format( "%s%s", - when (getMainViewModel().appSettings.value.themeMode) { + when (mainViewModel.appSettings.value.themeMode) { ThemeMode.FollowDeviceTheme -> "Follow device theme" ThemeMode.DarkMode -> "Dark mode" ThemeMode.LightMode -> "Light mode" }, - if (getMainViewModel().appSettings.value.dynamicColor) " (dynamic color enabled)" else "" + if (mainViewModel.appSettings.value.dynamicColor) " (dynamic color enabled)" else "" ), onClick = { dialogAppTheme.value = true } ) @@ -176,13 +209,13 @@ fun SettingsActivity.MainView( }, title = "Black background", description = "Make app background to black color. Only in dark mode and turned off background image.", - isChecked = getMainViewModel().appSettings.value.blackBackground, + isChecked = mainViewModel.appSettings.value.blackBackground, onValueChanged = { value -> - getMainViewModel().appSettings.value = - getMainViewModel().appSettings.value.clone( + mainViewModel.appSettings.value = + mainViewModel.appSettings.value.clone( blackBackground = value ) - getMainViewModel().saveSettings() + mainViewModel.saveSettings() } ) OptionItem( @@ -195,7 +228,7 @@ fun SettingsActivity.MainView( ) }, title = "Background image", - description = when (getMainViewModel().appSettings.value.backgroundImage) { + description = when (mainViewModel.appSettings.value.backgroundImage) { BackgroundImageOption.None -> "None" BackgroundImageOption.YourCurrentWallpaper -> "Your current wallpaper" BackgroundImageOption.PickFileFromMedia -> "Your picked image" @@ -235,6 +268,13 @@ fun SettingsActivity.MainView( ) OptionItem( modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.blank_24), + "", + modifier = Modifier.padding(end = 15.dp) + ) + }, title = "Application permissions", description = "Click here for allow and manage app permissions you granted.", onClick = { @@ -257,13 +297,13 @@ fun SettingsActivity.MainView( }, title = "Open link inside app", description = "Open clicked link without leaving this app. Turn off to open link in default browser.", - isChecked = getMainViewModel().appSettings.value.openLinkInsideApp, + isChecked = mainViewModel.appSettings.value.openLinkInsideApp, onValueChanged = { value -> - getMainViewModel().appSettings.value = - getMainViewModel().appSettings.value.clone( + mainViewModel.appSettings.value = + mainViewModel.appSettings.value.clone( openLinkInsideApp = value ) - getMainViewModel().saveSettings() + mainViewModel.saveSettings() } ) OptionItem( @@ -294,34 +334,53 @@ fun SettingsActivity.MainView( content = { OptionItem( modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_baseline_info_24), + "", + modifier = Modifier.padding(end = 15.dp) + ) + }, title = "Version", description = "Current version: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})\nClick here to check for update", onClick = { - showSnackBar("This option is in development. Check back soon.", true) + onShowSnackBar?.let { it("This option is in development. Check back soon.", true, null, null) } /* TODO: Implement here: Check for updates */ } ) OptionItem( modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.google_fonts_device_reset_24), + "", + modifier = Modifier.padding(end = 15.dp) + ) + }, title = "Changelogs", description = "Tap to view app changelog", onClick = { - openLink( + context.openLink( url = "https://github.com/ZoeMeow1027/DutSchedule/blob/stable/CHANGELOG.md", - context = context, - customTab = getMainViewModel().appSettings.value.openLinkInsideApp, + customTab = mainViewModel.appSettings.value.openLinkInsideApp, ) } ) OptionItem( modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), + leadingIcon = { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.github_mark_24), + "", + modifier = Modifier.padding(end = 15.dp) + ) + }, title = "GitHub (click to open link)", description = "https://github.com/ZoeMeow1027/DutSchedule", onClick = { - openLink( + context.openLink( url = "https://github.com/ZoeMeow1027/DutSchedule", - context = context, - customTab = getMainViewModel().appSettings.value.openLinkInsideApp, + customTab = mainViewModel.appSettings.value.openLinkInsideApp, ) } ) @@ -333,60 +392,63 @@ fun SettingsActivity.MainView( ) DialogAppThemeSettings( isVisible = dialogAppTheme.value, - themeModeValue = getMainViewModel().appSettings.value.themeMode, - dynamicColorEnabled = getMainViewModel().appSettings.value.dynamicColor, + themeModeValue = mainViewModel.appSettings.value.themeMode, + dynamicColorEnabled = mainViewModel.appSettings.value.dynamicColor, onDismiss = { dialogAppTheme.value = false }, onValueChanged = { themeMode, dynamicColor -> - getMainViewModel().appSettings.value = getMainViewModel().appSettings.value.clone( + mainViewModel.appSettings.value = mainViewModel.appSettings.value.clone( themeMode = themeMode, dynamicColor = dynamicColor ) - getMainViewModel().saveSettings() + mainViewModel.saveSettings() } ) DialogAppBackgroundSettings( context = context, - value = getMainViewModel().appSettings.value.backgroundImage, + value = mainViewModel.appSettings.value.backgroundImage, isVisible = dialogBackground.value, - onDismiss = { dialogBackground.value = false }, - onValueChanged = { value -> - when (value) { - BackgroundImageOption.None -> { - getMainViewModel().appSettings.value = - getMainViewModel().appSettings.value.clone( + onDismiss = { dialogBackground.value = false } + ) { value -> + when (value) { + BackgroundImageOption.None -> { + mainViewModel.appSettings.value = + mainViewModel.appSettings.value.clone( + backgroundImage = value + ) + } + + BackgroundImageOption.YourCurrentWallpaper -> { + val compPer = + PermissionRequestActivity.checkPermissionManageExternalStorage().isGranted + if (compPer) { + mainViewModel.appSettings.value = + mainViewModel.appSettings.value.clone( backgroundImage = value ) - } - BackgroundImageOption.YourCurrentWallpaper -> { - val compPer = PermissionRequestActivity.checkPermissionManageExternalStorage().isGranted - if (compPer) { - getMainViewModel().appSettings.value = - getMainViewModel().appSettings.value.clone( - backgroundImage = value - ) - } else { - showSnackBar( - text = "You need to grant All files access in application permission to use this feature. You can use \"Choose a image from media\" without this permission.", - clearPrevious = true, - actionText = "Grant", - action = { - Intent(context, PermissionRequestActivity::class.java).also { - context.startActivity(it) - } + } else { + onShowSnackBar?.let { + it( + "You need to grant All files access in application permission to use this feature. You can use \"Choose a image from media\" without this permission.", + true, + "Grant" + ) { + Intent(context, PermissionRequestActivity::class.java).also { + context.startActivity(it) } - ) + } } } - BackgroundImageOption.PickFileFromMedia -> { - // Launch the photo picker and let the user choose only images. - mediaRequest.let { it() } - } } - dialogBackground.value = false - getMainViewModel().saveSettings() + BackgroundImageOption.PickFileFromMedia -> { + // Launch the photo picker and let the user choose only images. + mediaRequest.let { it() } + } } - ) + + dialogBackground.value = false + mainViewModel.saveSettings() + } BackHandler( enabled = dialogAppTheme.value || dialogBackground.value, onBack = { diff --git a/app/src/main/java/io/zoemeow/dutschedule/utils/FunctionExtension.kt b/app/src/main/java/io/zoemeow/dutschedule/utils/FunctionExtension.kt new file mode 100644 index 0000000..6b87bf1 --- /dev/null +++ b/app/src/main/java/io/zoemeow/dutschedule/utils/FunctionExtension.kt @@ -0,0 +1,30 @@ +package io.zoemeow.dutschedule.utils + +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.browser.customtabs.CustomTabColorSchemeParams +import androidx.browser.customtabs.CustomTabsIntent + +fun Context.openLink( + url: String, + customTab: Boolean = true +) { + when (customTab) { + false -> { + this.startActivity( + Intent(Intent.ACTION_VIEW, Uri.parse(url)) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ) + } + + true -> { + val builder = CustomTabsIntent.Builder() + val defaultColors = CustomTabColorSchemeParams.Builder().build() + builder.setDefaultColorSchemeParams(defaultColors) + + val customTabsIntent = builder.build() + customTabsIntent.launchUrl(this, Uri.parse(url)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt b/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt index 6d2c4c2..4b8177c 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt @@ -10,11 +10,11 @@ import com.google.gson.reflect.TypeToken import dagger.hilt.android.lifecycle.HiltViewModel import io.dutwrapper.dutwrapper.Utils import io.dutwrapper.dutwrapper.model.utils.DutSchoolYearItem -import io.zoemeow.dutschedule.model.DUTAccountInstance -import io.zoemeow.dutschedule.model.DUTNewsInstance import io.zoemeow.dutschedule.model.NotificationHistory import io.zoemeow.dutschedule.model.ProcessVariable import io.zoemeow.dutschedule.model.account.AccountSession +import io.zoemeow.dutschedule.model.account.DUTAccountInstance +import io.zoemeow.dutschedule.model.news.DUTNewsInstance import io.zoemeow.dutschedule.model.news.NewsFetchType import io.zoemeow.dutschedule.model.settings.AppSettings import io.zoemeow.dutschedule.repository.DutRequestRepository @@ -60,6 +60,7 @@ class MainViewModel @Inject constructor( } ) + // TODO: Change this to VariableState /** * Get current school week if possible. */ @@ -88,7 +89,10 @@ class MainViewModel @Inject constructor( /** * Save all current settings to file in storage. */ - fun saveSettings(saveSettingsOnly: Boolean = false) { + fun saveSettings( + saveSettingsOnly: Boolean = false, + onCompleted: (() -> Unit)? = null + ) { launchOnScope( script = { fileModuleRepository.saveAppSettings(appSettings.value) @@ -98,7 +102,8 @@ class MainViewModel @Inject constructor( fileModuleRepository.saveAccountSubjectScheduleCache(ArrayList(accountSession.getSubjectScheduleCache())) fileModuleRepository.saveNotificationHistory(ArrayList(notificationHistory.toList())) } - } + }, + invokeOnCompleted = { onCompleted?.let { it() } } ) } @@ -140,11 +145,6 @@ class MainViewModel @Inject constructor( } catch (_: Exception) { } } } - - // TODO: Get account subject schedule from cache -// fileModuleRepository.getAccountSubjectScheduleCache().also { -// subjectSchedule.data.value = it -// } } ) } diff --git a/app/src/main/res/drawable/blank_24.xml b/app/src/main/res/drawable/blank_24.xml new file mode 100644 index 0000000..4fa1ef9 --- /dev/null +++ b/app/src/main/res/drawable/blank_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/github_mark_24.xml b/app/src/main/res/drawable/github_mark_24.xml new file mode 100644 index 0000000..01af5bc --- /dev/null +++ b/app/src/main/res/drawable/github_mark_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/google_fonts_device_reset_24.xml b/app/src/main/res/drawable/google_fonts_device_reset_24.xml new file mode 100644 index 0000000..95294f1 --- /dev/null +++ b/app/src/main/res/drawable/google_fonts_device_reset_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_info_24.xml b/app/src/main/res/drawable/ic_baseline_info_24.xml index 8ba6037..e0ecb40 100644 --- a/app/src/main/res/drawable/ic_baseline_info_24.xml +++ b/app/src/main/res/drawable/ic_baseline_info_24.xml @@ -1,5 +1,5 @@ - + android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> diff --git a/build.gradle b/build.gradle index 8b559fb..35fb10a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '8.3.1' apply false + id 'com.android.application' version '8.3.2' apply false id 'org.jetbrains.kotlin.android' version '1.8.10' apply false id 'com.google.dagger.hilt.android' version '2.44' apply false } From f99611d2a2a529b481b59c3840bc9fdfb0014c43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BA=BF=20T=C3=B9ng?= <47247560+ZoeMeow1027@users.noreply.github.com> Date: Thu, 11 Apr 2024 11:55:10 +0700 Subject: [PATCH 10/21] Update project - Make a fix for notification tab that's not full screen in main view tabbed. - Fixed when clear a notification and undo it, it won't open correctly. - Renamed from NotificationDialogBox to NotificationScaffold. --- .../dutschedule/model/NotificationHistory.kt | 7 +- .../ui/component/main/NotificationItem.kt | 139 ++++++++++++++++++ .../main/notification/NotificationItem.kt | 125 ---------------- .../ui/view/main/MainViewDashboard.kt | 13 +- .../ui/view/main/MainViewTabbed.kt | 16 +- .../main/NotificationScaffold.kt} | 18 +-- 6 files changed, 158 insertions(+), 160 deletions(-) create mode 100644 app/src/main/java/io/zoemeow/dutschedule/ui/component/main/NotificationItem.kt delete mode 100644 app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationItem.kt rename app/src/main/java/io/zoemeow/dutschedule/ui/{component/main/notification/NotificationDialogBox.kt => view/main/NotificationScaffold.kt} (93%) diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/NotificationHistory.kt b/app/src/main/java/io/zoemeow/dutschedule/model/NotificationHistory.kt index 4789dff..cf36044 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/NotificationHistory.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/NotificationHistory.kt @@ -6,7 +6,7 @@ import io.zoemeow.dutschedule.utils.getRandomString import java.io.Serializable data class NotificationHistory( - private val id: String = getRandomString(32), + val id: String = getRandomString(32), val title: String, val description: String, val tag: Int = 0, @@ -48,16 +48,13 @@ data class NotificationHistory( isReceived: Boolean? = null, changeIdAfterCopy: Boolean = false ): NotificationHistory { - val map: Map = mapOf() - map.plus(intentArguments ?: this.parameters) - return NotificationHistory( id = if (changeIdAfterCopy) getRandomString(32) else id, title = title ?: this.title, description = description ?: this.description, timestamp = timestamp ?: this.timestamp, tag = tag ?: this.tag, - parameters = map, + parameters = intentArguments ?: this.parameters, isRead = isRead ?: this.isRead, isReceived = isReceived ?: this.isReceived ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/NotificationItem.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/NotificationItem.kt new file mode 100644 index 0000000..9f9c517 --- /dev/null +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/NotificationItem.kt @@ -0,0 +1,139 @@ +package io.zoemeow.dutschedule.ui.component.main + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.clickable +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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.zoemeow.dutschedule.model.NotificationHistory +import io.zoemeow.dutschedule.utils.CustomDateUtil +import io.zoemeow.dutschedule.utils.getRandomString + +@Composable +fun NotificationItem( + modifier: Modifier = Modifier, + item: NotificationHistory, + showDate: Boolean = false, + isVisible: Boolean = true, + onClick: (() -> Unit)? = null, + onClear: (() -> Unit)? = null, + opacity: Float = 1f +) { + AnimatedVisibility( + visible = isVisible, + enter = fadeIn(animationSpec = tween(400)), + exit = fadeOut(animationSpec = tween(400)), + ) { + Surface( + modifier = modifier.clickable { onClick?.let { it() } }, + shape = RoundedCornerShape(5.dp), + color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = opacity), + content = { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 10.dp, vertical = 3.dp), + verticalAlignment = Alignment.CenterVertically, + content = { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 5.dp) + .weight(0.9f), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + if (showDate) { + Text( + CustomDateUtil.unixToDuration(item.timestamp), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(bottom = 5.dp) + ) + } + Text( + item.title, + style = MaterialTheme.typography.titleMedium, + overflow = TextOverflow.Ellipsis, + maxLines = 2 + ) + Spacer(modifier = Modifier.size(3.dp)) + Text( + item.description, + style = MaterialTheme.typography.bodyMedium, + overflow = TextOverflow.Ellipsis, + maxLines = 3 + ) + } + Column( + modifier = Modifier.weight(0.10f), + content = { + IconButton( + modifier = Modifier.size(36.dp), + onClick = { onClear?.let { it() } } + ) { + Icon(Icons.Default.Clear, "") + } + } + ) + } + ) + } + ) + } + +} + +@Preview +@Composable +private fun Preview1() { + val notificationHistory = NotificationHistory( + id = getRandomString(32), + title = "News global", + description = "V/v Xét giao Đồ án tốt nghiệp học kỳ 2/23-24", + tag = 1, + timestamp = 1708534800000, + parameters = mapOf(), + isRead = false + ) + NotificationItem( + item = notificationHistory + ) +} + +@Preview +@Composable +private fun Preview2() { + val notificationHistory = NotificationHistory( + id = getRandomString(32), + title = "Thầy Lê Kim Hùng thông báo đến lớp: Phương pháp luận nghiên cứu khoa học [20.Nh29]", + description = "Chiều mai (thứ sáu, 23/2) thầy Hùng bận việc từ 16.00 nên ngày mai ta nghỉ tiết 9-10 (HP PPNCKH). Ta còn nhiều tuần để bù (báo các em biết).", + tag = 1, + timestamp = 1708534800000, + parameters = mapOf(), + isRead = false + ) + NotificationItem( + item = notificationHistory + ) +} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationItem.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationItem.kt deleted file mode 100644 index 014c169..0000000 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationItem.kt +++ /dev/null @@ -1,125 +0,0 @@ -package io.zoemeow.dutschedule.ui.component.main.notification - -import androidx.compose.foundation.clickable -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.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Clear -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import io.zoemeow.dutschedule.model.NotificationHistory -import io.zoemeow.dutschedule.utils.CustomDateUtil -import io.zoemeow.dutschedule.utils.getRandomString - -@Composable -fun NotificationItem( - modifier: Modifier = Modifier, - item: NotificationHistory, - showDate: Boolean = false, - onClick: (() -> Unit)? = null, - onClear: (() -> Unit)? = null, - opacity: Float = 1f -) { - Surface( - modifier = modifier.clickable { onClick?.let { it() } }, - shape = RoundedCornerShape(5.dp), - color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = opacity), - content = { - Row( - modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 3.dp), - verticalAlignment = Alignment.CenterVertically, - content = { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 5.dp) - .weight(0.9f), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.Start - ) { - if (showDate) { - Text( - CustomDateUtil.unixToDuration(item.timestamp), - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(bottom = 5.dp) - ) - } - Text( - item.title, - style = MaterialTheme.typography.titleMedium, - overflow = TextOverflow.Ellipsis, - maxLines = 2 - ) - Spacer(modifier = Modifier.size(3.dp)) - Text( - item.description, - style = MaterialTheme.typography.bodyMedium, - overflow = TextOverflow.Ellipsis, - maxLines = 3 - ) - } - Column( - modifier = Modifier.weight(0.10f), - content = { - IconButton( - modifier = Modifier.size(36.dp), - onClick = { onClear?.let { it() } } - ) { - Icon(Icons.Default.Clear, "") - } - } - ) - } - ) - } - ) -} - -@Preview -@Composable -private fun Preview1() { - val notificationHistory = NotificationHistory( - id = getRandomString(32), - title = "News global", - description = "V/v Xét giao Đồ án tốt nghiệp học kỳ 2/23-24", - tag = 1, - timestamp = 1708534800000, - parameters = mapOf(), - isRead = false - ) - NotificationItem( - item = notificationHistory - ) -} - -@Preview -@Composable -private fun Preview2() { - val notificationHistory = NotificationHistory( - id = getRandomString(32), - title = "Thầy Lê Kim Hùng thông báo đến lớp: Phương pháp luận nghiên cứu khoa học [20.Nh29]", - description = "Chiều mai (thứ sáu, 23/2) thầy Hùng bận việc từ 16.00 nên ngày mai ta nghỉ tiết 9-10 (HP PPNCKH). Ta còn nhiều tuần để bù (báo các em biết).", - tag = 1, - timestamp = 1708534800000, - parameters = mapOf(), - isRead = false - ) - NotificationItem( - item = notificationHistory - ) -} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt index d8310a3..4700c1d 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt @@ -44,7 +44,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.AccountActivity -import io.zoemeow.dutschedule.activity.BaseActivity import io.zoemeow.dutschedule.activity.MainActivity import io.zoemeow.dutschedule.activity.NewsActivity import io.zoemeow.dutschedule.model.CustomClock @@ -54,7 +53,6 @@ import io.zoemeow.dutschedule.ui.component.main.DateAndTimeSummaryItem import io.zoemeow.dutschedule.ui.component.main.LessonTodaySummaryItem import io.zoemeow.dutschedule.ui.component.main.SchoolNewsSummaryItem import io.zoemeow.dutschedule.ui.component.main.UpdateAvailableSummaryItem -import io.zoemeow.dutschedule.ui.component.main.notification.NotificationDialogBox import io.zoemeow.dutschedule.utils.BackgroundImageUtil import io.zoemeow.dutschedule.utils.CustomDateUtil import kotlinx.datetime.Clock @@ -318,7 +316,7 @@ fun MainActivity.MainViewDashboard( ) } ) - NotificationDialogBox( + NotificationScaffold( itemList = getMainViewModel().notificationHistory, snackBarHostState = snackBarHostState, isVisible = isNotificationOpened.value, @@ -344,15 +342,15 @@ fun MainActivity.MainViewDashboard( } } }, - onClear = { - val itemTemp = it.clone() - getMainViewModel().notificationHistory.remove(it) + onClear = { item -> + val item1 = item.clone() + getMainViewModel().notificationHistory.remove(item) getMainViewModel().saveSettings() showSnackBar( text = "Deleted notifications!", actionText = "Undo", action = { - getMainViewModel().notificationHistory.add(itemTemp) + getMainViewModel().notificationHistory.add(item1) getMainViewModel().saveSettings() } ) @@ -372,7 +370,6 @@ fun MainActivity.MainViewDashboard( clearPrevious = true ) }, - height = 1f, opacity = getControlBackgroundAlpha() ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewTabbed.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewTabbed.kt index 715e479..c17c027 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewTabbed.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewTabbed.kt @@ -4,18 +4,15 @@ import android.content.Context import android.content.Intent import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Info -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem @@ -23,12 +20,8 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource @@ -40,7 +33,6 @@ import androidx.navigation.compose.rememberNavController import io.zoemeow.dutschedule.activity.MainActivity import io.zoemeow.dutschedule.activity.NewsActivity import io.zoemeow.dutschedule.model.NavBarItem -import io.zoemeow.dutschedule.ui.component.main.notification.NotificationDialogBox import io.zoemeow.dutschedule.ui.view.account.AccountMainView import io.zoemeow.dutschedule.ui.view.news.NewsMainView import io.zoemeow.dutschedule.ui.view.settings.SettingsMainView @@ -139,7 +131,7 @@ fun MainActivity.MainViewTabbed( } composable(NavBarItem.notification.route) { - NotificationDialogBox( + NotificationScaffold( itemList = getMainViewModel().notificationHistory.toList(), snackBarHostState = snackBarHostState, isVisible = true, @@ -156,9 +148,9 @@ fun MainActivity.MainViewTabbed( } } }, - onClear = { - val itemTemp = it.clone() - getMainViewModel().notificationHistory.remove(it) + onClear = { item -> + val itemTemp = item.clone() + getMainViewModel().notificationHistory.remove(item) getMainViewModel().saveSettings() showSnackBar( text = "Deleted notifications!", diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationDialogBox.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/NotificationScaffold.kt similarity index 93% rename from app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationDialogBox.kt rename to app/src/main/java/io/zoemeow/dutschedule/ui/view/main/NotificationScaffold.kt index 5acb354..d793743 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/notification/NotificationDialogBox.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/NotificationScaffold.kt @@ -1,18 +1,14 @@ -package io.zoemeow.dutschedule.ui.component.main.notification +package io.zoemeow.dutschedule.ui.view.main import android.graphics.Bitmap import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState @@ -26,11 +22,12 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -41,11 +38,13 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.model.NotificationHistory +import io.zoemeow.dutschedule.ui.component.main.NotificationItem import io.zoemeow.dutschedule.utils.CustomDateUtil +import okhttp3.internal.concurrent.TaskRunner @OptIn(ExperimentalMaterial3Api::class) @Composable -fun NotificationDialogBox( +fun NotificationScaffold( isVisible: Boolean = false, itemList: List, containerColor: Color, @@ -56,7 +55,6 @@ fun NotificationDialogBox( onClick: ((NotificationHistory) -> Unit)? = null, onClear: ((NotificationHistory) -> Unit)? = null, onClearAll: (() -> Unit)? = null, - height: Float = 0.7f, opacity: Float = 1f, ) { AnimatedVisibility( @@ -112,8 +110,7 @@ fun NotificationDialogBox( content = { Column( modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(height) + .fillMaxSize() .padding(it) .padding(horizontal = 15.dp) ) { @@ -141,6 +138,7 @@ fun NotificationDialogBox( group.value.forEach { item -> NotificationItem( modifier = Modifier.padding(top = 2.dp, bottom = 5.dp), + isVisible = true, opacity = opacity, onClick = { onClick?.let { it(item) } }, onClear = { onClear?.let { it(item) } }, From 68f3acd449d2bc9d3caa1480b9268db2d825d377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BA=BF=20T=C3=B9ng?= <47247560+ZoeMeow1027@users.noreply.github.com> Date: Thu, 11 Apr 2024 20:22:52 +0700 Subject: [PATCH 11/21] Update AppSettings.kt --- .../java/io/zoemeow/dutschedule/model/settings/AppSettings.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/settings/AppSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/model/settings/AppSettings.kt index c4cac11..34e134c 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/settings/AppSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/settings/AppSettings.kt @@ -6,7 +6,7 @@ import java.io.Serializable data class AppSettings( @SerializedName("appsettings.layout.mainview.dashboardview") - val mainScreenDashboardView: Boolean = true, + val mainScreenDashboardView: Boolean = false, @SerializedName("appsettings.appearance.thememode") val themeMode: ThemeMode = ThemeMode.FollowDeviceTheme, From 8a27fce022201ef320aca49ee0c9867fff7b8996 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BA=BF=20T=C3=B9ng?= Date: Wed, 17 Apr 2024 18:31:54 +0700 Subject: [PATCH 12/21] Update project - Merge function extensions into one. - Update Vietnamese language strings. --- .../dutschedule/activity/NewsActivity.kt | 1 + .../service/NewsBackgroundUpdateService.kt | 24 ++-- .../ui/component/news/NewsListPage.kt | 2 +- .../news/NewsSearchOptionAndHistory.kt | 41 ++++--- .../ui/component/news/NewsSearchResult.kt | 17 ++- .../dialog/DialogAppBackgroundSettings.kt | 5 +- .../ui/view/main/MainViewDashboard.kt | 3 +- .../ui/view/main/MainViewTabbed.kt | 1 + .../ui/view/main/NotificationScaffold.kt | 9 +- .../dutschedule/ui/view/news/MainView.kt | 61 ++++------ .../dutschedule/ui/view/news/NewsDetail.kt | 21 +++- .../dutschedule/ui/view/news/NewsSearch.kt | 12 +- .../dutschedule/ui/view/settings/MainView.kt | 44 +++---- .../dutschedule/utils/BackgroundImageUtil.kt | 17 +++ .../dutschedule/utils/FunctionExtension.kt | 109 ++++++++++++++++++ .../dutschedule/utils/LazyColumnExtension.kt | 40 ------- .../dutschedule/utils/RowScopeExtension.kt | 44 ------- .../dutschedule/utils/StringExtension.kt | 33 ------ app/src/main/res/values-vi/strings.xml | 56 ++++++++- app/src/main/res/values/strings.xml | 56 ++++++++- 20 files changed, 357 insertions(+), 239 deletions(-) delete mode 100644 app/src/main/java/io/zoemeow/dutschedule/utils/LazyColumnExtension.kt delete mode 100644 app/src/main/java/io/zoemeow/dutschedule/utils/RowScopeExtension.kt delete mode 100644 app/src/main/java/io/zoemeow/dutschedule/utils/StringExtension.kt diff --git a/app/src/main/java/io/zoemeow/dutschedule/activity/NewsActivity.kt b/app/src/main/java/io/zoemeow/dutschedule/activity/NewsActivity.kt index 806606f..cff3921 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/activity/NewsActivity.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/activity/NewsActivity.kt @@ -36,6 +36,7 @@ class NewsActivity : BaseActivity() { "activity_detail" -> { NewsDetail( + context = context, snackBarHostState = snackBarHostState, containerColor = containerColor, contentColor = contentColor diff --git a/app/src/main/java/io/zoemeow/dutschedule/service/NewsBackgroundUpdateService.kt b/app/src/main/java/io/zoemeow/dutschedule/service/NewsBackgroundUpdateService.kt index e0ba264..05e68fe 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/service/NewsBackgroundUpdateService.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/service/NewsBackgroundUpdateService.kt @@ -13,9 +13,9 @@ import io.dutwrapper.dutwrapper.model.news.NewsGlobalItem import io.dutwrapper.dutwrapper.model.news.NewsSubjectItem import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.PermissionRequestActivity -import io.zoemeow.dutschedule.model.news.DUTNewsInstance import io.zoemeow.dutschedule.model.NotificationHistory import io.zoemeow.dutschedule.model.ProcessState +import io.zoemeow.dutschedule.model.news.DUTNewsInstance import io.zoemeow.dutschedule.model.news.NewsFetchType import io.zoemeow.dutschedule.model.settings.AppSettings import io.zoemeow.dutschedule.model.settings.SubjectCode @@ -449,24 +449,24 @@ class NewsBackgroundUpdateService : BaseService( // Title will make announcement about lecturer and subjects val notifyTitle = when (newsItem.lessonStatus) { LessonStatus.Leaving -> { - String.format( - context.getString(R.string.service_newsbackgroundservice_newssubject_title_noannouncement), + context.getString( + R.string.service_newsbackgroundservice_newssubject_title_noannouncement, context.getString(R.string.service_newsbackgroundservice_newssubject_title_noannouncement_leaving), newsItem.lecturerName, affectedClassrooms ) } LessonStatus.MakeUp -> { - String.format( - context.getString(R.string.service_newsbackgroundservice_newssubject_title_noannouncement), + context.getString( + R.string.service_newsbackgroundservice_newssubject_title_noannouncement, context.getString(R.string.service_newsbackgroundservice_newssubject_title_noannouncement_makeup), newsItem.lecturerName, affectedClassrooms ) } else -> { - String.format( - context.getString(R.string.service_newsbackgroundservice_newssubject_title_announcement), + context.getString( + R.string.service_newsbackgroundservice_newssubject_title_announcement, newsItem.lecturerName, affectedClassrooms ) @@ -481,18 +481,18 @@ class NewsBackgroundUpdateService : BaseService( ) { // Date & lessons notifyContentList.add( - String.format( - context.getString(R.string.service_newsbackgroundservice_newssubject_date), + context.getString( + R.string.service_newsbackgroundservice_newssubject_date, CustomDateUtil.dateUnixToString(newsItem.affectedDate, "dd/MM/yyyy"), - if (newsItem.affectedLesson != null) newsItem.affectedLesson.toString() else "(unknown)" + if (newsItem.affectedLesson != null) newsItem.affectedLesson.toString() else context.getString(R.string.service_newsbackgroundservice_newssubject_lessonunknown) ) ) // Make-up room if (newsItem.lessonStatus == LessonStatus.MakeUp) { // Make up in room notifyContentList.add( - String.format( - context.getString(R.string.service_newsbackgroundservice_newssubject_room), + context.getString( + R.string.service_newsbackgroundservice_newssubject_room, newsItem.affectedRoom ) ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/news/NewsListPage.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/news/NewsListPage.kt index 9bcde2d..faec90a 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/news/NewsListPage.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/news/NewsListPage.kt @@ -45,7 +45,7 @@ fun NewsListPage( .padding(start = 20.dp, end = 20.dp) .endOfListReached( lazyListState = lazyListState, - endOfListReached = { endOfListReached?.let { it() } } + onReached = { endOfListReached?.let { it() } } ), horizontalAlignment = if (newsList.isNotEmpty()) Alignment.Start else Alignment.CenterHorizontally, verticalArrangement = if (newsList.isNotEmpty()) Arrangement.Top else Arrangement.Center, diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/news/NewsSearchOptionAndHistory.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/news/NewsSearchOptionAndHistory.kt index f6df90e..afefd89 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/news/NewsSearchOptionAndHistory.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/news/NewsSearchOptionAndHistory.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.component.news +import android.content.Context import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.fadeIn @@ -42,6 +43,7 @@ import io.zoemeow.dutschedule.model.news.NewsSearchHistory @OptIn(ExperimentalMaterial3Api::class) @Composable fun NewsSearchOptionAndHistory( + context: Context, modifier: Modifier = Modifier, isVisible: MutableTransitionState, searchHistory: List, @@ -67,7 +69,7 @@ fun NewsSearchOptionAndHistory( modifier = modifier.fillMaxSize(), content = { Text( - "Search method", + context.getString(R.string.news_search_searchoption_method), style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(bottom = 5.dp) ) @@ -84,8 +86,8 @@ fun NewsSearchOptionAndHistory( label = { Text( when (item) { - NewsSearchType.ByTitle -> "By title" - NewsSearchType.ByContent -> "By content" + NewsSearchType.ByTitle -> context.getString(R.string.news_search_searchoption_method_bytitle) + NewsSearchType.ByContent -> context.getString(R.string.news_search_searchoption_method_bycontent) } ) } @@ -94,7 +96,7 @@ fun NewsSearchOptionAndHistory( } ) Text( - "News type", + context.getString(R.string.news_search_searchoption_type), style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(bottom = 5.dp) ) @@ -115,8 +117,8 @@ fun NewsSearchOptionAndHistory( label = { Text( when (item) { - NewsType.Subject -> "News subject" - NewsType.Global -> "News global" + NewsType.Subject -> context.getString(R.string.news_search_searchoption_type_bysubject) + NewsType.Global -> context.getString(R.string.news_search_searchoption_type_byglobal) } ) } @@ -134,7 +136,7 @@ fun NewsSearchOptionAndHistory( verticalAlignment = Alignment.CenterVertically, content = { Text( - "Search history", + context.getString(R.string.news_search_searchoption_history), style = MaterialTheme.typography.titleMedium ) Spacer(modifier = Modifier.weight(1f)) @@ -156,20 +158,17 @@ fun NewsSearchOptionAndHistory( headlineContent = { Column { Text(queryItem.query) - Text( - "Search ${ - when (queryItem.newsMethod) { - NewsSearchType.ByTitle -> "by title" - NewsSearchType.ByContent -> "by content" - } - } in ${ - when (queryItem.newsType) { - NewsType.Subject -> "News subject" - NewsType.Global -> "News global" - } - }", - style = MaterialTheme.typography.bodyMedium, - ) + Text(context.getString( + R.string.news_search_searchoption_history_data, + when (queryItem.newsMethod) { + NewsSearchType.ByTitle -> context.getString(R.string.news_search_searchoption_method_bytitle) + NewsSearchType.ByContent -> context.getString(R.string.news_search_searchoption_method_bycontent) + }, + when (queryItem.newsType) { + NewsType.Subject -> context.getString(R.string.news_search_searchoption_type_bysubject) + NewsType.Global -> context.getString(R.string.news_search_searchoption_type_byglobal) + } + )) } }, leadingContent = { diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/news/NewsSearchResult.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/news/NewsSearchResult.kt index 7ecf5bc..84a5ee5 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/news/NewsSearchResult.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/news/NewsSearchResult.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.component.news +import android.content.Context import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -17,11 +18,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import io.dutwrapper.dutwrapper.model.news.NewsGlobalItem +import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.model.ProcessState import io.zoemeow.dutschedule.utils.endOfListReached @Composable fun NewsSearchResult( + context: Context, modifier: Modifier = Modifier, newsList: List, lazyListState: LazyListState, @@ -43,19 +46,25 @@ fun NewsSearchResult( when (processState) { ProcessState.Running -> { Text( - "Fetching news. Please wait...", + context.getString(R.string.news_search_fetching), textAlign = TextAlign.Center ) } ProcessState.NotRunYet -> { Text( - "Tap search on top to get started.", + context.getString(R.string.news_search_getstarted), + textAlign = TextAlign.Center + ) + } + ProcessState.Failed -> { + Text( + context.getString(R.string.news_search_failed), textAlign = TextAlign.Center ) } else -> { Text( - "No available news matches your search. Try again with new query.", + context.getString(R.string.news_search_noavailablenews), textAlign = TextAlign.Center ) } @@ -66,7 +75,7 @@ fun NewsSearchResult( modifier = Modifier .endOfListReached( lazyListState = lazyListState, - endOfListReached = { + onReached = { onEndOfList?.let { it() } } ), diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogAppBackgroundSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogAppBackgroundSettings.kt index 01ffc57..376b7bf 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogAppBackgroundSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogAppBackgroundSettings.kt @@ -13,7 +13,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import io.zoemeow.dutschedule.activity.PermissionRequestActivity -import io.zoemeow.dutschedule.activity.SettingsActivity import io.zoemeow.dutschedule.model.settings.BackgroundImageOption import io.zoemeow.dutschedule.ui.component.base.DialogBase import io.zoemeow.dutschedule.ui.component.base.DialogRadioButton @@ -55,13 +54,13 @@ fun DialogAppBackgroundSettings( title = String.format( "Your current wallpaper%s", when { - // TODO: This isn't unavailable for Android 14 + // This isn't unavailable for Android 14 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) -> { "\n(This option is unavailable on Android 14)" } // Permission is not granted. (!PermissionRequestActivity.checkPermissionManageExternalStorage().isGranted) -> { - "\n(You need to grant access all file permission)" + "\n(You'll need to grant access all file access permission)" } // Else, no exception else -> { "" } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt index 4700c1d..0e1c151 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt @@ -101,7 +101,7 @@ fun MainActivity.MainViewDashboard( contentColor = contentColor, topBar = { TopAppBar( - title = { Text(text = "DutSchedule") }, + title = { Text(text = context.getString(R.string.app_name)) }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent) ) }, @@ -317,6 +317,7 @@ fun MainActivity.MainViewDashboard( } ) NotificationScaffold( + context = context, itemList = getMainViewModel().notificationHistory, snackBarHostState = snackBarHostState, isVisible = isNotificationOpened.value, diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewTabbed.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewTabbed.kt index c17c027..9baad50 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewTabbed.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewTabbed.kt @@ -132,6 +132,7 @@ fun MainActivity.MainViewTabbed( composable(NavBarItem.notification.route) { NotificationScaffold( + context = context, itemList = getMainViewModel().notificationHistory.toList(), snackBarHostState = snackBarHostState, isVisible = true, diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/NotificationScaffold.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/NotificationScaffold.kt index d793743..6f8ce6c 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/NotificationScaffold.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/NotificationScaffold.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.view.main +import android.content.Context import android.graphics.Bitmap import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.slideInVertically @@ -26,8 +27,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -40,11 +39,11 @@ import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.model.NotificationHistory import io.zoemeow.dutschedule.ui.component.main.NotificationItem import io.zoemeow.dutschedule.utils.CustomDateUtil -import okhttp3.internal.concurrent.TaskRunner @OptIn(ExperimentalMaterial3Api::class) @Composable fun NotificationScaffold( + context: Context, isVisible: Boolean = false, itemList: List, containerColor: Color, @@ -83,7 +82,7 @@ fun NotificationScaffold( contentColor = contentColor, topBar = { TopAppBar( - title = { Text(text = "Notifications") }, + title = { Text(text = context.getString(R.string.notification_panel_title)) }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), actions = { if (itemList.isNotEmpty()) { @@ -124,7 +123,7 @@ fun NotificationScaffold( .padding(top = 15.dp), contentAlignment = Alignment.Center ) { - Text("No notifications") + Text(text = context.getString(R.string.notification_panel_no_notifications)) } } else { itemList.groupBy { p -> p.timestamp } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt index 1667cb8..72d1a17 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt @@ -6,13 +6,10 @@ import androidx.activity.ComponentActivity import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons @@ -21,12 +18,12 @@ import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Search import androidx.compose.material3.BottomAppBar import androidx.compose.material3.BottomAppBarDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold -import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SingleChoiceSegmentedButtonRow @@ -40,18 +37,13 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import com.google.gson.Gson -import io.dutwrapper.dutwrapper.model.enums.NewsSearchType import io.dutwrapper.dutwrapper.model.news.NewsGlobalItem import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.NewsActivity import io.zoemeow.dutschedule.model.ProcessState import io.zoemeow.dutschedule.model.news.NewsFetchType -import io.zoemeow.dutschedule.ui.component.base.ButtonBase import io.zoemeow.dutschedule.ui.component.news.NewsListPage import io.zoemeow.dutschedule.viewmodel.MainViewModel import kotlinx.coroutines.CoroutineScope @@ -104,7 +96,7 @@ fun NewsMainView( contentColor = contentColor, topBar = { TopAppBar( - title = { Text(text = "News") }, + title = { Text(text = context.getString(R.string.news_title)) }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), navigationIcon = { if (onBack != null) { @@ -115,7 +107,7 @@ fun NewsMainView( content = { Icon( Icons.AutoMirrored.Filled.ArrowBack, - "", + context.getString(R.string.back), modifier = Modifier.size(25.dp) ) } @@ -153,7 +145,7 @@ fun NewsMainView( } }, selected = pagerState.currentPage == 0, label = { - Text("Global") + Text(text = context.getString(R.string.news_tabname_global)) } ) SegmentedButton( @@ -163,50 +155,47 @@ fun NewsMainView( } }, selected = pagerState.currentPage == 1, label = { - Text("Subject") + Text(text = context.getString(R.string.news_tabname_subject)) } ) } } }, floatingActionButton = { - if (when (pagerState.currentPage) { - 0 -> { - mainViewModel.newsInstance.newsGlobal.processState.value != ProcessState.Running - } - - 1 -> { - mainViewModel.newsInstance.newsSubject.processState.value != ProcessState.Running - } - - else -> false - } - ) { - FloatingActionButton( - onClick = { - when (pagerState.currentPage) { - 0 -> { + FloatingActionButton( + onClick = { + when (pagerState.currentPage) { + 0 -> { + if (mainViewModel.newsInstance.newsGlobal.processState.value != ProcessState.Running) { mainViewModel.newsInstance.fetchGlobalNews( fetchType = NewsFetchType.ClearAndFirstPage, forceRequest = true ) } + } - 1 -> { + 1 -> { + if (mainViewModel.newsInstance.newsSubject.processState.value != ProcessState.Running) { mainViewModel.newsInstance.fetchSubjectNews( fetchType = NewsFetchType.ClearAndFirstPage, forceRequest = true ) } - - else -> {} } - }, - content = { + + else -> {} + } + }, + content = { + if (pagerState.currentPage == 0 && mainViewModel.newsInstance.newsGlobal.processState.value == ProcessState.Running) { + CircularProgressIndicator(modifier = Modifier.size(24.dp)) + } else if (pagerState.currentPage == 1 && mainViewModel.newsInstance.newsSubject.processState.value == ProcessState.Running) { + CircularProgressIndicator(modifier = Modifier.size(24.dp)) + } else { Icon(Icons.Default.Refresh, "Refresh") } - ) - } + } + ) } ) }, diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsDetail.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsDetail.kt index 1de95f4..caef6a9 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsDetail.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsDetail.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.view.news +import android.content.Context import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -27,12 +28,14 @@ import com.google.gson.reflect.TypeToken import io.dutwrapper.dutwrapper.model.enums.NewsType import io.dutwrapper.dutwrapper.model.news.NewsGlobalItem import io.dutwrapper.dutwrapper.model.news.NewsSubjectItem +import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.NewsActivity import io.zoemeow.dutschedule.ui.component.news.NewsDetailScreen @OptIn(ExperimentalMaterial3Api::class) @Composable fun NewsActivity.NewsDetail( + context: Context, snackBarHostState: SnackbarHostState, containerColor: Color, contentColor: Color @@ -47,7 +50,7 @@ fun NewsActivity.NewsDetail( contentColor = contentColor, topBar = { TopAppBar( - title = { Text("News detail") }, + title = { Text(context.getString(R.string.news_detail_title)) }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), navigationIcon = { IconButton( @@ -58,7 +61,7 @@ fun NewsActivity.NewsDetail( content = { Icon( Icons.AutoMirrored.Filled.ArrowBack, - "", + context.getString(R.string.back), modifier = Modifier.size(25.dp) ) } @@ -71,9 +74,9 @@ fun NewsActivity.NewsDetail( ExtendedFloatingActionButton( content = { Row { - Icon(Icons.Default.Add, "Add to news filter") + Icon(Icons.Default.Add, context.getString(R.string.news_detail_addtofilter_fab)) Spacer(modifier = Modifier.size(3.dp)) - Text("Add to news filter") + Text(context.getString(R.string.news_detail_addtofilter_fab)) } }, onClick = { @@ -82,10 +85,16 @@ fun NewsActivity.NewsDetail( // getMainViewModel().appSettings.value.newsFilterList.add() // } // TODO: Develop a add news filter function for news subject detail. - showSnackBar("This function is in development. Check back soon.") + showSnackBar( + text = context.getString(R.string.feature_not_ready), + clearPrevious = true + ) } catch (ex: Exception) { ex.printStackTrace() - showSnackBar("We can't add this subject in this news to your filter! You can instead add manually them.") + showSnackBar( + text = context.getString(R.string.news_detail_addtofilter_failed), + clearPrevious = true + ) } } ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsSearch.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsSearch.kt index 9054aa2..13a84be 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsSearch.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsSearch.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.google.gson.Gson import io.dutwrapper.dutwrapper.model.enums.NewsType +import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.NewsActivity import io.zoemeow.dutschedule.model.ProcessState import io.zoemeow.dutschedule.ui.component.news.NewsSearchOptionAndHistory @@ -86,13 +87,16 @@ fun NewsActivity.NewsSearch( } }, placeholder = { - Text("Type here to search") + Text(context.getString(R.string.news_search_searchbox_placeholder)) }, trailingIcon = { if (isSearchFocused.targetState) { IconButton( content = { - Icon(Icons.Default.Clear, "") + Icon( + Icons.Default.Clear, + context.getString(R.string.clear) + ) }, onClick = { newsSearchViewModel.query.value = "" @@ -127,7 +131,7 @@ fun NewsActivity.NewsSearch( content = { Icon( Icons.AutoMirrored.Filled.ArrowBack, - "", + context.getString(R.string.back), modifier = Modifier.size(25.dp) ) } @@ -150,6 +154,7 @@ fun NewsActivity.NewsSearch( }, content = { padding -> NewsSearchResult( + context = context, modifier = Modifier .fillMaxSize() .padding(padding) @@ -175,6 +180,7 @@ fun NewsActivity.NewsSearch( } ) NewsSearchOptionAndHistory( + context = context, modifier = Modifier .padding(padding) .padding(horizontal = 10.dp) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt index add82e9..1730a6d 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt @@ -111,7 +111,7 @@ fun SettingsMainView( content = { Icon( Icons.AutoMirrored.Filled.ArrowBack, - "", + context.getString(R.string.back), modifier = Modifier.size(25.dp) ) } @@ -188,13 +188,13 @@ fun SettingsMainView( }, title = context.getString(R.string.settings_option_apptheme), description = String.format( - "%s%s", + "%s %s", when (mainViewModel.appSettings.value.themeMode) { - ThemeMode.FollowDeviceTheme -> "Follow device theme" - ThemeMode.DarkMode -> "Dark mode" - ThemeMode.LightMode -> "Light mode" + ThemeMode.FollowDeviceTheme -> context.getString(R.string.settings_option_apptheme_choice_followdevice) + ThemeMode.DarkMode -> context.getString(R.string.settings_option_apptheme_choice_dark) + ThemeMode.LightMode -> context.getString(R.string.settings_option_apptheme_choice_light) }, - if (mainViewModel.appSettings.value.dynamicColor) " (dynamic color enabled)" else "" + if (mainViewModel.appSettings.value.dynamicColor) context.getString(R.string.settings_option_apptheme_choice_dynamiccolorenabled) else "" ), onClick = { dialogAppTheme.value = true } ) @@ -203,12 +203,12 @@ fun SettingsMainView( leadingIcon = { Icon( imageVector = ImageVector.vectorResource(R.drawable.ic_baseline_contrast_24), - "Black background settings", + context.getString(R.string.settings_option_blackbackground), modifier = Modifier.padding(end = 15.dp) ) }, - title = "Black background", - description = "Make app background to black color. Only in dark mode and turned off background image.", + title = context.getString(R.string.settings_option_blackbackground), + description = context.getString(R.string.settings_option_blackbackground_description), isChecked = mainViewModel.appSettings.value.blackBackground, onValueChanged = { value -> mainViewModel.appSettings.value = @@ -223,15 +223,15 @@ fun SettingsMainView( leadingIcon = { Icon( imageVector = ImageVector.vectorResource(R.drawable.ic_baseline_image_24), - "Background image settings", + context.getString(R.string.settings_option_wallpaperbackground), modifier = Modifier.padding(end = 15.dp) ) }, - title = "Background image", + title = context.getString(R.string.settings_option_wallpaperbackground), description = when (mainViewModel.appSettings.value.backgroundImage) { - BackgroundImageOption.None -> "None" - BackgroundImageOption.YourCurrentWallpaper -> "Your current wallpaper" - BackgroundImageOption.PickFileFromMedia -> "Your picked image" + BackgroundImageOption.None -> context.getString(R.string.settings_option_wallpaperbackground_choice_none) + BackgroundImageOption.YourCurrentWallpaper -> context.getString(R.string.settings_option_wallpaperbackground_choice_currentwallpaper) + BackgroundImageOption.PickFileFromMedia -> context.getString(R.string.settings_option_wallpaperbackground_choice_pickedimage) }, onClick = { dialogBackground.value = true } ) @@ -241,18 +241,18 @@ fun SettingsMainView( ContentRegion( modifier = Modifier.padding(top = 10.dp), textModifier = Modifier.padding(horizontal = 20.dp), - text = "Miscellaneous settings", + text = context.getString(R.string.settings_category_miscellaneous), content = { OptionItem( modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), leadingIcon = { Icon( imageVector = ImageVector.vectorResource(R.drawable.google_fonts_globe_24), - "App language", + context.getString(R.string.settings_option_applanguage), modifier = Modifier.padding(end = 15.dp) ) }, - title = "App language", + title = context.getString(R.string.settings_option_applanguage), description = Locale.getDefault().displayName, onClick = { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -275,8 +275,8 @@ fun SettingsMainView( modifier = Modifier.padding(end = 15.dp) ) }, - title = "Application permissions", - description = "Click here for allow and manage app permissions you granted.", + title = context.getString(R.string.settings_option_apppermission), + description = context.getString(R.string.settings_option_apppermission_description), onClick = { context.startActivity( Intent( @@ -291,12 +291,12 @@ fun SettingsMainView( leadingIcon = { Icon( imageVector = ImageVector.vectorResource(R.drawable.ic_baseline_web_24), - "App language", + context.getString(R.string.settings_option_openlinkinsideapp), modifier = Modifier.padding(end = 15.dp) ) }, - title = "Open link inside app", - description = "Open clicked link without leaving this app. Turn off to open link in default browser.", + title = context.getString(R.string.settings_option_openlinkinsideapp), + description = context.getString(R.string.settings_option_openlinkinsideapp_description), isChecked = mainViewModel.appSettings.value.openLinkInsideApp, onValueChanged = { value -> mainViewModel.appSettings.value = diff --git a/app/src/main/java/io/zoemeow/dutschedule/utils/BackgroundImageUtil.kt b/app/src/main/java/io/zoemeow/dutschedule/utils/BackgroundImageUtil.kt index 311ee7a..cd72b04 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/utils/BackgroundImageUtil.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/utils/BackgroundImageUtil.kt @@ -5,6 +5,7 @@ import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri +import android.os.Build import androidx.core.graphics.drawable.toBitmap import java.io.File @@ -12,6 +13,10 @@ class BackgroundImageUtil { companion object { fun getCurrentWallpaperBackground(context: Context): Bitmap? { return try { + // WallpaperManager API isn't working on Android 13 because Google has deprecated. So, return null instead. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + return null + } val wallpaperManager = WallpaperManager.getInstance(context) wallpaperManager.drawable?.toBitmap() } catch (_: Exception) { @@ -43,6 +48,18 @@ class BackgroundImageUtil { } } + fun deleteImageFromAppData(context: Context): Boolean { + try { + val file = File("${context.filesDir.path}/image/background.jpg") + run { + File("${context.filesDir.path}/image").mkdir() + return file.delete() + } + } catch (_: Exception) { + return false + } + } + // https://stackoverflow.com/questions/75172380/kotlin-jetpack-compose-display-bytearray-or-filestream-as-image-in-android fun getImageFromAppData(context: Context): Bitmap? { return try { diff --git a/app/src/main/java/io/zoemeow/dutschedule/utils/FunctionExtension.kt b/app/src/main/java/io/zoemeow/dutschedule/utils/FunctionExtension.kt index 6b87bf1..afde51d 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/utils/FunctionExtension.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/utils/FunctionExtension.kt @@ -5,6 +5,28 @@ import android.content.Intent import android.net.Uri import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.flow.distinctUntilChanged +import java.math.BigInteger +import java.security.MessageDigest +import java.text.Normalizer fun Context.openLink( url: String, @@ -27,4 +49,91 @@ fun Context.openLink( customTabsIntent.launchUrl(this, Uri.parse(url)) } } +} + +@Composable +fun Modifier.endOfListReached( + lazyListState: LazyListState, + buffer: Int = 1, + onReached: () -> Unit +): Modifier { + val shouldLoadMore = remember { + derivedStateOf { + try { + val layoutInfo = lazyListState.layoutInfo + val totalItemsNumber = layoutInfo.totalItemsCount + val lastVisibleItemIndex = layoutInfo.visibleItemsInfo.last().index + 1 + + lastVisibleItemIndex > (totalItemsNumber - buffer) + } catch (ex: Exception) { + false + } + } + } + LaunchedEffect(shouldLoadMore) { + snapshotFlow { shouldLoadMore.value } + .distinctUntilChanged() + .collect { + if (shouldLoadMore.value) + onReached() + } + } + return this +} + +@Composable +fun RowScope.TableCell( + modifier: Modifier = Modifier, + text: String, + backgroundColor: Color = MaterialTheme.colorScheme.surface, + contentAlign: Alignment = Alignment.Center, + textAlign: TextAlign = TextAlign.Start, + weight: Float +) { + Surface( + modifier = modifier.weight(weight), + border = BorderStroke(0.5.dp, MaterialTheme.colorScheme.inverseSurface), + color = backgroundColor, + content = { + Box( + modifier = Modifier.padding(1.dp), + contentAlignment = contentAlign, + content = { + Text( + text = text, + textAlign = textAlign, + modifier = Modifier.padding(8.dp), + ) + } + ) + } + ) +} + +fun String.toNonAccent(): String { + val temp = Normalizer.normalize(this, Normalizer.Form.NFD) + return "\\p{InCombiningDiacriticalMarks}+".toRegex().replace(temp, "") +} + +// https://stackoverflow.com/a/64171625 +fun String.calcMD5(): String { + val md = MessageDigest.getInstance("MD5") + return BigInteger(1, md.digest(this.toByteArray())).toString(16).padStart(32, '0') +} + +fun String.calcToSumByCharArray(): Int { + var result = 0 + + this.toByteArray().forEach { + result += (it * 5) + } + + return result +} + +fun getRandomString(length: Int): String { + val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9') + return (1..length) + .map { allowedChars.random() } + .joinToString("") } \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/utils/LazyColumnExtension.kt b/app/src/main/java/io/zoemeow/dutschedule/utils/LazyColumnExtension.kt deleted file mode 100644 index 8c6ed60..0000000 --- a/app/src/main/java/io/zoemeow/dutschedule/utils/LazyColumnExtension.kt +++ /dev/null @@ -1,40 +0,0 @@ -package io.zoemeow.dutschedule.utils - -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Modifier -import kotlinx.coroutines.flow.distinctUntilChanged - -@Composable -fun Modifier.endOfListReached( - lazyListState: LazyListState, - buffer: Int = 1, - endOfListReached: () -> Unit -): Modifier { - val shouldLoadMore = remember { - derivedStateOf { - try { - val layoutInfo = lazyListState.layoutInfo - val totalItemsNumber = layoutInfo.totalItemsCount - val lastVisibleItemIndex = layoutInfo.visibleItemsInfo.last().index + 1 - - lastVisibleItemIndex > (totalItemsNumber - buffer) - } catch (ex: Exception) { - false - } - } - } - LaunchedEffect(shouldLoadMore) { - snapshotFlow { shouldLoadMore.value } - .distinctUntilChanged() - .collect { - if (shouldLoadMore.value) - endOfListReached() - } - } - return this -} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/utils/RowScopeExtension.kt b/app/src/main/java/io/zoemeow/dutschedule/utils/RowScopeExtension.kt deleted file mode 100644 index 9c6f03c..0000000 --- a/app/src/main/java/io/zoemeow/dutschedule/utils/RowScopeExtension.kt +++ /dev/null @@ -1,44 +0,0 @@ -package io.zoemeow.dutschedule.utils - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp - -@Composable -fun RowScope.TableCell( - modifier: Modifier = Modifier, - text: String, - backgroundColor: Color = MaterialTheme.colorScheme.surface, - contentAlign: Alignment = Alignment.Center, - textAlign: TextAlign = TextAlign.Start, - weight: Float -) { - Surface( - modifier = modifier.weight(weight), - border = BorderStroke(0.5.dp, MaterialTheme.colorScheme.inverseSurface), - color = backgroundColor, - content = { - Box( - modifier = Modifier.padding(1.dp), - contentAlignment = contentAlign, - content = { - Text( - text = text, - textAlign = textAlign, - modifier = Modifier.padding(8.dp), - ) - } - ) - } - ) -} \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/utils/StringExtension.kt b/app/src/main/java/io/zoemeow/dutschedule/utils/StringExtension.kt deleted file mode 100644 index 7d505ff..0000000 --- a/app/src/main/java/io/zoemeow/dutschedule/utils/StringExtension.kt +++ /dev/null @@ -1,33 +0,0 @@ -package io.zoemeow.dutschedule.utils - -import java.math.BigInteger -import java.security.MessageDigest -import java.text.Normalizer - -fun String.toNonAccent(): String { - val temp = Normalizer.normalize(this, Normalizer.Form.NFD) - return "\\p{InCombiningDiacriticalMarks}+".toRegex().replace(temp, "") -} - -// https://stackoverflow.com/a/64171625 -fun String.calcMD5(): String { - val md = MessageDigest.getInstance("MD5") - return BigInteger(1, md.digest(this.toByteArray())).toString(16).padStart(32, '0') -} - -fun String.calcToSumByCharArray(): Int { - var result = 0 - - this.toByteArray().forEach { - result += (it * 5) - } - - return result -} - -fun getRandomString(length: Int): String { - val allowedChars = ('A'..'Z') + ('a'..'z') + ('0'..'9') - return (1..length) - .map { allowedChars.random() } - .joinToString("") -} \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index ae01a60..421f2c6 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -1,6 +1,10 @@ DutSchedule + Quay lại + Xoá + Tính năng này đang được phát triển. Hãy kiểm tra lại sau. + Thông báo ứng dụng Kênh này sẽ gửi thông báo cập nhật quan trọng của ứng dụng cho bạn. Tin tức chung @@ -9,13 +13,16 @@ Kênh này sẽ gửi thông báo mới trong \"Thông báo lớp học phần\" trên sv.dut.udn.vn. Dịch vụ cập nhật tin tức trong nền Kênh này sẽ đảm bảo dịch vụ này có thể chạy trong nền. Bạn có thể tắt thông báo này nếu bạn không muốn hiển thị chúng. + Thông báo chung - Thông báo mới của %s về %s - Thông báo %s của %s về %s + Thông báo mới của %1$s về %2$s + Thông báo %1$s của %2$s về %3$s nghỉ học học bù - Vào ngày %s với tiết học %s - Phòng sẽ học bù: %s + (không rõ) + Vào ngày %1$s với tiết học %2$s + Phòng sẽ học bù: %1$s + Cài đặt Thông báo Cài đặt cập nhật tin tức trong nền @@ -24,4 +31,45 @@ Nhấn vào đây để quản lí thông báo ứng dụng trong cài đặt Android. Hiển thị Chủ đề ứng dụng + Theo chủ đề hệ thống + Chủ đề tối + Chủ đề sáng + (dynamic color đang bật) + Nền đen + Đặt màu nền của ứng dụng là màu đen. Chỉ hoạt động trong chế độ tối và bạn không bật hình nền ứng dụng. + Hình nền ứng dụng + Đã vô hiệu hoá + Hình nền hiện tại của bạn + Ảnh bạn đã chọn + Cài đặt linh tinh + Ngôn ngữ ứng dụng + Cấp quyền cho ứng dụng + Nhấn vào đây để cho phép và quản lý quyền hạn ứng dụng bạn đã cấp phép. + Mở liên kết trong ứng dụng + Mở liên kết đã nhấp mà không rời khỏi ứng dụng. Vô hiệu hoá để mở trong trình duyệt mặc định của bạn. + + Thông báo + Không có thông báo + + Tin tức + Chung + Lớp học phần + + Chi tiết tin tức + Thêm vào bộ lọc + Chúng tôi không thể thêm lớp học phần này vào bộ lọc tin tức của bạn! Hãy thử lại hoặc thêm bộ lọc này thủ công trong Cài đặt. + + Nhấn vào đây để tìm kiếm + Nhấn thanh tìm kiếm ở trên cùng để bắt đầu. + Đang tải tin tức. Vui lòng chờ… + Đã có lỗi xảy ra khi chúng tôi tải tin tức từ máy chủ. Đảm bảo rằng bạn có kết nối internet và thử lại. + Không có tin tức trùng khớp với từ khoá của bạn. Hãy thử lại với từ khoá mới. + Tìm kiếm theo + Tiêu đề + Nội dung + Kiểu tin tức + Chung + Lớp học phần + Lịch sử tìm kiếm + Tìm kiếm %2$s theo %1$s \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8f8882b..c7faa1f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,10 @@ DutSchedule + Back + Clear + This function is in development. Check back soon. + App updates This will send you app recommend and important updates. News Global @@ -9,13 +13,16 @@ This will receive new messages in \"Thông báo lớp học phần\" on sv.dut.udn.vn. News Background Update Service This will ensure this service will able to run in background. You can turn off this notification if you don\'t want show it. + News global - New announcement with %s about %s - %s lesson(s) with %s about %s + New announcement with %1$s about %2$s + %1$s lesson(s) with %2$s about %3$s Leaving Making up - On %s at lesson(s) %s - Room will make up: %s + (unknown) + On %1$s at lesson(s) %2$s + Room will make up: %1$s + Settings Notifications News schedule in background settings @@ -24,4 +31,45 @@ Click here to manage app notifications in Android app settings. Appearance App theme + Follow device theme + Dark mode + Light mode + (dynamic color enabled) + Black background + Make app background to black color. Only in dark mode and turned off background image. + App background wallpaper + Disabled + Your current wallpaper + Your picked image + Miscellaneous settings + App language + Grant app permissions + Click here for allow and manage app permissions you granted. + Open link inside app + Open clicked link without leaving this app. Disable to open link in default browser. + + Notifications + No notifications + + News + Global + Subject + + News Detail + Add to filter + We can\'t add this subject in this news to your filter! Try again or manually add them in Settings. + + Type here to search + Tap search on top to get started. + Fetching news. Please wait… + A problem prevent us from getting news from server. Make sure you\'re connected to internet, and try again. + No available news matches your query. Try again with new one. + Search method + By title + By content + News type + News Global + News Subject + Search history + Search %1$s in %2$s \ No newline at end of file From 83fe865098925e12b0abc3ba12e0c3461b3c8bf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BA=BF=20T=C3=B9ng?= <47247560+ZoeMeow1027@users.noreply.github.com> Date: Thu, 18 Apr 2024 18:19:29 +0700 Subject: [PATCH 13/21] Update Vietnamese strings --- .../dialog/DialogAppBackgroundSettings.kt | 16 ++--- .../settings/dialog/DialogAppThemeSettings.kt | 20 +++--- .../dialog/DialogSchoolYearSettings.kt | 27 ++++---- .../dutschedule/ui/view/news/MainView.kt | 2 +- .../dutschedule/ui/view/news/NewsDetail.kt | 2 +- .../dutschedule/ui/view/news/NewsSearch.kt | 4 +- .../ui/view/settings/ExperimentSettings.kt | 59 ++++++++++-------- .../ui/view/settings/LanguageSettings.kt | 6 +- .../dutschedule/ui/view/settings/MainView.kt | 31 ++++++---- app/src/main/res/values-vi/strings.xml | 61 ++++++++++++++++++- app/src/main/res/values/strings.xml | 61 ++++++++++++++++++- 11 files changed, 210 insertions(+), 79 deletions(-) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogAppBackgroundSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogAppBackgroundSettings.kt index 376b7bf..5b84a25 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogAppBackgroundSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogAppBackgroundSettings.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.PermissionRequestActivity import io.zoemeow.dutschedule.model.settings.BackgroundImageOption import io.zoemeow.dutschedule.ui.component.base.DialogBase @@ -29,7 +30,7 @@ fun DialogAppBackgroundSettings( modifier = Modifier .fillMaxWidth() .padding(25.dp), - title = "App background", + title = context.getString(R.string.settings_dialog_wallpaperbackground_title), isVisible = isVisible, canDismiss = true, isTitleCentered = true, @@ -43,7 +44,7 @@ fun DialogAppBackgroundSettings( modifier = Modifier.fillMaxWidth(), content = { DialogRadioButton( - title = "None", + title = context.getString(R.string.settings_dialog_wallpaperbackground_choice_none), selected = value == BackgroundImageOption.None, onClick = { onDismiss() @@ -52,15 +53,16 @@ fun DialogAppBackgroundSettings( ) DialogRadioButton( title = String.format( - "Your current wallpaper%s", + "%s%s", + context.getString(R.string.settings_dialog_wallpaperbackground_choice_currentwallpaper), when { // This isn't unavailable for Android 14 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) -> { - "\n(This option is unavailable on Android 14)" + "\n(${context.getString(R.string.settings_dialog_wallpaperbackground_choice_currentwallpaper_disa14)})" } // Permission is not granted. (!PermissionRequestActivity.checkPermissionManageExternalStorage().isGranted) -> { - "\n(You'll need to grant access all file access permission)" + "\n(${context.getString(R.string.settings_dialog_wallpaperbackground_choice_currentwallpaper_dismisperext)})" } // Else, no exception else -> { "" } @@ -77,7 +79,7 @@ fun DialogAppBackgroundSettings( } ) DialogRadioButton( - title = "Choose a image from media", + title = context.getString(R.string.settings_dialog_wallpaperbackground_choice_pickaimage), selected = value == BackgroundImageOption.PickFileFromMedia, onClick = { onDismiss() @@ -90,7 +92,7 @@ fun DialogAppBackgroundSettings( actionButtons = { TextButton( onClick = onDismiss, - content = { Text("Cancel") }, + content = { Text(context.getString(R.string.action_cancel)) }, modifier = Modifier.padding(start = 8.dp), ) } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogAppThemeSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogAppThemeSettings.kt index 44c2fdb..7e24c3f 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogAppThemeSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogAppThemeSettings.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.component.settings.dialog +import android.content.Context import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -24,6 +25,7 @@ import io.zoemeow.dutschedule.ui.component.base.DialogRadioButton @Composable fun DialogAppThemeSettings( + context: Context, isVisible: Boolean = false, themeModeValue: ThemeMode, dynamicColorEnabled: Boolean, @@ -34,7 +36,7 @@ fun DialogAppThemeSettings( modifier = Modifier .fillMaxWidth() .padding(25.dp), - title = "App theme", + title = context.getString(R.string.settings_dialog_apptheme_title), isVisible = isVisible, canDismiss = false, isTitleCentered = true, @@ -46,7 +48,7 @@ fun DialogAppThemeSettings( modifier = Modifier.fillMaxWidth(), content = { DialogRadioButton( - title = "Follow device theme", + title = context.getString(R.string.settings_dialog_apptheme_choice_followdevice), selected = themeModeValue == ThemeMode.FollowDeviceTheme, onClick = { onValueChanged( @@ -56,7 +58,7 @@ fun DialogAppThemeSettings( } ) DialogRadioButton( - title = "Light mode", + title = context.getString(R.string.settings_dialog_apptheme_choice_light), selected = themeModeValue == ThemeMode.LightMode, onClick = { onValueChanged( @@ -66,7 +68,7 @@ fun DialogAppThemeSettings( } ) DialogRadioButton( - title = "Dark mode", + title = context.getString(R.string.settings_dialog_apptheme_choice_dark), selected = themeModeValue == ThemeMode.DarkMode, onClick = { onValueChanged( @@ -76,7 +78,7 @@ fun DialogAppThemeSettings( } ) DialogCheckboxButton( - title = "Dynamic color", + title = context.getString(R.string.settings_dialog_apptheme_choice_dynamiccolor), isChecked = dynamicColorEnabled, onValueChanged = { value -> onValueChanged(themeModeValue, value) @@ -95,11 +97,7 @@ fun DialogAppThemeSettings( modifier = Modifier.size(24.dp), // tint = if (mainViewModel.isDarkTheme.value) Color.White else Color.Black ) - Text( - "Your OS needs at least:\n" + - "- Android 9 to follow device theme.\n" + - "- Android 12 to enable dynamic color." - ) + Text(context.getString(R.string.settings_dialog_apptheme_note)) } ) } @@ -108,7 +106,7 @@ fun DialogAppThemeSettings( actionButtons = { TextButton( onClick = onDismiss, - content = { Text("OK") }, + content = { Text(context.getString(R.string.action_ok)) }, modifier = Modifier.padding(start = 8.dp), ) } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogSchoolYearSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogSchoolYearSettings.kt index c284807..3d0d440 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogSchoolYearSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogSchoolYearSettings.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.component.settings.dialog +import android.content.Context import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -17,6 +18,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.SettingsActivity import io.zoemeow.dutschedule.model.account.SchoolYearItem import io.zoemeow.dutschedule.ui.component.base.DialogBase @@ -25,6 +27,7 @@ import io.zoemeow.dutschedule.ui.component.base.OutlinedTextBox @OptIn(ExperimentalMaterial3Api::class) @Composable fun DialogSchoolYearSettings( + context: Context, isVisible: Boolean = false, dismissRequested: (() -> Unit)? = null, currentSchoolYearItem: SchoolYearItem, @@ -44,7 +47,7 @@ fun DialogSchoolYearSettings( modifier = Modifier .fillMaxWidth() .padding(25.dp), - title = "School year settings", + title = context.getString(R.string.settings_dialog_schyear_title), isVisible = isVisible, canDismiss = false, isTitleCentered = true, @@ -58,7 +61,7 @@ fun DialogSchoolYearSettings( modifier = Modifier.fillMaxWidth(), ) { Text( - "Edit your value below to adjust school year variable (careful when changing settings here)", + context.getString(R.string.settings_dialog_schyear_description), modifier = Modifier.padding(bottom = 10.dp) ) ExposedDropdownMenuBox( @@ -69,14 +72,14 @@ fun DialogSchoolYearSettings( modifier = Modifier .fillMaxWidth() .menuAnchor(), - title = "School year", + title = context.getString(R.string.settings_dialog_schyear_choice_schyear), value = String.format("20%d-20%d", currentSettings.value.year, currentSettings.value.year+1) ) DropdownMenu( expanded = dropDownSchoolYear.value, onDismissRequest = { dropDownSchoolYear.value = false }, content = { - 23.downTo(10).forEach { + 27.downTo(10).forEach { DropdownMenuItem( text = { Text(String.format("20%2d-20%2d", it, it+1)) }, onClick = { @@ -99,11 +102,12 @@ fun DialogSchoolYearSettings( modifier = Modifier .fillMaxWidth() .menuAnchor(), - title = "Semester", + title = context.getString(R.string.settings_dialog_schyear_choice_semester), value = String.format( - "Semester %d%s", + "%s %d%s", + context.getString(R.string.settings_dialog_schyear_choice_semester), if (currentSettings.value.semester <= 2) currentSettings.value.semester else 2, - if (currentSettings.value.semester > 2) " (in summer)" else "" + if (currentSettings.value.semester > 2) " (${context.getString(R.string.settings_dialog_schyear_choice_insummer)})" else "" ) ) DropdownMenu( @@ -113,9 +117,10 @@ fun DialogSchoolYearSettings( 1.rangeTo(3).forEach { DropdownMenuItem( text = { Text(String.format( - "Semester %d%s", + "%s %d%s", + context.getString(R.string.settings_dialog_schyear_choice_semester), if (it <= 2) it else 2, - if (it > 2) " (in summer)" else "" + if (it > 2) " (${context.getString(R.string.settings_dialog_schyear_choice_insummer)})" else "" )) }, onClick = { currentSettings.value = currentSettings.value.clone( @@ -134,12 +139,12 @@ fun DialogSchoolYearSettings( actionButtons = { TextButton( onClick = { onSubmit?.let { it(currentSettings.value) } }, - content = { Text("Save") }, + content = { Text(context.getString(R.string.action_save)) }, modifier = Modifier.padding(start = 8.dp), ) TextButton( onClick = { dismissRequested?.let { it() } }, - content = { Text("Cancel") }, + content = { Text(context.getString(R.string.action_cancel)) }, modifier = Modifier.padding(start = 8.dp), ) } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt index 72d1a17..cc0a166 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt @@ -107,7 +107,7 @@ fun NewsMainView( content = { Icon( Icons.AutoMirrored.Filled.ArrowBack, - context.getString(R.string.back), + context.getString(R.string.action_back), modifier = Modifier.size(25.dp) ) } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsDetail.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsDetail.kt index caef6a9..5cfa0b3 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsDetail.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsDetail.kt @@ -61,7 +61,7 @@ fun NewsActivity.NewsDetail( content = { Icon( Icons.AutoMirrored.Filled.ArrowBack, - context.getString(R.string.back), + context.getString(R.string.action_back), modifier = Modifier.size(25.dp) ) } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsSearch.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsSearch.kt index 13a84be..e18339c 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsSearch.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsSearch.kt @@ -95,7 +95,7 @@ fun NewsActivity.NewsSearch( content = { Icon( Icons.Default.Clear, - context.getString(R.string.clear) + context.getString(R.string.action_clear) ) }, onClick = { @@ -131,7 +131,7 @@ fun NewsActivity.NewsSearch( content = { Icon( Icons.AutoMirrored.Filled.ArrowBack, - context.getString(R.string.back), + context.getString(R.string.action_back), modifier = Modifier.size(25.dp) ) } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt index 46cc0cc..2f39815 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat +import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.SettingsActivity import io.zoemeow.dutschedule.model.settings.BackgroundImageOption import io.zoemeow.dutschedule.ui.component.base.DividerItem @@ -56,7 +57,7 @@ fun SettingsActivity.ExperimentSettings( contentColor = contentColor, topBar = { TopAppBar( - title = { Text("Experiment settings") }, + title = { Text(context.getString(R.string.settings_experiment_title)) }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), navigationIcon = { IconButton( @@ -67,7 +68,7 @@ fun SettingsActivity.ExperimentSettings( content = { Icon( Icons.AutoMirrored.Filled.ArrowBack, - "", + context.getString(R.string.action_back), modifier = Modifier.size(25.dp) ) } @@ -84,13 +85,13 @@ fun SettingsActivity.ExperimentSettings( ContentRegion( modifier = Modifier.padding(top = 10.dp), textModifier = Modifier.padding(horizontal = 20.dp), - text = "Global variable settings", + text = context.getString(R.string.settings_experiment_category_globalvar), content = { OptionItem( modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), - title = "Current school year settings", - description = String.format( - "Year: 20%d-20%d, Semester: %s%s", + title = context.getString(R.string.settings_experiment_option_currentschyear), + description = context.getString( + R.string.settings_experiment_option_currentschyear_description, getMainViewModel().appSettings.value.currentSchoolYear.year, getMainViewModel().appSettings.value.currentSchoolYear.year + 1, when (getMainViewModel().appSettings.value.currentSchoolYear.semester) { @@ -98,7 +99,7 @@ fun SettingsActivity.ExperimentSettings( 2 -> "2" else -> "2" }, - if (getMainViewModel().appSettings.value.currentSchoolYear.semester > 2) " (in summer)" else "" + if (getMainViewModel().appSettings.value.currentSchoolYear.semester > 2) " ${context.getString(R.string.settings_experiment_option_currentschyear_insummer)}" else "" ), onClick = { dialogSchoolYear.value = true @@ -110,57 +111,60 @@ fun SettingsActivity.ExperimentSettings( ContentRegion( modifier = Modifier.padding(top = 10.dp), textModifier = Modifier.padding(horizontal = 20.dp), - text = "Appearance", + text = context.getString(R.string.settings_experiment_category_appearance), content = { OptionItem( modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), - title = "Background opacity", + title = context.getString(R.string.settings_experiment_option_bgopacity), description = String.format( "%2.0f%% %s", (getMainViewModel().appSettings.value.backgroundImageOpacity * 100), if (getMainViewModel().appSettings.value.backgroundImage == BackgroundImageOption.None) { - "(You need enable background image to take effect)" + "(${context.getString(R.string.settings_experiment_option_required_enableimage)})" } else "" ), onClick = { - showSnackBar("This option is in development. Check back soon.", true) + showSnackBar(context.getString(R.string.feature_not_ready), true) /* TODO: Implement here: Background opacity */ } ) OptionItem( modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), - title = "Component opacity", + title = context.getString(R.string.settings_experiment_option_componentopacity), description = String.format( "%2.0f%% %s", (getMainViewModel().appSettings.value.componentOpacity * 100), if (getMainViewModel().appSettings.value.backgroundImage == BackgroundImageOption.None) { - "(You need enable background image to take effect)" + "(${context.getString(R.string.settings_experiment_option_required_enableimage)})" } else "" ), onClick = { - showSnackBar("This option is in development. Check back soon.", true) + showSnackBar(context.getString(R.string.feature_not_ready), true) /* TODO: Implement here: Component opacity */ } ) // https://stackoverflow.com/questions/72932093/jetpack-compose-is-there-a-way-to-restart-whole-app-programmatically OptionSwitchItem( modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), - title = "Main screen dashboard view", + title = context.getString(R.string.settings_experiment_option_dashboardview), isVisible = true, isEnabled = true, isChecked = getMainViewModel().appSettings.value.mainScreenDashboardView, - description = String.format( - "%s", - if (getMainViewModel().appSettings.value.mainScreenDashboardView) "Enabled" else "Disabled (tab view)" - ), + description = when (getMainViewModel().appSettings.value.mainScreenDashboardView) { + true -> context.getString(R.string.settings_experiment_option_dashboardview_choice_enabled) + false -> context.getString(R.string.settings_experiment_option_dashboardview_choice_disabled) + }, onValueChanged = { showSnackBar( - text = String.format( - "This will %s your dashboard view. Application will restart. To confirm, click Confirm button.", - if (getMainViewModel().appSettings.value.mainScreenDashboardView) "disable" else "enable", + text = context.getString( + R.string.settings_experiment_option_dashboardview_warning, + when (getMainViewModel().appSettings.value.mainScreenDashboardView) { + true -> context.getString(R.string.settings_experiment_option_dashboardview_warning_disable) + false -> context.getString(R.string.settings_experiment_option_dashboardview_warning_enable) + } ), clearPrevious = true, - actionText = "Confirm", + actionText = context.getString(R.string.action_confirm), action = { getMainViewModel().appSettings.value = getMainViewModel().appSettings.value.clone( mainScreenDashboardView = !getMainViewModel().appSettings.value.mainScreenDashboardView @@ -185,14 +189,14 @@ fun SettingsActivity.ExperimentSettings( ContentRegion( modifier = Modifier.padding(top = 10.dp), textModifier = Modifier.padding(horizontal = 20.dp), - text = "Troubleshooting", + text = context.getString(R.string.settings_experiment_category_troubleshooting), content = { OptionItem( modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), - title = "Debug log (not work yet)", - description = "Get debug log for this application to troubleshoot issues.", + title = context.getString(R.string.settings_experiment_option_debuglog), + description = context.getString(R.string.settings_experiment_option_debuglog_description), onClick = { - showSnackBar("This option is in development. Check back soon.", true) + showSnackBar(context.getString(R.string.feature_not_ready), true) /* TODO: Implement here: Debug log */ } ) @@ -203,6 +207,7 @@ fun SettingsActivity.ExperimentSettings( } ) DialogSchoolYearSettings( + context = context, isVisible = dialogSchoolYear.value, dismissRequested = { dialogSchoolYear.value = false }, currentSchoolYearItem = getMainViewModel().appSettings.value.currentSchoolYear, diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/LanguageSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/LanguageSettings.kt index 02abb40..dc583f3 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/LanguageSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/LanguageSettings.kt @@ -60,7 +60,7 @@ fun SettingsActivity.LanguageSettings( contentColor = contentColor, topBar = { TopAppBar( - title = { Text("App Language") }, + title = { Text(context.getString(R.string.settings_applanguage_title)) }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), navigationIcon = { IconButton( @@ -120,6 +120,7 @@ fun SettingsActivity.LanguageSettings( Locale.Builder().setLanguageTag(tag).build().apply { LanguageItem( title = this.displayName, + context = context, selected = (currentTag.lowercase() == tag.lowercase()), clicked = { Log.d("AppLanguage", String.format("Requested changes to %s", this.displayName)) @@ -143,6 +144,7 @@ fun SettingsActivity.LanguageSettings( @Composable private fun LanguageItem( title: String, + context: Context, selected: Boolean = false, clicked: (() -> Unit)? = null ) { @@ -163,7 +165,7 @@ private fun LanguageItem( fontSize = 19.sp ) if (selected) { - Icon(Icons.Default.Check, "Selected") + Icon(Icons.Default.Check, context.getString(R.string.tooltip_selected)) } } ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt index 1730a6d..ad27859 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt @@ -111,7 +111,7 @@ fun SettingsMainView( content = { Icon( Icons.AutoMirrored.Filled.ArrowBack, - context.getString(R.string.back), + context.getString(R.string.action_back), modifier = Modifier.size(25.dp) ) } @@ -311,12 +311,12 @@ fun SettingsMainView( leadingIcon = { Icon( imageVector = ImageVector.vectorResource(R.drawable.google_fonts_science_24), - "Experiment settings", + context.getString(R.string.settings_option_experiemntsettings), modifier = Modifier.padding(end = 15.dp) ) }, - title = "Experiment settings", - description = "Our current experiment settings before public.", + title = context.getString(R.string.settings_option_experiemntsettings), + description = context.getString(R.string.settings_option_experiemntsettings_description), onClick = { val intent = Intent(context, SettingsActivity::class.java) intent.action = "settings_experimentsettings" @@ -330,7 +330,7 @@ fun SettingsMainView( modifier = Modifier .padding(top = 10.dp), textModifier = Modifier.padding(horizontal = 20.dp), - text = "About", + text = context.getString(R.string.settings_category_about), content = { OptionItem( modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), @@ -341,10 +341,14 @@ fun SettingsMainView( modifier = Modifier.padding(end = 15.dp) ) }, - title = "Version", - description = "Current version: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})\nClick here to check for update", + title = context.getString(R.string.settings_category_about), + description = context.getString( + R.string.settings_option_version_description, + BuildConfig.VERSION_NAME, + BuildConfig.VERSION_CODE + ), onClick = { - onShowSnackBar?.let { it("This option is in development. Check back soon.", true, null, null) } + onShowSnackBar?.let { it(context.getString(R.string.feature_not_ready), true, null, null) } /* TODO: Implement here: Check for updates */ } ) @@ -357,8 +361,8 @@ fun SettingsMainView( modifier = Modifier.padding(end = 15.dp) ) }, - title = "Changelogs", - description = "Tap to view app changelog", + title = context.getString(R.string.settings_option_changelog), + description = context.getString(R.string.settings_option_changelog_description), onClick = { context.openLink( url = "https://github.com/ZoeMeow1027/DutSchedule/blob/stable/CHANGELOG.md", @@ -375,7 +379,7 @@ fun SettingsMainView( modifier = Modifier.padding(end = 15.dp) ) }, - title = "GitHub (click to open link)", + title = context.getString(R.string.settings_option_github), description = "https://github.com/ZoeMeow1027/DutSchedule", onClick = { context.openLink( @@ -391,6 +395,7 @@ fun SettingsMainView( } ) DialogAppThemeSettings( + context = context, isVisible = dialogAppTheme.value, themeModeValue = mainViewModel.appSettings.value.themeMode, dynamicColorEnabled = mainViewModel.appSettings.value.dynamicColor, @@ -428,9 +433,9 @@ fun SettingsMainView( } else { onShowSnackBar?.let { it( - "You need to grant All files access in application permission to use this feature. You can use \"Choose a image from media\" without this permission.", + context.getString(R.string.permission_missing_all_file_access), true, - "Grant" + context.getString(R.string.action_grant) ) { Intent(context, PermissionRequestActivity::class.java).also { context.startActivity(it) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 421f2c6..b79308e 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -1,9 +1,18 @@ DutSchedule - Quay lại - Xoá Tính năng này đang được phát triển. Hãy kiểm tra lại sau. + Bạn cần phải cho phép Quyền truy cập mọi tệp trong quyền hạn ứng dụng để sử dụng tùy chọn này. + + OK + Lưu + Huỷ bỏ + Quay lại + Xoá + Cho phép + Xác nhận + + Đã chọn Thông báo ứng dụng Kênh này sẽ gửi thông báo cập nhật quan trọng của ứng dụng cho bạn. @@ -47,6 +56,54 @@ Nhấn vào đây để cho phép và quản lý quyền hạn ứng dụng bạn đã cấp phép. Mở liên kết trong ứng dụng Mở liên kết đã nhấp mà không rời khỏi ứng dụng. Vô hiệu hoá để mở trong trình duyệt mặc định của bạn. + Cài đặt thử nghiệm + Các cài đặt thử nghiệm của chúng tôi trước khi đưa vào chính thức. Sử dụng nó một cách thận trọng. + Về ứng dụng + Phiên bản + Hiện tại: %1$s (%2$d)\nNhấp vào đây để kiểm tra cập nhật. + Nhật ký thay đổi + Nhấn để xem ghi chú phát hành của ứng dụng + GitHub (nhấp để mở liên kết) + + Chủ đề ứng dụng + Theo chủ đề hệ thống + Chủ đề tối + Chủ đề sáng + Dynamic color + Hệ điều hành của bạn cần tối thiểu:\n- Android 9 để theo chủ đề hệ thống;\n- Android 12 để bật dynamic color. + Hình nền ứng dụng + Đã vô hiệu hóa + Hình nền hiện tại của bạn + Tùy chọn này không có sẵn trên Android 14 + Bạn cần phải cho phép Quyền truy cập mọi tệp + Chọn một hình ảnh từ phương tiện + + Ngôn ngữ ứng dụng + + Cài đặt thử nghiệm + Cài đặt biến công khai + Năm học hiện tại + Năm: 20%1$d-20%2$d, học kỳ: %3$s%4$s + học kỳ hè + Hiển thị + Bạn cần bật hình nền ứng dụng để tùy chọn có tác dụng + Độ mờ hình nền + Độ mờ thành phần điều khiển + Chế độ xem bảng tổng quát trên màn hình chính + Đã bật + Đã vô hiệu hóa (chế độ xem điều hướng thẻ) + Điều này sẽ %1$s chế độ xem bảng tổng quát của bạn. Ứng dụng sẽ khởi động lại. Nhấn vào \"Xác nhận\" để tiếp tục. + vô hiệu hóa + kích hoạt + Xử lý sự cố + Bản ghi gỡ lỗi (chưa hoạt động) + Xem bản ghi gỡ lỗi ứng dụng để tìm nguyên nhân và xử lý sự cố. + + Cài đặt biến năm học + Chỉnh sửa giá trị để điều chỉnh biến năm học (Cẩn thận khi thay đổi tại đây. Điều này sẽ áp dụng tất cả tính năng trong ứng dụng) + Năm học + Học kỳ + học kỳ hè Thông báo Không có thông báo diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c7faa1f..02c336d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,9 +1,18 @@ DutSchedule - Back - Clear This function is in development. Check back soon. + You need to grant All files access in application permissions to use this option. + + OK + Save + Cancel + Back + Clear + Grant + Confirm + + Selected App updates This will send you app recommend and important updates. @@ -47,6 +56,54 @@ Click here for allow and manage app permissions you granted. Open link inside app Open clicked link without leaving this app. Disable to open link in default browser. + Experiment settings + Our current experiment settings before public. Use this at your own risk. + About + Version + Current: %1$s (%2$d)\nClick here to check for update. + Changelog + Tap to view app changelog + GitHub (click to open link) + + App theme + Follow device theme + Dark mode + Light mode + Dynamic color + Your OS needs at least:\n- - Android 9 to follow device theme;\n- Android 12 to enable dynamic color. + App background wallpaper + Disabled + Your current wallpaper + This option is unavailable on Android 14 + You\'ll need to grant access all file access permission + Pick a image from media + + App language + + Experiment Settings + Global variable settings + Current school year + Year: 20%1$d-20%2$d, semester: %3$s%4$s + in summer + Appearance + You need enable background image to take effect + Background opacity + Component opacity + Main screen dashboard view + Enabled + Disabled (navigation tab view) + This will %1$s your dashboard view. Application will restart. Click \"Confirm\" button to continue. + disable + enable + Troubleshooting + Debug log (not working yet) + Get debug log for this application to find and troubleshoot issues. + + School year variable settings + Edit your value below to adjust school year variable (Careful when changing settings here. This will affect all functions in application) + School year + Semester + in summer Notifications No notifications From f9faa33e69484101556efbea710083d3a4e860f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BA=BF=20T=C3=B9ng?= <47247560+ZoeMeow1027@users.noreply.github.com> Date: Fri, 19 Apr 2024 01:53:48 +0700 Subject: [PATCH 14/21] Update Vietnamese Strings --- .../dutschedule/activity/SettingsActivity.kt | 1 + .../ui/view/account/TrainingResult.kt | 9 ++-- .../view/settings/NewsNotificationSettings.kt | 47 +++++++++---------- .../settings/ParseNewsSubjectNotification.kt | 21 +++++---- app/src/main/res/values-vi/strings.xml | 10 ++++ app/src/main/res/values/strings.xml | 10 ++++ 6 files changed, 62 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/io/zoemeow/dutschedule/activity/SettingsActivity.kt b/app/src/main/java/io/zoemeow/dutschedule/activity/SettingsActivity.kt index 7e20a9b..8475626 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/activity/SettingsActivity.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/activity/SettingsActivity.kt @@ -59,6 +59,7 @@ class SettingsActivity : BaseActivity() { when (intent.action) { "settings_newssubjectnewparse" -> { ParseNewsSubjectNotification( + context = context, snackBarHostState = snackBarHostState, containerColor = containerColor, contentColor = contentColor diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt index 52f7188..13079da 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.AccountActivity import io.zoemeow.dutschedule.model.ProcessState import io.zoemeow.dutschedule.ui.component.base.ButtonBase @@ -66,7 +67,7 @@ fun AccountActivity.TrainingResult( content = { Icon( Icons.AutoMirrored.Filled.ArrowBack, - "", + context.getString(R.string.action_back), modifier = Modifier.size(25.dp) ) } @@ -81,7 +82,7 @@ fun AccountActivity.TrainingResult( getMainViewModel().accountSession.fetchAccountTrainingStatus(force = true) }, content = { - Icon(Icons.Default.Refresh, "Refresh") + Icon(Icons.Default.Refresh, context.getString(R.string.action_refresh)) } ) } @@ -200,14 +201,14 @@ fun AccountActivity.TrainingResult( .padding(bottom = 5.dp) ) OutlinedTextBox( - title = "Khen thuong", + title = "Commend and rewards", value = it.graduateStatus?.info1 ?: "(unknown)", modifier = Modifier .fillMaxWidth() .padding(bottom = 5.dp) ) OutlinedTextBox( - title = "Ky luat", + title = "Discipline", value = it.graduateStatus?.info2 ?: "(unknown)", modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt index a10a5ee..a8f9652 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt @@ -47,6 +47,7 @@ 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 io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.PermissionRequestActivity import io.zoemeow.dutschedule.activity.SettingsActivity import io.zoemeow.dutschedule.model.settings.SubjectCode @@ -98,7 +99,7 @@ fun SettingsActivity.NewsNotificationSettings( content = { Icon( Icons.AutoMirrored.Filled.ArrowBack, - "Back", + context.getString(R.string.action_back), modifier = Modifier.size(25.dp) ) } @@ -114,7 +115,7 @@ fun SettingsActivity.NewsNotificationSettings( onFetchNewsStateChanged = { duration -> if (duration > 0) { if (PermissionRequestActivity.checkPermissionScheduleExactAlarm(context).isGranted) { - // TODO: Fetch news in background onClick + // Fetch news in background onClick val dataTemp = getMainViewModel().appSettings.value.clone( fetchNewsBackgroundDuration = duration ) @@ -184,30 +185,28 @@ fun SettingsActivity.NewsNotificationSettings( getMainViewModel().appSettings.value = dataTemp getMainViewModel().saveSettings(saveSettingsOnly = true) showSnackBar( - text = "Done! You will notify \"${ - when (code) { - -1 -> "nothing" - 0 -> "all subject news notifications" - 1 -> "news match your subject schedule" - 2 -> "news match your filter list" - else -> "(unknown)" - } - }\".", + text = when (code) { + -1 -> "Done! You have disabled news subject notification." + 0 -> "Done! You will notify all subject news notifications." + 1 -> "Done! You will notify news match your subject schedule." + 2 -> "Done! You will notify news match your filter list below." + else -> "(unknown)" + }, clearPrevious = true ) }, subjectFilterList = getMainViewModel().appSettings.value.newsBackgroundFilterList, onSubjectFilterAdd = { - // TODO: Add a filter + // Add a filter dialogAddNew.value = true }, onSubjectFilterDelete = { data -> - // TODO: Delete a filter + // Delete a filter tempDeleteItem.value = data dialogDeleteItem.value = true }, onSubjectFilterClear = { - // TODO: Delete all filters + // Delete all filters dialogDeleteAll.value = true }, opacity = getControlBackgroundAlpha() @@ -217,7 +216,7 @@ fun SettingsActivity.NewsNotificationSettings( isVisible = dialogAddNew.value, onDismiss = { dialogAddNew.value = false }, onDone = { syId, cId, subName -> - // TODO: Add item manually + // Add item manually try { val item = SubjectCode(syId, cId, subName) getMainViewModel().appSettings.value.newsBackgroundFilterList.add(item) @@ -236,7 +235,7 @@ fun SettingsActivity.NewsNotificationSettings( isVisible = dialogDeleteItem.value, onDismiss = { dialogDeleteItem.value = false }, onDone = { - // TODO: Clear item on tempDeleteItem.value + // Clear item on tempDeleteItem.value try { getMainViewModel().appSettings.value.newsBackgroundFilterList.remove(tempDeleteItem.value) getMainViewModel().saveSettings(saveSettingsOnly = true) @@ -258,7 +257,7 @@ fun SettingsActivity.NewsNotificationSettings( isVisible = dialogDeleteAll.value, onDismiss = { dialogDeleteAll.value = false }, onDone = { - // TODO: Clear all items + // Clear all items try { getMainViewModel().appSettings.value.newsBackgroundFilterList.clear() getMainViewModel().saveSettings(saveSettingsOnly = true) @@ -437,7 +436,7 @@ private fun MainView( isEnabled = fetchNewsInBackgroundDuration > 0, isChecked = isNewsGlobalEnabled, onClick = { - // TODO: Refresh news state changed + // Refresh news state changed onNewsGlobalStateChanged?.let { it(!isNewsGlobalEnabled) } } ) @@ -453,7 +452,7 @@ private fun MainView( isEnabled = fetchNewsInBackgroundDuration > 0, isChecked = isNewsSubjectEnabled == -1, onClick = { - // TODO: Subject news notification off - onClick + // Subject news notification off - onClick onNewsSubjectStateChanged?.let { it(-1) } } ) @@ -463,7 +462,7 @@ private fun MainView( isEnabled = fetchNewsInBackgroundDuration > 0, isChecked = isNewsSubjectEnabled == 0, onClick = { - // TODO: Subject news notification all - onClick + // Subject news notification all - onClick onNewsSubjectStateChanged?.let { it(0) } } ) @@ -473,7 +472,7 @@ private fun MainView( isEnabled = fetchNewsInBackgroundDuration > 0, isChecked = isNewsSubjectEnabled == 1, onClick = { - // TODO: Subject news notification your subject schedule - onClick + // Subject news notification your subject schedule - onClick onNewsSubjectStateChanged?.let { it(1) } } ) @@ -483,7 +482,7 @@ private fun MainView( isEnabled = fetchNewsInBackgroundDuration > 0, isChecked = isNewsSubjectEnabled == 2, onClick = { - // TODO: Subject news notification custom list - onClick + // Subject news notification custom list - onClick onNewsSubjectStateChanged?.let { it(2) } } ) @@ -561,7 +560,7 @@ private fun MainView( leadingIcon = { Icon(Icons.Default.Add, "Add a subject news filter") }, isEnabled = isNewsSubjectEnabled == 2, onClick = { - // TODO: Add a subject news filter + // Add a subject news filter onSubjectFilterAdd?.let { it() } } ) @@ -571,7 +570,7 @@ private fun MainView( leadingIcon = { Icon(Icons.Default.Delete, "Clear all subject news filter") }, isEnabled = isNewsSubjectEnabled == 2, onClick = { - // TODO: Clear all subject news filter list + // Clear all subject news filter list onSubjectFilterClear?.let { it() } } ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ParseNewsSubjectNotification.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ParseNewsSubjectNotification.kt index 9e3eca5..e99ad57 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ParseNewsSubjectNotification.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ParseNewsSubjectNotification.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.view.settings +import android.content.Context import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -38,6 +39,7 @@ import io.zoemeow.dutschedule.ui.component.base.SwitchWithTextInSurface @OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsActivity.ParseNewsSubjectNotification( + context: Context, snackBarHostState: SnackbarHostState, containerColor: Color, contentColor: Color @@ -49,7 +51,7 @@ fun SettingsActivity.ParseNewsSubjectNotification( contentColor = contentColor, topBar = { TopAppBar( - title = { Text("New parse method on notification") }, + title = { Text(context.getString(R.string.settings_parsenewssubject_title)) }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), navigationIcon = { IconButton( @@ -60,7 +62,7 @@ fun SettingsActivity.ParseNewsSubjectNotification( content = { Icon( Icons.AutoMirrored.Filled.ArrowBack, - "", + context.getString(R.string.action_back), modifier = Modifier.size(25.dp) ) } @@ -89,14 +91,17 @@ fun SettingsActivity.ParseNewsSubjectNotification( .padding(20.dp), content = { Text( - "New Making up lesson in a subject", + when (getMainViewModel().appSettings.value.newsBackgroundParseNewsSubject) { + true -> context.getString(R.string.settings_parsenewssubject_preview_titleenabled) + false -> context.getString(R.string.settings_parsenewssubject_preview_titledisabled) + }, style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(bottom = 5.dp) ) Text( when (getMainViewModel().appSettings.value.newsBackgroundParseNewsSubject) { - true -> "Lecturer: ...\nOn ... at lesson(s) ...\nRoom will make up: ..." - false -> "Person messaged: Class will MAKED UP at lesson 1-4, date: dd/MM/yyyy, at room A123" + true -> context.getString(R.string.settings_parsenewssubject_preview_descenabled) + false -> context.getString(R.string.settings_parsenewssubject_preview_descdisabled) } ) } @@ -104,7 +109,7 @@ fun SettingsActivity.ParseNewsSubjectNotification( } ) SwitchWithTextInSurface( - text = "Use this feature", + text = context.getString(R.string.settings_parsenewssubject_choice_enable), enabled = true, checked = getMainViewModel().appSettings.value.newsBackgroundParseNewsSubject, onCheckedChange = { @@ -124,10 +129,10 @@ fun SettingsActivity.ParseNewsSubjectNotification( ) { Icon( imageVector = ImageVector.vectorResource(id = R.drawable.ic_baseline_info_24), - contentDescription = "info_icon", + contentDescription = context.getString(R.string.tooltip_info), modifier = Modifier.size(24.dp), ) - Text("Use the new parser for news subject if supported. Turned off or unsupported news subject won't affected.") + Text(context.getString(R.string.settings_parsenewssubject_info)) } } ) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index b79308e..11ef847 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -11,8 +11,10 @@ Xoá Cho phép Xác nhận + Làm mới Đã chọn + Thông tin Thông báo ứng dụng Kênh này sẽ gửi thông báo cập nhật quan trọng của ứng dụng cho bạn. @@ -79,6 +81,14 @@ Chọn một hình ảnh từ phương tiện Ngôn ngữ ứng dụng + + Xử lý tin tức lớp học phần trong thông báo + Thông báo {học bù} của {giáo viên} về {danh sách lớp học} + {Giáo viên} thông báo đến lớp {danh sách lớp học} + Vào ngày {ngày} với tiết học {tiết học}\nPhòng sẽ học bù: {phòng} + {Giáo viên} nhắn: Lớp {học bù} ngày: {ngày},Tiết:{tiết học}, phòng {phòng} + Sử dụng tính năng này + Sử dụng trình xử lý mới cho tin tức lớp học phần nếu có thể. Tắt nó đi hoặc tin tức không được hỗ trợ sẽ không bị ảnh hưởng. Cài đặt thử nghiệm Cài đặt biến công khai diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 02c336d..918ead4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,8 +11,10 @@ Clear Grant Confirm + Refresh Selected + Information App updates This will send you app recommend and important updates. @@ -79,6 +81,14 @@ Pick a image from media App language + + Parse subject news in notification + {Making up} lesson(s) with {Lecturer} about {Class List} + {Lecturer} thông báo đến lớp {Class List} + On {date} at lesson(s) {lessons}\nRoom will make up: {room} + {Lecturer} nhắn: Lớp {Making up} ngày: {date},Tiết:{Lessons}, phòng {Room} + Use this feature + Use the new parser for news subject if supported. Turned off or unsupported news subject won\'t affected. Experiment Settings Global variable settings From 8be8968d70a967c2f437f27d6ef92ce83770cdcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BA=BF=20T=C3=B9ng?= <47247560+ZoeMeow1027@users.noreply.github.com> Date: Fri, 19 Apr 2024 17:13:58 +0700 Subject: [PATCH 15/21] Update Vietnamese Strings --- .../io/zoemeow/dutschedule/GlobalVariables.kt | 2 +- .../dutschedule/model/ProcessVariable.kt | 2 +- .../dutschedule/model/VariableListState.kt | 2 +- .../dutschedule/model/VariableState.kt | 2 +- .../settings/AddNewSubjectFilterDialog.kt | 17 +-- .../settings/DeleteASubjectFilterDialog.kt | 17 +-- .../settings/DeleteAllSubjectFilterDialog.kt | 11 +- .../dutschedule/ui/view/news/MainView.kt | 32 ++++-- .../view/settings/NewsNotificationSettings.kt | 103 +++++++++++------- app/src/main/res/values-vi/strings.xml | 48 +++++++- app/src/main/res/values/strings.xml | 46 ++++++++ 11 files changed, 208 insertions(+), 74 deletions(-) diff --git a/app/src/main/java/io/zoemeow/dutschedule/GlobalVariables.kt b/app/src/main/java/io/zoemeow/dutschedule/GlobalVariables.kt index 193a907..ee4a8bf 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/GlobalVariables.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/GlobalVariables.kt @@ -2,6 +2,6 @@ package io.zoemeow.dutschedule class GlobalVariables { companion object { - const val requestExpiredDuration = 1000 * 60 * 5 + const val REQUEST_EXPIRED_DURATION = 1000 * 60 * 5 } } \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/ProcessVariable.kt b/app/src/main/java/io/zoemeow/dutschedule/model/ProcessVariable.kt index 51b572a..4b60e98 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/ProcessVariable.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/ProcessVariable.kt @@ -19,7 +19,7 @@ data class ProcessVariable( val onAfterRefresh: ((Boolean) -> Unit)? = null ) { private fun isExpired(): Boolean { - return (lastRequest.longValue + GlobalVariables.requestExpiredDuration) < System.currentTimeMillis() + return (lastRequest.longValue + GlobalVariables.REQUEST_EXPIRED_DURATION) < System.currentTimeMillis() } private fun isSuccessfulRequestExpired(): Boolean { diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/VariableListState.kt b/app/src/main/java/io/zoemeow/dutschedule/model/VariableListState.kt index c18ce1a..5828ce5 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/VariableListState.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/VariableListState.kt @@ -15,7 +15,7 @@ data class VariableListState( val parameters: MutableMap = mutableMapOf() ) { fun isExpired(): Boolean { - return (lastRequest.longValue + GlobalVariables.requestExpiredDuration) < System.currentTimeMillis() + return (lastRequest.longValue + GlobalVariables.REQUEST_EXPIRED_DURATION) < System.currentTimeMillis() } fun isSuccessfulRequestExpired(): Boolean { diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/VariableState.kt b/app/src/main/java/io/zoemeow/dutschedule/model/VariableState.kt index 73bf944..a041d4d 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/VariableState.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/VariableState.kt @@ -13,7 +13,7 @@ data class VariableState( val parameters: MutableMap = mutableMapOf() ) { fun isExpired(): Boolean { - return (lastRequest.longValue + GlobalVariables.requestExpiredDuration) < System.currentTimeMillis() + return (lastRequest.longValue + GlobalVariables.REQUEST_EXPIRED_DURATION) < System.currentTimeMillis() } fun isSuccessfulRequestExpired(): Boolean { diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/AddNewSubjectFilterDialog.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/AddNewSubjectFilterDialog.kt index 3c51764..ab4afd1 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/AddNewSubjectFilterDialog.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/AddNewSubjectFilterDialog.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.component.settings +import android.content.Context import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -17,11 +18,13 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.SettingsActivity import io.zoemeow.dutschedule.ui.component.base.DialogBase @Composable fun SettingsActivity.AddNewSubjectFilterDialog( + context: Context, isVisible: Boolean = false, onDismiss: (() -> Unit)? = null, onDone: ((String, String, String) -> Unit)? = null @@ -32,7 +35,7 @@ fun SettingsActivity.AddNewSubjectFilterDialog( DialogBase( modifier = Modifier.fillMaxWidth().padding(25.dp), - title = "Add new filter", + title = context.getString(R.string.settings_newsnotify_newsfilter_dialogadd_title), isVisible = isVisible, canDismiss = false, dismissClicked = { onDismiss?.let { it() } }, @@ -43,7 +46,7 @@ fun SettingsActivity.AddNewSubjectFilterDialog( modifier = Modifier.fillMaxWidth(), ) { Text( - text = "Enter your subject filter (you can view templates in sv.dut.udn.vn) and tap \"Add\" to add to filter above.\n\nExample:\n - 19 | 01 | Subject A\n - xx | 94A | Subject B\n\nNote:\n- You need to enter carefully, otherwise you won\'t received notifications exactly.", + text = context.getString(R.string.settings_newsnotify_newsfilter_dialogadd_description), modifier = Modifier.padding(bottom = 5.dp) ) Column( @@ -64,7 +67,7 @@ fun SettingsActivity.AddNewSubjectFilterDialog( OutlinedTextField( value = schoolYearId.value, onValueChange = { if (it.length <= 2) schoolYearId.value = it }, - label = { Text("School year ID") }, + label = { Text(context.getString(R.string.settings_newsnotify_newsfilter_dialogadd_schyear)) }, modifier = Modifier .fillMaxWidth() .wrapContentHeight() @@ -74,7 +77,7 @@ fun SettingsActivity.AddNewSubjectFilterDialog( OutlinedTextField( value = classId.value, onValueChange = { if (it.length <= 3) classId.value = it }, - label = { Text("Class ID") }, + label = { Text(context.getString(R.string.settings_newsnotify_newsfilter_dialogadd_class)) }, modifier = Modifier .fillMaxWidth() .wrapContentHeight() @@ -85,7 +88,7 @@ fun SettingsActivity.AddNewSubjectFilterDialog( OutlinedTextField( value = subjectName.value, onValueChange = { subjectName.value = it }, - label = { Text("Subject name") }, + label = { Text(context.getString(R.string.settings_newsnotify_newsfilter_dialogadd_schname)) }, modifier = Modifier.fillMaxWidth() ) } @@ -94,12 +97,12 @@ fun SettingsActivity.AddNewSubjectFilterDialog( actionButtons = { TextButton( onClick = { onDismiss?.let { it() } }, - content = { Text("Cancel") }, + content = { Text(context.getString(R.string.action_cancel)) }, modifier = Modifier.padding(start = 8.dp), ) TextButton( onClick = { onDone?.let { it(schoolYearId.value, classId.value,subjectName.value) } }, - content = { Text("Save") }, + content = { Text(context.getString(R.string.action_save)) }, modifier = Modifier.padding(start = 8.dp), ) } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/DeleteASubjectFilterDialog.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/DeleteASubjectFilterDialog.kt index 7a26bd2..3964f14 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/DeleteASubjectFilterDialog.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/DeleteASubjectFilterDialog.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.component.settings +import android.content.Context import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -13,12 +14,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.SettingsActivity import io.zoemeow.dutschedule.model.settings.SubjectCode import io.zoemeow.dutschedule.ui.component.base.DialogBase @Composable fun SettingsActivity.DeleteASubjectFilterDialog( + context: Context, subjectCode: SubjectCode = SubjectCode("", "", ""), isVisible: Boolean = false, onDismiss: (() -> Unit)? = null, @@ -28,7 +31,7 @@ fun SettingsActivity.DeleteASubjectFilterDialog( modifier = Modifier .fillMaxWidth() .padding(25.dp), - title = "Delete subject filter?", + title = context.getString(R.string.settings_newsnotify_newsfilter_dialogdelete_title), isVisible = isVisible, canDismiss = false, dismissClicked = { onDismiss?.let { it() } }, @@ -39,11 +42,9 @@ fun SettingsActivity.DeleteASubjectFilterDialog( modifier = Modifier.fillMaxWidth(), ) { Text( - text = String.format( - "%s\n%s\n\n%s", - "Are you sure you want to delete this filter?", - String.format("%s [%s.Nh%s]", subjectCode.subjectName, subjectCode.studentYearId, subjectCode.classId), - "This action is undone!" + text = context.getString( + R.string.settings_newsnotify_newsfilter_dialogdelete_description, + String.format("%s [%s.Nh%s]", subjectCode.subjectName, subjectCode.studentYearId, subjectCode.classId) ), modifier = Modifier.padding(bottom = 5.dp) ) @@ -52,7 +53,7 @@ fun SettingsActivity.DeleteASubjectFilterDialog( actionButtons = { TextButton( onClick = { onDismiss?.let { it() } }, - content = { Text("No, take me back") }, + content = { Text(context.getString(R.string.settings_newsnotify_newsfilter_dialogdelete_no)) }, modifier = Modifier.padding(start = 8.dp), ) ElevatedButton( @@ -61,7 +62,7 @@ fun SettingsActivity.DeleteASubjectFilterDialog( contentColor = Color.White ), onClick = { onDone?.let { it() } }, - content = { Text("Yes, delete it") }, + content = { Text(context.getString(R.string.settings_newsnotify_newsfilter_dialogdelete_yes)) }, modifier = Modifier.padding(start = 8.dp), ) } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/DeleteAllSubjectFilterDialog.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/DeleteAllSubjectFilterDialog.kt index bb6866b..838a36a 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/DeleteAllSubjectFilterDialog.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/DeleteAllSubjectFilterDialog.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.component.settings +import android.content.Context import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -13,18 +14,20 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.SettingsActivity import io.zoemeow.dutschedule.ui.component.base.DialogBase @Composable fun SettingsActivity.DeleteAllSubjectFilterDialog( + context: Context, isVisible: Boolean = false, onDismiss: (() -> Unit)? = null, onDone: (() -> Unit)? = null ) { DialogBase( modifier = Modifier.fillMaxWidth().padding(25.dp), - title = "Delete all subject filters?", + title = context.getString(R.string.settings_newsnotify_newsfilter_dialogdeleteall_title), isVisible = isVisible, canDismiss = false, dismissClicked = { onDismiss?.let { it() } }, @@ -35,7 +38,7 @@ fun SettingsActivity.DeleteAllSubjectFilterDialog( modifier = Modifier.fillMaxWidth(), ) { Text( - text = "Are you sure you want to delete all subject filter?\n\nThis action is undone!", + text = context.getString(R.string.settings_newsnotify_newsfilter_dialogdeleteall_description), modifier = Modifier.padding(bottom = 5.dp) ) } @@ -43,7 +46,7 @@ fun SettingsActivity.DeleteAllSubjectFilterDialog( actionButtons = { TextButton( onClick = { onDismiss?.let { it() } }, - content = { Text("No, take me back") }, + content = { Text(context.getString(R.string.settings_newsnotify_newsfilter_dialogdelete_no)) }, modifier = Modifier.padding(start = 8.dp), ) ElevatedButton( @@ -52,7 +55,7 @@ fun SettingsActivity.DeleteAllSubjectFilterDialog( contentColor = Color.White ), onClick = { onDone?.let { it() } }, - content = { Text("Yes, delete it") }, + content = { Text(context.getString(R.string.settings_newsnotify_newsfilter_dialogdelete_yes)) }, modifier = Modifier.padding(start = 8.dp), ) } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt index cc0a166..5c7f330 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons @@ -37,6 +38,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.google.gson.Gson import io.dutwrapper.dutwrapper.model.news.NewsGlobalItem @@ -137,25 +139,39 @@ fun NewsMainView( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { - SingleChoiceSegmentedButtonRow { + SingleChoiceSegmentedButtonRow( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 40.dp) + ) { SegmentedButton( shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2), - onClick = { scope.launch { - pagerState.animateScrollToPage(0) - } }, + onClick = { + scope.launch { + pagerState.animateScrollToPage(0) + } + }, selected = pagerState.currentPage == 0, label = { Text(text = context.getString(R.string.news_tabname_global)) } ) SegmentedButton( + modifier = Modifier.wrapContentHeight(), shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2), - onClick = { scope.launch { - pagerState.animateScrollToPage(1) - } }, + onClick = { + scope.launch { + pagerState.animateScrollToPage(1) + } + }, selected = pagerState.currentPage == 1, label = { - Text(text = context.getString(R.string.news_tabname_subject)) + Text( + text = context.getString(R.string.news_tabname_subject), + overflow = TextOverflow.Visible, + softWrap = false, + maxLines = 1 + ) } ) } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt index a8f9652..438a0d4 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt @@ -45,6 +45,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.zoemeow.dutschedule.R @@ -87,7 +88,7 @@ fun SettingsActivity.NewsNotificationSettings( contentColor = contentColor, topBar = { TopAppBar( - title = { Text("News Notification Settings") }, + title = { Text(context.getString(R.string.settings_newsnotify_title)) }, // colors = TopAppBarDefaults.largeTopAppBarColors(containerColor = Color.Transparent, scrolledContainerColor = Color.Transparent), colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent, scrolledContainerColor = Color.Transparent), navigationIcon = { @@ -110,6 +111,7 @@ fun SettingsActivity.NewsNotificationSettings( }, ) { MainView( + context = context, padding = it, fetchNewsInBackgroundDuration = getMainViewModel().appSettings.value.newsBackgroundDuration, onFetchNewsStateChanged = { duration -> @@ -122,14 +124,17 @@ fun SettingsActivity.NewsNotificationSettings( getMainViewModel().appSettings.value = dataTemp getMainViewModel().saveSettings(saveSettingsOnly = true) showSnackBar( - text = "Successfully enabled fetch news in background! News will refresh every $duration minute(s).", + text = context.getString( + R.string.settings_newsnotify_fetchnewsinbackground_enabled, + duration + ), clearPrevious = true ) } else { showSnackBar( - text = "You need to enable Alarms & Reminders in Android app settings to use this feature.", + text = context.getString(R.string.permission_missing_alarm_and_reminders), clearPrevious = true, - actionText = "Open", + actionText = context.getString(R.string.action_grant), action = { Intent(context, PermissionRequestActivity::class.java).also { intent -> context.startActivity(intent) @@ -144,7 +149,7 @@ fun SettingsActivity.NewsNotificationSettings( getMainViewModel().appSettings.value = dataTemp getMainViewModel().saveSettings(saveSettingsOnly = true) showSnackBar( - text = "Successfully disabled fetch news in background!", + text = context.getString(R.string.settings_newsnotify_fetchnewsinbackground_disabled), clearPrevious = true ) } @@ -163,9 +168,10 @@ fun SettingsActivity.NewsNotificationSettings( getMainViewModel().appSettings.value = dataTemp getMainViewModel().saveSettings(saveSettingsOnly = true) showSnackBar( - text = "Successfully ${ - if (enabled) "enabled" else "disabled" - } global news notification!", + text = when (enabled) { + true -> context.getString(R.string.settings_newsnotify_newsglobal_enabled) + false -> context.getString(R.string.settings_newsnotify_newsglobal_disabled) + }, clearPrevious = true ) }, @@ -186,11 +192,12 @@ fun SettingsActivity.NewsNotificationSettings( getMainViewModel().saveSettings(saveSettingsOnly = true) showSnackBar( text = when (code) { - -1 -> "Done! You have disabled news subject notification." - 0 -> "Done! You will notify all subject news notifications." - 1 -> "Done! You will notify news match your subject schedule." - 2 -> "Done! You will notify news match your filter list below." - else -> "(unknown)" + -1 -> context.getString(R.string.settings_newsnotify_newssubject_notify_disabled) + 0 -> context.getString(R.string.settings_newsnotify_newssubject_notify_all) + 1 -> context.getString(R.string.settings_newsnotify_newssubject_notify_matchsubsch) + 2 -> context.getString(R.string.settings_newsnotify_newssubject_notify_matchfilter) + // TODO: No code valid + else -> "----------" }, clearPrevious = true ) @@ -213,6 +220,7 @@ fun SettingsActivity.NewsNotificationSettings( ) } AddNewSubjectFilterDialog( + context = context, isVisible = dialogAddNew.value, onDismiss = { dialogAddNew.value = false }, onDone = { syId, cId, subName -> @@ -222,7 +230,13 @@ fun SettingsActivity.NewsNotificationSettings( getMainViewModel().appSettings.value.newsBackgroundFilterList.add(item) getMainViewModel().saveSettings(saveSettingsOnly = true) showSnackBar( - String.format("Successfully added %s [%s.Nh%s]", subName, syId, subName), + text = context.getString( + R.string.settings_newsnotify_newsfilter_notify_add, + subName, + syId, + ".Nh", + cId + ), clearPrevious = true ) } catch (_: Exception) { } @@ -231,6 +245,7 @@ fun SettingsActivity.NewsNotificationSettings( } ) DeleteASubjectFilterDialog( + context = context, subjectCode = tempDeleteItem.value, isVisible = dialogDeleteItem.value, onDismiss = { dialogDeleteItem.value = false }, @@ -240,10 +255,11 @@ fun SettingsActivity.NewsNotificationSettings( getMainViewModel().appSettings.value.newsBackgroundFilterList.remove(tempDeleteItem.value) getMainViewModel().saveSettings(saveSettingsOnly = true) showSnackBar( - String.format( - "Successfully deleted %s [%s.Nh%s]", + text = context.getString( + R.string.settings_newsnotify_newsfilter_notify_add, tempDeleteItem.value.subjectName, tempDeleteItem.value.studentYearId, + ".Nh", tempDeleteItem.value.classId ), clearPrevious = true @@ -254,6 +270,7 @@ fun SettingsActivity.NewsNotificationSettings( } ) DeleteAllSubjectFilterDialog( + context = context, isVisible = dialogDeleteAll.value, onDismiss = { dialogDeleteAll.value = false }, onDone = { @@ -262,7 +279,7 @@ fun SettingsActivity.NewsNotificationSettings( getMainViewModel().appSettings.value.newsBackgroundFilterList.clear() getMainViewModel().saveSettings(saveSettingsOnly = true) showSnackBar( - "Successfully cleared all filters!", + text = context.getString(R.string.settings_newsnotify_newsfilter_notify_deleteall), clearPrevious = true ) } catch (_: Exception) { } @@ -285,6 +302,7 @@ fun SettingsActivity.NewsNotificationSettings( @OptIn(ExperimentalLayoutApi::class) @Composable private fun MainView( + context: Context, padding: PaddingValues = PaddingValues(0.dp), fetchNewsInBackgroundDuration: Int = 0, onFetchNewsStateChanged: ((Int) -> Unit)? = null, @@ -310,7 +328,7 @@ private fun MainView( .verticalScroll(rememberScrollState()) ) { SwitchWithTextInSurface( - text = "Refresh news in background", + text = context.getString(R.string.settings_newsnotify_fetchnewsinbackground), enabled = true, checked = fetchNewsInBackgroundDuration > 0, onCheckedChange = { @@ -324,11 +342,11 @@ private fun MainView( ContentRegion( modifier = Modifier.padding(top = 10.dp), textModifier = Modifier.padding(horizontal = 20.dp), - text = "Notification settings" + text = context.getString(R.string.settings_newsnotify_category_notification) ) { SimpleCardItem( padding = PaddingValues(horizontal = 20.4.dp, vertical = 5.dp), - title = "Fetch news duration", + title = context.getString(R.string.settings_newsnotify_fetchnewsinbackground_duration), clicked = { }, opacity = opacity, content = { @@ -386,7 +404,7 @@ private fun MainView( if (durationTemp.intValue == min) { Icon( Icons.Default.Check, - "Selected", + context.getString(R.string.tooltip_selected), modifier = Modifier.size(20.dp) ) } @@ -409,7 +427,7 @@ private fun MainView( } }, content = { - Text("Save") + Text(context.getString(R.string.action_save)) } ) } @@ -417,10 +435,10 @@ private fun MainView( ) OptionItem( modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), - title = "News parse method on notification", + title = context.getString(R.string.settings_parsenewssubject_title), description = when (isNewSubjectNotificationParseEnabled) { - true -> "Enabled (special notification for news subject)" - false -> "Disabled (regular notification for news subject)" + true -> context.getString(R.string.settings_newsnotify_parsenewssubject_enabled) + false -> context.getString(R.string.settings_newsnotify_parsenewssubject_disabled) }, onClick = { onNewSubjectNotificationParseStateChanged?.let { it() } } ) @@ -428,10 +446,10 @@ private fun MainView( DividerItem(padding = PaddingValues(top = 5.dp, bottom = 15.dp)) ContentRegion( textModifier = Modifier.padding(horizontal = 20.dp), - text = "Global news notification" + text = context.getString(R.string.settings_newsnotify_newsglobal_title) ) { CheckboxOption( - title = "Enable global news notification", + title = context.getString(R.string.settings_newsnotify_newsglobal_enable), modifierInside = Modifier.padding(horizontal = 6.5.dp), isEnabled = fetchNewsInBackgroundDuration > 0, isChecked = isNewsGlobalEnabled, @@ -444,11 +462,11 @@ private fun MainView( ContentRegion( modifier = Modifier.padding(top = 10.dp), textModifier = Modifier.padding(horizontal = 20.dp), - text = "Subject news notification" + text = context.getString(R.string.settings_newsnotify_newssubject_title) ) { RadioButtonOption( modifierInside = Modifier.padding(horizontal = 6.5.dp), - title = "Off", + title = context.getString(R.string.settings_newsnotify_newssubject_disabled), isEnabled = fetchNewsInBackgroundDuration > 0, isChecked = isNewsSubjectEnabled == -1, onClick = { @@ -458,7 +476,7 @@ private fun MainView( ) RadioButtonOption( modifierInside = Modifier.padding(horizontal = 6.5.dp), - title = "All subject news notifications", + title = context.getString(R.string.settings_newsnotify_newssubject_all), isEnabled = fetchNewsInBackgroundDuration > 0, isChecked = isNewsSubjectEnabled == 0, onClick = { @@ -468,7 +486,7 @@ private fun MainView( ) RadioButtonOption( modifierInside = Modifier.padding(horizontal = 6.5.dp), - title = "Match your subject schedule", + title = context.getString(R.string.settings_newsnotify_newssubject_matchsubsch), isEnabled = fetchNewsInBackgroundDuration > 0, isChecked = isNewsSubjectEnabled == 1, onClick = { @@ -478,7 +496,7 @@ private fun MainView( ) RadioButtonOption( modifierInside = Modifier.padding(horizontal = 6.5.dp), - title = "Follow custom list", + title = context.getString(R.string.settings_newsnotify_newssubject_matchfilter), isEnabled = fetchNewsInBackgroundDuration > 0, isChecked = isNewsSubjectEnabled == 2, onClick = { @@ -490,12 +508,12 @@ private fun MainView( DividerItem(padding = PaddingValues(top = 5.dp, bottom = 15.dp)) ContentRegion( textModifier = Modifier.padding(horizontal = 20.dp), - text = "News subject filter" + text = context.getString(R.string.settings_newsnotify_newsfilter_title) ) { if (isNewsSubjectEnabled != 2) { SimpleCardItem( padding = PaddingValues(horizontal = 20.4.dp, vertical = 7.dp), - title = "News subject filter list is disabled", + title = context.getString(R.string.settings_newsnotify_newsfilter_disabledwarning_title), content = { Column( modifier = Modifier @@ -506,7 +524,7 @@ private fun MainView( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.Start ) { - Text("To manage your subject news filter, please check \"Follow custom list\" option first.") + Text(context.getString(R.string.settings_newsnotify_newsfilter_disabledwarning_description)) } }, clicked = { }, @@ -515,7 +533,7 @@ private fun MainView( } SimpleCardItem( padding = PaddingValues(horizontal = 20.4.dp, vertical = 5.dp), - title = "Your current filter list", + title = context.getString(R.string.settings_newsnotify_newsfilter_list_title), clicked = { }, opacity = opacity, content = { @@ -529,7 +547,7 @@ private fun MainView( horizontalAlignment = Alignment.Start ) { if (subjectFilterList.size == 0) { - Text("Your added subject filter will shown here.") + Text(context.getString(R.string.settings_newsnotify_newsfilter_list_nofilters)) } subjectFilterList.forEach { code -> OptionItem( @@ -545,7 +563,7 @@ private fun MainView( } }, content = { - Icon(Icons.Default.Delete, "Delete") + Icon(Icons.Default.Delete, context.getString(R.string.action_delete)) } ) } @@ -556,8 +574,8 @@ private fun MainView( ) OptionItem( modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), - title = "Add a subject news filter", - leadingIcon = { Icon(Icons.Default.Add, "Add a subject news filter") }, + title = context.getString(R.string.settings_newsnotify_newsfilter_add), + leadingIcon = { Icon(Icons.Default.Add, context.getString(R.string.settings_newsnotify_newsfilter_add)) }, isEnabled = isNewsSubjectEnabled == 2, onClick = { // Add a subject news filter @@ -566,8 +584,8 @@ private fun MainView( ) OptionItem( modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), - title = "Clear all subject news filter list", - leadingIcon = { Icon(Icons.Default.Delete, "Clear all subject news filter") }, + title = context.getString(R.string.settings_newsnotify_newsfilter_deleteall), + leadingIcon = { Icon(Icons.Default.Delete, context.getString(R.string.settings_newsnotify_newsfilter_deleteall)) }, isEnabled = isNewsSubjectEnabled == 2, onClick = { // Clear all subject news filter list @@ -582,6 +600,7 @@ private fun MainView( @Composable private fun MainViewPreview() { MainView( + context = LocalContext.current, fetchNewsInBackgroundDuration = 30, onFetchNewsStateChanged = { }, isNewsGlobalEnabled = true, diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 11ef847..426b3be 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -3,12 +3,15 @@ DutSchedule Tính năng này đang được phát triển. Hãy kiểm tra lại sau. Bạn cần phải cho phép Quyền truy cập mọi tệp trong quyền hạn ứng dụng để sử dụng tùy chọn này. + Bạn cần phải bật quyền Chuông báo và lời nhắc trong cài đặt Android để sử dụng tính năng này. OK + Mở Lưu Huỷ bỏ Quay lại - Xoá + Xóa + Xóa Cho phép Xác nhận Làm mới @@ -114,6 +117,49 @@ Năm học Học kỳ học kỳ hè + + Cài đặt thông báo tin tức + Làm mới tin tức trong nền + Đã vô hiệu hóa làm mới tin tức trong nền! + Tin tức sẽ được làm mới mỗi %1$d phút! + Đã bật (xử lý thông báo cho tin tức lớp học phần) + Đã vô hiệu hóa (thông báo như tin tức chung) + Cài đặt thông báo + Tần suất làm mới tin tức + Thông báo tin tức chung + Bật thông báo tin tức chung + Đã kích hoạt thông báo tin tức chung! + Đã vô hiệu hóa thông báo tin tức chung! + Thông báo tin tức lớp học phần + Vô hiệu hóa + Tất cả tin tức + Theo lịch học của bạn + Theo bộ lọc ở dưới + Xong! Bạn đã vô hiệu hóa thông báo lớp học phần. + Xong! Bạn sẽ được thông báo tất cả lớp học phần. + Xong! Bạn sẽ được thông báo tin tức theo lịch học của bạn. + Xong! Bạn sẽ được thông báo tin tức theo bộ lọc của bạn. + Bộ lọc tin tức lớp học phần + Danh sách tùy chỉnh bộ lọc đã bị tắt + Để quản lý bộ lọc tùy chỉnh của bạn, hãy chọn tùy chọn "Theo bộ lọc ở dưới" trước. + Danh sách bộ lọc tùy chỉnh của bạn + Bộ lọc tùy chỉnh bạn đã thêm sẽ xuất hiện tại đây. + Thêm một bộ lọc tin tưc + Xóa tất cả bộ lọc tin tức + Đã thêm %1$s [%2$s%3$s%4$s]! + Đã xóa %1$s [%2$s%3$s%4$s]! + Đã xóa tất cả bộ lọc tin tức! + Thêm một bộ lọc + Điền thông tin bộ lọc tin tức của bạn (bạn có thể xem mẫu ở sv.dut.udn.vn) và nhấn \"Thêm\" để thêm vào danh sách.\n\nVí dụ:\n- 19 | 01 | Subject A\n- xx | 94A | Subject B\n\nLưu ý:\n- Bạn cần nhập cẩn thận, nếu không bạn sẽ không nhận được thông báo chính xác. + ID năm học + ID lớp + Tên môn học + Xóa bộ lọc tin tức? + Bạn có chắc chắn muốn xóa bộ lọc này?\n%1$s\n\nHành động này không thể hoàn tác. + Không, ra khỏi đây + Đúng, xóa nó + Xóa tất cả bộ lọc tin tức? + Bạn có chắc chắn muốn xóa tất cả bộ lọc tin tức không?\n\nHành động này không thể hoàn tác. Thông báo Không có thông báo diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 918ead4..f0cb02d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3,11 +3,14 @@ DutSchedule This function is in development. Check back soon. You need to grant All files access in application permissions to use this option. + You need to enable Alarms & Reminders in Android app settings to use this feature. OK + Open Save Cancel Back + Delete Clear Grant Confirm @@ -114,6 +117,49 @@ School year Semester in summer + + News Notification Settings + Refresh news in background + Successfully disabled fetch news in background! + News will refresh every %1$d minute(s)! + Enabled (special notification for news subject) + Disabled (notify like news global) + Notification settings + Fetch news duration + Global news notification + Enable global news notification + Successfully enabled global news notifications! + Successfully disabled global news notifications! + Subject news notification + Disabled + All news + Match your subject schedule + Follow custom list + Done! You have disabled news subject notifications. + Done! You will notify all subject news notifications. + Done! You will notify news match your subject schedule list. + Done! You will notify news match your filter list below. + News subject filter + News subject filter list is disabled + To manage your subject news filter, please enable "Follow custom list" option first. + Your current filter list + Your added subject filter will shown here. + Add a subject news filter + Clear subject news filter list + Successfully added %1$s [%2$s%3$s%4$s] + Successfully deleted %1$s [%2$s%3$s%4$s] + Successfully cleared all filters! + Add new filter + Enter your subject filter (you can view templates in sv.dut.udn.vn) and tap \"Add\" to add to filter list.\n\nExample:\n- 19 | 01 | Subject A\n- xx | 94A | Subject B\n\nNote:\n- You need to enter carefully, otherwise you won\'t received notifications exactly. + School year ID + Class ID + Subject name + Delete subject filter? + Are you sure you want to delete this filter?\n%1$s\n\nThis action is undone! + No, take me back + Yes, delete it + Delete all subject filters? + Are you sure you want to delete all subject filter?\n\nThis action is undone? Notifications No notifications From fd87fc5d5ebbef20d6557d5c6f37f8e3aab127ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BA=BF=20T=C3=B9ng?= <47247560+ZoeMeow1027@users.noreply.github.com> Date: Fri, 3 May 2024 18:40:39 +0700 Subject: [PATCH 16/21] Change SplashScreen icon background --- .idea/deploymentTargetSelector.xml | 10 + app/build.gradle | 31 +- app/src/main/ic_launcher-playstore.png | Bin 46363 -> 41272 bytes .../dutschedule/activity/BaseActivity.kt | 15 +- .../zoemeow/dutschedule/model/NavBarItem.kt | 10 +- .../ui/component/main/NotificationItem.kt | 12 +- .../ui/component/news/NewsDetailScreen.kt | 16 +- .../ui/view/account/AccountInformation.kt | 3 +- .../dutschedule/ui/view/account/MainView.kt | 6 +- .../dutschedule/ui/view/account/SubjectFee.kt | 3 +- .../ui/view/account/SubjectInformation.kt | 3 +- .../ui/view/account/TrainingResult.kt | 3 +- .../ui/view/account/TrainingSubjectResult.kt | 3 +- .../ui/view/main/MainViewTabbed.kt | 16 +- .../ui/view/main/NotificationScaffold.kt | 10 +- .../dutschedule/ui/view/news/MainView.kt | 3 +- .../dutschedule/ui/view/news/NewsDetail.kt | 5 +- .../dutschedule/ui/view/news/NewsSearch.kt | 3 +- .../ui/view/settings/ExperimentSettings.kt | 3 +- .../ui/view/settings/LanguageSettings.kt | 5 +- .../dutschedule/ui/view/settings/MainView.kt | 6 +- .../view/settings/NewsNotificationSettings.kt | 3 +- .../settings/ParseNewsSubjectNotification.kt | 3 +- .../dutschedule/utils/CustomDateUtil.kt | 23 +- .../dutschedule/viewmodel/MainViewModel.kt | 11 +- .../res/drawable/ic_launcher_foreground.xml | 773 ++++-------------- app/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 2140 -> 3608 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 3744 -> 3608 bytes app/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 1228 -> 2082 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 2220 -> 2082 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 3076 -> 5174 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 5604 -> 5174 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 5264 -> 8754 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 9176 -> 8754 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 7710 -> 12560 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 13266 -> 12560 bytes app/src/main/res/values-night-v31/themes.xml | 8 + app/src/main/res/values-vi/strings.xml | 7 +- app/src/main/res/values/strings.xml | 7 +- build.gradle | 2 +- 40 files changed, 331 insertions(+), 672 deletions(-) create mode 100644 .idea/deploymentTargetSelector.xml create mode 100644 app/src/main/res/values-night-v31/themes.xml diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 68388f5..fbb9fc9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -53,23 +53,22 @@ android { } dependencies { - implementation 'androidx.core:core-ktx:1.12.0' + implementation 'androidx.core:core-ktx:1.13.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' - implementation 'androidx.activity:activity-compose:1.8.2' - implementation platform('androidx.compose:compose-bom:2024.04.00') - implementation "androidx.compose.ui:ui:1.6.5" - implementation "androidx.compose.ui:ui-tooling-preview:1.6.5" - implementation 'androidx.compose.material3:material3' - implementation platform('androidx.compose:compose-bom:2024.04.00') + implementation 'androidx.activity:activity-compose:1.9.0' + implementation platform('androidx.compose:compose-bom:2024.05.00') + implementation "androidx.compose.ui:ui:1.6.7" + implementation "androidx.compose.ui:ui-tooling-preview:1.6.7" + implementation platform('androidx.compose:compose-bom:2024.05.00') implementation 'androidx.compose.ui:ui-graphics' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' - androidTestImplementation platform('androidx.compose:compose-bom:2024.04.00') - androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.6.5" - androidTestImplementation platform('androidx.compose:compose-bom:2024.04.00') - debugImplementation "androidx.compose.ui:ui-tooling:1.6.5" - debugImplementation "androidx.compose.ui:ui-test-manifest:1.6.5" + androidTestImplementation platform('androidx.compose:compose-bom:2024.05.00') + androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.6.7" + androidTestImplementation platform('androidx.compose:compose-bom:2024.05.00') + debugImplementation "androidx.compose.ui:ui-tooling:1.6.7" + debugImplementation "androidx.compose.ui:ui-test-manifest:1.6.7" implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.appcompat:appcompat-resources:1.6.1" @@ -82,10 +81,10 @@ dependencies { runtimeOnly 'androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0' // https://mvnrepository.com/artifact/androidx.fragment/fragment-ktx - runtimeOnly 'androidx.fragment:fragment-ktx:1.7.0-rc01' + runtimeOnly 'androidx.fragment:fragment-ktx:1.7.0' // https://mvnrepository.com/artifact/androidx.compose.material3/material3 - runtimeOnly 'androidx.compose.material3:material3:1.2.1' + implementation 'androidx.compose.material3:material3:1.2.1' // AlarmManager for restart service after closed // Required to avoid crash on Android 12 - API 31 @@ -126,10 +125,12 @@ dependencies { implementation 'com.github.dutwrapper:dutwrapper-java:v1.9.0' - implementation 'com.google.android.material:material:1.11.0' + implementation 'com.google.android.material:material:1.12.0' // Display time ago in news subject implementation 'com.github.marlonlom:timeago:4.0.3' + + implementation 'androidx.core:core-splashscreen:1.2.0-alpha01' } // Allow references to generated code diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png index 88426674a9b8e5fd4308fd7cae621cea4cdbc72b..371c693263d2495a40b1ea990f06541a0abfa350 100644 GIT binary patch literal 41272 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 diff --git a/app/src/main/java/io/zoemeow/dutschedule/activity/BaseActivity.kt b/app/src/main/java/io/zoemeow/dutschedule/activity/BaseActivity.kt index d80cb98..89467a9 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/activity/BaseActivity.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/activity/BaseActivity.kt @@ -6,6 +6,7 @@ import android.graphics.Bitmap import android.net.Uri import android.os.Bundle import android.os.StrictMode +import android.window.SplashScreen import androidx.activity.ComponentActivity import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent @@ -14,15 +15,18 @@ import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent import androidx.compose.foundation.Image import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.graphics.Color @@ -32,6 +36,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.SoftwareKeyboardController +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.compose.viewModel import io.zoemeow.dutschedule.model.settings.BackgroundImageOption @@ -72,6 +77,11 @@ abstract class BaseActivity: ComponentActivity() { permitAllPolicy() setContent { + // Initialize MainViewModel + if (!isMainViewModelInitialized()) { + mainViewModel = viewModel() + } + // SnackBar state snackBarHostState = remember { SnackbarHostState() } snackBarScope = rememberCoroutineScope() @@ -80,11 +90,6 @@ abstract class BaseActivity: ComponentActivity() { focusManager = LocalFocusManager.current keyboardController = LocalSoftwareKeyboardController.current - // Initialize MainViewModel - if (!isMainViewModelInitialized()) { - mainViewModel = viewModel() - } - DutScheduleTheme( darkTheme = when (mainViewModel.appSettings.value.themeMode) { ThemeMode.DarkMode -> true diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/NavBarItem.kt b/app/src/main/java/io/zoemeow/dutschedule/model/NavBarItem.kt index 82c34f0..a691054 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/NavBarItem.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/NavBarItem.kt @@ -8,32 +8,32 @@ import androidx.compose.ui.graphics.vector.ImageVector import io.zoemeow.dutschedule.R data class NavBarItem( - val title: String, + val titleResId: Int, val icon: ImageVector? = null, val resourceIconId: Int? = null, val route: String ) { companion object { val news = NavBarItem( - title = "News", + titleResId = R.string.news_title, resourceIconId = R.drawable.ic_baseline_newspaper_24, route = "news" ) val account = NavBarItem( - title = "Account", + titleResId = R.string.account_title, icon = Icons.Default.AccountCircle, route = "account" ) val notification = NavBarItem( - title = "Notifications", + titleResId = R.string.notification_panel_title, icon = Icons.Default.Notifications, route = "notifications" ) val settings = NavBarItem( - title = "Settings", + titleResId = R.string.settings_title, icon = Icons.Default.Settings, route = "settings" ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/NotificationItem.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/NotificationItem.kt index 9f9c517..724a2cf 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/NotificationItem.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/NotificationItem.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.component.main +import android.content.Context import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn @@ -23,6 +24,9 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -32,6 +36,7 @@ import io.zoemeow.dutschedule.utils.getRandomString @Composable fun NotificationItem( + context: Context, modifier: Modifier = Modifier, item: NotificationHistory, showDate: Boolean = false, @@ -66,7 +71,10 @@ fun NotificationItem( ) { if (showDate) { Text( - CustomDateUtil.unixToDuration(item.timestamp), + CustomDateUtil.unixToDurationWithLocale( + context = context, + unix = item.timestamp + ), style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(bottom = 5.dp) ) @@ -117,6 +125,7 @@ private fun Preview1() { isRead = false ) NotificationItem( + context = LocalContext.current, item = notificationHistory ) } @@ -134,6 +143,7 @@ private fun Preview2() { isRead = false ) NotificationItem( + context = LocalContext.current, item = notificationHistory ) } \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/news/NewsDetailScreen.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/news/NewsDetailScreen.kt index da11988..aaffd8d 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/news/NewsDetailScreen.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/news/NewsDetailScreen.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.component.news +import android.content.Context import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -30,6 +31,7 @@ import io.zoemeow.dutschedule.utils.CustomDateUtil @Composable fun NewsDetailScreen( + context: Context, newsItem: NewsGlobalItem, newsType: NewsType, padding: PaddingValues = PaddingValues(0.dp), @@ -38,6 +40,7 @@ fun NewsDetailScreen( when (newsType) { NewsType.Global -> { NewsDetailBody_NewsGlobal( + context = context, padding = padding, newsItem = newsItem, linkClicked = linkClicked @@ -45,6 +48,7 @@ fun NewsDetailScreen( } NewsType.Subject -> { NewsDetailBody_NewsSubject( + context = context, padding = padding, newsItem = newsItem as NewsSubjectItem, linkClicked = linkClicked @@ -55,6 +59,7 @@ fun NewsDetailScreen( @Composable private fun NewsDetailBody_NewsGlobal( + context: Context, padding: PaddingValues, newsItem: NewsGlobalItem, linkClicked: ((String) -> Unit)? = null @@ -81,7 +86,10 @@ private fun NewsDetailBody_NewsGlobal( "dd/MM/yyyy", "UTC" ) - } (${CustomDateUtil.unixToDuration(newsItem.date)})", + } (${CustomDateUtil.unixToDurationWithLocale( + context = context, + unix = newsItem.date + )})", style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(vertical = 7.dp) ) @@ -154,6 +162,7 @@ private fun NewsDetailBody_NewsGlobal( @Composable private fun NewsDetailBody_NewsSubject( + context: Context, padding: PaddingValues, newsItem: NewsSubjectItem, linkClicked: ((String) -> Unit)? = null @@ -184,7 +193,10 @@ private fun NewsDetailBody_NewsSubject( "dd/MM/yyyy", "UTC" ) - } (${CustomDateUtil.unixToDuration(newsItem.date)})", + } (${CustomDateUtil.unixToDurationWithLocale( + context = context, + unix = newsItem.date + )})", style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(vertical = 7.dp) ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/AccountInformation.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/AccountInformation.kt index fb9acea..f906731 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/AccountInformation.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/AccountInformation.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.view.account +import android.app.Activity.RESULT_OK import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -52,7 +53,7 @@ fun AccountActivity.AccountInformation( navigationIcon = { IconButton( onClick = { - setResult(ComponentActivity.RESULT_OK) + setResult(RESULT_OK) finish() }, content = { diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt index 72008df..700cb6e 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.view.account +import android.app.Activity.RESULT_OK import android.content.Context import android.content.Intent import androidx.activity.ComponentActivity @@ -30,6 +31,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.AccountActivity import io.zoemeow.dutschedule.model.ProcessState import io.zoemeow.dutschedule.model.account.AccountAuth @@ -61,7 +63,7 @@ fun AccountActivity.MainView( showSnackBar(text = text, clearPrevious = clearPrevious, actionText = actionText, action = action) }, onBack = { - setResult(ComponentActivity.RESULT_OK) + setResult(RESULT_OK) finish() } ) @@ -90,7 +92,7 @@ fun AccountMainView( contentColor = contentColor, topBar = { TopAppBar( - title = { Text("Account") }, + title = { Text(context.getString(R.string.account_title)) }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), navigationIcon = { if (onBack != null) { diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectFee.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectFee.kt index 6d502e4..454e33f 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectFee.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectFee.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.view.account +import android.app.Activity.RESULT_OK import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -53,7 +54,7 @@ fun AccountActivity.SubjectFee( navigationIcon = { IconButton( onClick = { - setResult(ComponentActivity.RESULT_OK) + setResult(RESULT_OK) finish() }, content = { diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt index c546c0b..0bb6e88 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.view.account +import android.app.Activity.RESULT_CANCELED import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -59,7 +60,7 @@ fun AccountActivity.SubjectInformation( navigationIcon = { IconButton( onClick = { - setResult(ComponentActivity.RESULT_CANCELED) + setResult(RESULT_CANCELED) finish() }, content = { diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt index 13079da..0fa93d8 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.view.account +import android.app.Activity.RESULT_OK import android.content.Context import android.content.Intent import androidx.activity.ComponentActivity @@ -61,7 +62,7 @@ fun AccountActivity.TrainingResult( navigationIcon = { IconButton( onClick = { - setResult(ComponentActivity.RESULT_OK) + setResult(RESULT_OK) finish() }, content = { diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingSubjectResult.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingSubjectResult.kt index fe6db53..d5beea1 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingSubjectResult.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingSubjectResult.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.view.account +import android.app.Activity.RESULT_CANCELED import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.compose.foundation.background @@ -155,7 +156,7 @@ fun AccountActivity.TrainingSubjectResult( if (searchEnabled.value) { dismissSearchBar() } else { - setResult(ComponentActivity.RESULT_CANCELED) + setResult(RESULT_CANCELED) finish() } }, diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewTabbed.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewTabbed.kt index 9baad50..4924e6f 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewTabbed.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewTabbed.kt @@ -77,14 +77,14 @@ fun MainActivity.MainViewTabbed( }, icon = { if (it.resourceIconId != null) { - Icon(painter = painterResource(id = it.resourceIconId), it.title) + Icon(painter = painterResource(id = it.resourceIconId), context.getString(it.titleResId)) } else if (it.icon != null) { - Icon(imageVector = it.icon, it.title) + Icon(imageVector = it.icon, context.getString(it.titleResId)) } else { - Icon(imageVector = Icons.Default.Info, it.title) + Icon(imageVector = Icons.Default.Info, context.getString(it.titleResId)) } }, - label = { Text(it.title) } + label = { Text(context.getString(it.titleResId)) } ) } ) @@ -140,12 +140,12 @@ fun MainActivity.MainViewTabbed( contentColor = contentColor, onClick = { item -> if (listOf(1, 2).contains(item.tag)) { - Intent(context, NewsActivity::class.java).also { - it.action = "activity_detail" + Intent(context, NewsActivity::class.java).also { intent -> + intent.action = "activity_detail" for (map1 in item.parameters) { - it.putExtra(map1.key, map1.value) + intent.putExtra(map1.key, map1.value) } - context.startActivity(it) + context.startActivity(intent) } } }, diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/NotificationScaffold.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/NotificationScaffold.kt index 6f8ce6c..703bf67 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/NotificationScaffold.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/NotificationScaffold.kt @@ -106,11 +106,11 @@ fun NotificationScaffold( ) }, snackbarHost = { snackBarHostState?.let { SnackbarHost(hostState = it) } }, - content = { + content = { paddingValues -> Column( modifier = Modifier .fillMaxSize() - .padding(it) + .padding(paddingValues) .padding(horizontal = 15.dp) ) { Column( @@ -130,12 +130,16 @@ fun NotificationScaffold( .toSortedMap(compareByDescending { it }) .forEach(action = { group -> Text( - CustomDateUtil.unixToDuration(group.key), + CustomDateUtil.unixToDurationWithLocale( + context = context, + unix = group.key + ), style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(top = 5.dp, bottom = 4.dp) ) group.value.forEach { item -> NotificationItem( + context = context, modifier = Modifier.padding(top = 2.dp, bottom = 5.dp), isVisible = true, opacity = opacity, diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt index 5c7f330..4ef40a8 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.view.news +import android.app.Activity.RESULT_OK import android.content.Context import android.content.Intent import androidx.activity.ComponentActivity @@ -70,7 +71,7 @@ fun NewsActivity.MainView( componentBackgroundAlpha = getControlBackgroundAlpha(), mainViewModel = getMainViewModel(), onBack = { - setResult(ComponentActivity.RESULT_OK) + setResult(RESULT_OK) finish() } ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsDetail.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsDetail.kt index 5cfa0b3..982f6ef 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsDetail.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsDetail.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.view.news +import android.app.Activity.RESULT_OK import android.content.Context import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Row @@ -55,7 +56,7 @@ fun NewsActivity.NewsDetail( navigationIcon = { IconButton( onClick = { - setResult(ComponentActivity.RESULT_OK) + setResult(RESULT_OK) finish() }, content = { @@ -104,6 +105,7 @@ fun NewsActivity.NewsDetail( when (newsType) { "news_global" -> { NewsDetailScreen( + context = context, padding = it, newsItem = Gson().fromJson(newsData, object : TypeToken() {}.type), newsType = NewsType.Global, @@ -118,6 +120,7 @@ fun NewsActivity.NewsDetail( } "news_subject" -> { NewsDetailScreen( + context = context, padding = it, newsItem = Gson().fromJson(newsData, object : TypeToken() {}.type) as NewsGlobalItem, newsType = NewsType.Subject, diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsSearch.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsSearch.kt index e18339c..2f85a6d 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsSearch.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsSearch.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.view.news +import android.app.Activity.RESULT_OK import android.content.Context import android.content.Intent import androidx.activity.ComponentActivity @@ -124,7 +125,7 @@ fun NewsActivity.NewsSearch( if (isSearchFocused.targetState) { dismissFocus() } else { - setResult(ComponentActivity.RESULT_OK) + setResult(RESULT_OK) finish() } }, diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt index 2f39815..ea0a712 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.view.settings +import android.app.Activity.RESULT_CANCELED import android.content.ComponentName import android.content.Context import android.content.Intent @@ -62,7 +63,7 @@ fun SettingsActivity.ExperimentSettings( navigationIcon = { IconButton( onClick = { - setResult(ComponentActivity.RESULT_CANCELED) + setResult(RESULT_CANCELED) finish() }, content = { diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/LanguageSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/LanguageSettings.kt index dc583f3..b763d76 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/LanguageSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/LanguageSettings.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.view.settings +import android.app.Activity.RESULT_CANCELED import android.app.LocaleManager import android.content.Context import android.os.Build @@ -65,13 +66,13 @@ fun SettingsActivity.LanguageSettings( navigationIcon = { IconButton( onClick = { - setResult(ComponentActivity.RESULT_CANCELED) + setResult(RESULT_CANCELED) finish() }, content = { Icon( Icons.AutoMirrored.Filled.ArrowBack, - "", + context.getString(R.string.action_back), modifier = Modifier.size(25.dp) ) } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt index ad27859..b00979c 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt @@ -1,11 +1,11 @@ package io.zoemeow.dutschedule.ui.view.settings +import android.app.Activity.RESULT_OK import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build import android.provider.Settings -import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -71,7 +71,7 @@ fun SettingsActivity.MainView( showSnackBar(text = text, clearPrevious = clearPrevious, actionText = actionText, action = action) }, onBack = { - setResult(ComponentActivity.RESULT_OK) + setResult(RESULT_OK) finish() } ) @@ -100,7 +100,7 @@ fun SettingsMainView( contentColor = contentColor, topBar = { TopAppBar( - title = { Text(context.getString(R.string.settings_name)) }, + title = { Text(context.getString(R.string.settings_title)) }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), navigationIcon = { if (onBack != null) { diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt index 438a0d4..6be5bd0 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.view.settings +import android.app.Activity.RESULT_OK import android.content.Context import android.content.Intent import androidx.activity.ComponentActivity @@ -94,7 +95,7 @@ fun SettingsActivity.NewsNotificationSettings( navigationIcon = { IconButton( onClick = { - setResult(ComponentActivity.RESULT_OK) + setResult(RESULT_OK) finish() }, content = { diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ParseNewsSubjectNotification.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ParseNewsSubjectNotification.kt index e99ad57..32253bd 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ParseNewsSubjectNotification.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ParseNewsSubjectNotification.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.view.settings +import android.app.Activity.RESULT_OK import android.content.Context import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Arrangement @@ -56,7 +57,7 @@ fun SettingsActivity.ParseNewsSubjectNotification( navigationIcon = { IconButton( onClick = { - setResult(ComponentActivity.RESULT_OK) + setResult(RESULT_OK) finish() }, content = { diff --git a/app/src/main/java/io/zoemeow/dutschedule/utils/CustomDateUtil.kt b/app/src/main/java/io/zoemeow/dutschedule/utils/CustomDateUtil.kt index cfe7310..c106054 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/utils/CustomDateUtil.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/utils/CustomDateUtil.kt @@ -1,7 +1,14 @@ package io.zoemeow.dutschedule.utils import android.annotation.SuppressLint +import android.content.Context +import androidx.compose.ui.platform.LocalConfiguration import com.github.marlonlom.utilities.timeago.TimeAgo +import com.github.marlonlom.utilities.timeago.TimeAgoMessages +import io.zoemeow.dutschedule.R +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlinx.datetime.toLocalDateTime import java.text.SimpleDateFormat import java.util.Calendar import java.util.Date @@ -10,6 +17,7 @@ import java.util.TimeZone import kotlin.time.DurationUnit import kotlin.time.toDuration + class CustomDateUtil { companion object { /** @@ -52,18 +60,25 @@ class CustomDateUtil { return SimpleDateFormat(format, Locale.getDefault()).format(Date()) } - fun unixToDuration(unix: Long = System.currentTimeMillis()): String { + fun unixToDurationWithLocale( + context: Context, + unix: Long = System.currentTimeMillis(), + langTag: String = Locale.getDefault().toLanguageTag() + ): String { val duration = (System.currentTimeMillis() - unix).toDuration(DurationUnit.MILLISECONDS) return when (duration.inWholeHours) { in 0..23 -> { - "Today" + context.getString(R.string.time_today) } in 24..47 -> { - "Yesterday" + context.getString(R.string.time_yesterday) } else -> { - TimeAgo.using(unix) + val localeByLangTag = Locale.forLanguageTag(langTag) + val messages: TimeAgoMessages = + TimeAgoMessages.Builder().withLocale(localeByLangTag).build() + TimeAgo.using(unix, messages) } } } diff --git a/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt b/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt index 4b8177c..589da38 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt @@ -21,6 +21,8 @@ import io.zoemeow.dutschedule.repository.DutRequestRepository import io.zoemeow.dutschedule.repository.FileModuleRepository import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject @@ -162,13 +164,13 @@ class MainViewModel @Inject constructor( } } - private val runOnStartupEnabled = mutableStateOf(true) + private val _runOnStartup = MutableStateFlow(false) + val runOnStartup = _runOnStartup.asStateFlow() + private fun runOnStartup(invokeOnCompleted: (() -> Unit)? = null) { - if (!runOnStartupEnabled.value) + if (_runOnStartup.value) return - runOnStartupEnabled.value = false - appSettings.value = fileModuleRepository.getAppSettings() accountSession.setAccountSession(fileModuleRepository.getAccountSession()) accountSession.setSchoolYear(schoolYearItem = appSettings.value.currentSchoolYear) @@ -193,6 +195,7 @@ class MainViewModel @Inject constructor( forceRequest = true ) }) + _runOnStartup.value = true } ) } diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 47d318e..48a0b2c 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -3,609 +3,174 @@ android:height="108dp" android:viewportWidth="256" android:viewportHeight="256"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp index 6fd3463c3d1b0d6345b485317845030285c85b59..f5b390645b0968c7e3163f87282428435d313e35 100644 GIT binary patch literal 3608 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 e^;MiIn$6kkGE7CN`Z-4vohw(@cDFFcb+}9WY literal 2140 zcmV-i2&4B>Nk&Fg2mkd;Au{&`DF5w4Ahg9xf(z?5Cd}f~=teoKP?ohfDf1U`Z)wV6m`Q2TS zO7aaXa)@L)D^N);$z9U_GlLDF>)2zoZ5wB3Xml@7x3=wg9_>&6+O}=mwr$%S+g5Bl zX}&kM`J!$x&AkINlXZ`6+jiYTlqAWtwg3Mm*Tyq1p6z!L&phX>y)zJG+fLhx($^Q5 znJxH|Db)6#f!jukyiCXN2U&dQARIJH+Zr6CFcs0c!%=oL9eqdF(KFi5(bg(G5pmd# zv%>uh%y+NOgEleSBI=?r{bA{T@CKx8V=iU9a=$SRmegkyO;x!>0WsX-GV_^4>*xb_ z*FT;ClZ`2~uI_L^cTKpQ1{&5eLf4c}!-mo^b2-v=6j|1YM3E*%LMMJU>bHh42pJ=T zpOx{@tF|ur>Px!v){TSxVSb+sSVCAFLx6PXerb(IPTc<(4!vjDWv!qBQXZrKx$SOM ztljeUr{aEz(Ub-b#`|8gTVGb|XTILZAW~Ahp2j2R#&TjUXV!kZME~+1!vBR}_^_K_ z^W_SLc$_qc-aZ=?qsXrPE4ME5sj0YK9J7JATP$ZrKOtW0CtQ7q`!SkjF9RV7&;XL{ zQ@$Uai{3sPiP@MV(p@_m_nVGF2C^Z2(kUvEAls6ohmTbsmx0pGL=QG@V-M@CkgEJfDwZE=4$iIP&i7ApXiI zO)`-i4oN#cG~*EbE3VV+YBL=r?6B#k^4;^=WFkh2Ou79yo`)ls>nrv1#|9EW5_x|X zly{ry+++ezIT8FVv||co`F`DRv?G2vks9@GjIkt}c*;rFyt7^}>Bt)vw;d!vH;rt* z+ON-N+4}964Ze@ca<%CWeKd#(V!z$j87sIU>qAL$;&oH+d#~Luon-h5{FEG@kLE6o z05Qdol8!V={~PrvsCEw1*&bEu0aNy(UVA%BR?kSDj^K6|os4&@YWq;_9{)I^>O{js zzCZ{`!VkK(8>79tSGrW}vUzeH#NAVOn_@e7UmD5!X=T|kZYFTccZJfYH`ulljg_)b z00e|2@)>gdLmTx9Gxgwpkg&Jd+7_kY%vdg#tP$Mdt^KYEkhi&Jpl}@+%~TsdWLFh2 zfe=7Zy|@t(1Bm-fGhFPQehzNyt()5GCr2wvJ(%5SdMAjZ$Pl;cX{YTV1OO`+uhX9J zSxZK<)yzuSQ3C$=O4z?ikH-jefmmmTR?Oe$3&g#8SR^+HLBLwYO%cHTw(ZKCDXF*f zgB4^?Zui^fC}A7ae9-nF-F-!{Bqa*zbI-nAD0o10VqOr_HuNBpf^crBUbdQ{BX#%q z;_ZU)A)gPHTciAm0)lMFl63vz9KcNq;qAiV+N>l0g!Kfj7Q{r?$slcAs`OPYCW#f<7zehf0Jv%i1vAJ zdl+@2(|qoo&oh=96?rUAqFM6YchNk}T13(dI&wO3DsVc&cL}Ny5y^nzpDe*t)`;+T z9{I_lS(+2hmyY+P;x;dCTT0S3A}t;T_T_o&&A_TWoZ21vPYXgs-5Jd zUmi_NvE#1j57(z5^qWKY((%+@(TsNVywthev$`x95+uiCds^V5$mM;h%CZd)h)Ez> zr7Nbh8RJyv(v0`A{$PdiW=0Dp0TWp~*XohE!f&5Rp)e)o)!&RK3xn`-^gp>;_ADzH zzg^{bPk!g*Hm)nY_T_!8UPguhaoe*h54&;;yHVJWyvA8p?HDp(>6RTKf}go;!^_tF z#~;f!y>ic*PUn=6FC!ui;Fz__Z7b}>AEm2gI-Ap4;u)HKBj50`EGemLWzyqEA5oq& z0GJo(1Su&KT%W&|_olwYAe^m^Dc@gHzoB%^`2MxVEv0?P_dk}+C?&O{(*TEn`V1ng z9;;$74gx5MwA}+wrwly>s$US_-4?8T3iR}R!>bPy2pI3VAC>Rq(r*SWcs3rQkjre? zkHx2gpeS@#82bG`o;`Aa?lC__LCk(UxYBInbmBS`s!D^ zsoUACeekrY(spg6&UzmvktDln|56^!i9HL?$7d0-jU=gYveDpUzLpXs$u?~(j`3{U zwr$(iw{06!ux;C#!n18-@7oOAwv8h-Sex#?wM#j7FvGO07zH|H9g~^{j3Cn{C^>-$w@o0RWOl{Qo8E?(WTXdEK1~2+|-?06-?9 z6^0b;l90`%ZoJWd1#a6YIcD1K?hO>F2%{lvh?+(fQ4Qe`-Xs}K-P9@yj_y!UL_&-a z8^noseI-A9Wz@#hi3l-55|BN}P2@MyhKx$gNi02GRbn*n?>5NpFA*am(oHqdH6p|e zX+mBgLlQ7VAgf_~i#)IU1(Bw!HikolL?Bm?al_yuRDjqbM1Zyz(FAOtmuuVgIX9lG zT!+Xp)yWbe@yJI62G6zIb4A}Sm6;HJ~l5}h7= zA}BifNULYWxJt&j$YMFtNoTGL6kA+?aMiTA07_9t)fvM_Dr=nVDnUcRKtyZ!Kn|Hq zMJEs5_1NOk!0%u^Dg8JZrqSk0-`9v}JuD{T&(P@*qML6pM3UDpb89QFe2H!`t{ne0 zdEaMtWvT9^s(mcS^8rBv6kDFI+c6+V8G%>Ib9*!pbj)58gH)3HYufAT*Et?zLBq60 zmkS0aMP2i@RBUxw_OEv4+5H)U6rr=?yagCXG3R9q!-8V8QT1L};fEK$^ddN~4!bXz zMNn40<=MaG>HlQwHm#iFQaEmQ-NNDduV#>{-=+)zAZA2XJRpT#3Mqsm7xohKyuBiU z|E{e1W_H?jze(#)dHG9ElKuMNKc_IH5lF)Ut$AM9r7J7Fxt)sO_C^%>MNpRcTW}#$ z9)bHTCv1f;W$ha;efgh}97o#?FWg*J=jC}?5ydx}#S?%rWpgfKE#)h@FJsFDW!L+s z*|%wXxLSYMrpI!^k8MuH2(j7>2GD6TiZ6|cIO%kE{W!;p<)IO5IhyDdf~z-UAZ8Fv z1#l;6aw&&=d|;U4Lno4Rk{Ig}xij{AVRsDCwdr|KWH&=;YZyAjyf#r=!niq#M@G6O z6-PvRZ|wHL_Be7*k>ifYcV|0-8f5ZCW)g&>3WQp`z9MGR?0`bRA;>d=tIl5+<^v*& z={^YnB;dP%@bIf2F2<_*OfXT^N(-J^{dm zQ3tOAfS^P3#Lzq5`^NiZG4mD;W^3F55Dx^9%+0W-Lz$R}jPmx%{I68>yv~e0)s%>d zZ0Z&7+v8B1Dz8NV02b36jNG+D**1i+eULj4d#UU%MCqgiV@G05At{~8&?o@`%318d z_~BR^{zr>1%qQvn?Q0aSP1Iq;1ROjWR1mgshcNXg-1)ef&&qzoW2#ole$()hs1XpX z3*5p+(nERx#?3R9HUMMVT~HusuW4WOI8mddZWrhM=2P`9$WN04WB_PNR4Xd%_l)el z=uslN7dgmLjxjmz9Hx+kC>Ks5ZvfT|KCS6$3m+j`z zbgwYQquII#vB0}}cNmR=L@0jW;6?o`=_bMKE01p04U5}glw=M=yI?^Ykx7{(7;KM$ z9_;84XyeMp6^tq5xaI8H(WB)38K$sl1C)T(?I!SIuf&@P149w9-w5Xe)6acKqa%=Y z*#^L?E)G$}QO>e|Bs{WXM4UuL%Teim@-rqhayHbe+Z`qKGgs8#%-XO!qwW;T zg@|joKei+JlHhg9f&-E70gnghQL^~Lu<)|-H-uYClFd-|IRq%3-pl>CNeH6>oQt2V zckvU+*&veUulY6O(t2K(fU5h&Y|qfEe1&H|+*a8bStqzAM$0$GrQ>!wh>2d2tzn!b zCbWAriNAoTX&Oy~?4gCT%`57F!{TG;xhwV~<3%UQJlUl`$n6Sa2P%}RI}+P2lmnnN%wc#VtjTfc z9P9J-I{h*^_OACP@cw$$$!^ zvTse*g-DcOdRN?noQ12vJZG9w)WJiEld29N<*IqI-v!xSu$dwTxu(WDj%Pn#^18Mf zEYW=AS?wyhGzNn(5sKrq1l zu-*v(Fh}-viE!H*2t`{)N&o9v%M1)XxYV>*#=K?G};jhFGFm=0L~7MEN{{JD2^AS$nJeVh_A07f&45vB34-i zd(t!-038=Cj5CTkupNp!Ay=DHphu+jWEuZy$CLg4~+DCfP@Hff%7=?rA}l5{JCTqoDl(3{rS2H4wK~&bD-Ec zsT4UrlEc{iKN3*{rNKUw_6*#wG=N^?~HXM|Gk{#EQS;dS4XeBrD0niT&iF81{hAoV<0*h}is$98*Y_r#_(-Eq~KB%$Zd`!?yum-%STCj%%*&~o5d_I+|A0uhl02n*v; zfnmmG#)2^HQnLOr3`vn)seYXk40~S}-`6A*X#j z=RBjEYVZ4y)(`+}S4>t)s#XZYHcQ4St zX3KTgVaW$eJl9-z-F5z-t_T7et2o$#;!i?)h-ghDz&x4H+?GdL%2A9V0|5Ws8$5+) z?}hAI-c=j`NJ*q_hu2m0C{~-_z)!&95elO(ptA=hZeY}xMOMp6fgpQLnU8#7S^n~U z_o4_EFrLH`4?Q%+dCw{awxy=&eh3uqGRT1CT|AEptSAF1g(RtGq5EM0ZY#uKR_b+n zWJErcUT`UIUV4mVI3BL)qC}TqS_18r`BYlTS?t%FI8@Jy^pecaQ63gQAiemnH(L! z@pj&uA}fk`p5}!mnNssMK;VyJ$@{*XRD@xB!MU!xMVzR_ig_8I0jFNgNZy|TgTe7j zq>abIxjH^5bq%=xgzp1?)atX6Tv@1_>8Bw^aCsqP$(tIO2yP#ZQEc!Omb&qDNoy!h zOagb%J}5!;S3)2qS3(8MmL@BUX#{o{mxUkK)eASR9qP6(D_#q~B6{vEfQ+fkM~AeLcJ8^-a<*S@m*n`-xKOq3*b8JL4Y~zu0h`6qZN@;edC&*!cVTlj)Ot4> zIIb1$f}jajpYbTU4=&R($juLy&q4nOpeYNm;p9`+pEJ8vp%a1<>L7~zKOs0 z(40trI%C;Pwd|cl|GhN-eF2XT<6^fSfNxvC_NB?uVXdKy-Uvvse}3u=xYy{Pfc^>T zF&U}Tk?}|89{m&iYK^*p(@+1zev6a(#z#l&H7SN;VLBk@-~99}VE6a4KYdeeEKFbT e^;MiIn$6kkGE7CN`Z-4vohw(@cDFFcb+}9WY literal 3744 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 literal 1228 zcmV;-1T*_mNk&G*1ONb6MM6+kP&iDu1ONapFTe{BwFW`9&E}8&-60|-fUDMJXt=R$ zE6ZM63@MaANJKI8p_z5y4zaf25ZU`&?>9q!kl)?)4H1qb+cvFd+kWrcQT#uB>3wEk zL}d+i>Y#1gHX^cjs(+BBZQB`1YwcNqZQHhOn`OJ=q-XJZ3`0Dxtq z{QoD1*|t~AwpX`7l>}k{0BPGa?or$$ZMXibZQF*SS8pjtU{@%#!jOH~r%(bexqh1f z6kExZl<4gV5H_bs%2+;0cd}<`{0m`?Pi2(Q3-bjd&3$0$@XHPVNWg*U=Uv9-wG`sR z=pNn1_IdNhk!%Y$%U7-og)vudZXX_%>e;k+k-?!JpwWo+-A5?uI-+zvxN&2KgO_Vl z(K2Qx2S9X+GJ6{zOmUX;N82`3*Ak7d$!zs)q49)P5MbL_0S<9KyV$*O3)E zH5#%0$n0#}6d)p0+U`CswA98I9=pzAf0PjO^N8pPl*AZ-i)3Gi(r)a(ef#?LhP%&q z%1-RRefu`osn2}_;E`wEhFa$EU8tYAl;FJRkDY#68;Q;(cpF&Phf1tgB#wnEOd$pIl&_+=|2dc`ZNIK(!r0<{l&yyk5ikxw%^TYW7i1GBM^ zuRxKaoC?KtasnuEvI~bNo<2aV0l@5_R5@OS<3VDddJ9BlJB|xrZg4a^BoyU+)~qoM z!4arsFq!}k^(xTk$c_*+08hEc1K)faED(yF*tIwUK^V%t*Q3{ZMD;-`WDb8506E5P zSjPb+WgMk?1O_WicHCdC;&F0pMt&D|JF(x5?TfRQmWh)_{kx8t=3|-AhAL%56ihmn zkP(wH)jX`Y1d%Y<)a9?*XX!y^S(Zw$$-x**(n5Os`)`Z>zPyk8Mcb_hsli~pTfMm( zvG&#W$hUkmzT9n({NDfk%}fR>lTbJoj1b-FhxpK>ps5H~>Wz2n5N(|!31Ot(W1WVG zW5J5Soq0Z)41C~c-kpAXkPai#=+QHTb*CQULxaMm zB0SkA)~=JR5Nwg3jL4+8aJK(n$7B%R{r!2f_jgC)8?N_`#Z3S@Hut1&SL*fUFMH(6 zN?~e5ZvOcXO;uV}Q<9qhKVfzN diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp index 5607e58de57af2ef753a4096a84a10027e17fa6a..4f9d658e6efd8cb1f429e16f8e756ab27bb4dcda 100644 GIT binary patch literal 2082 zcmV+-2;KKmNk&E*2mkghKQJeOb%F;lC=G6 zrRui{Yz?jpUR*C`LAScXxMpcXw;|B!A@OpPU_hxI@n1RB-*_a;1HgJMxRS z70xK#*IiF{MZmRf+t@Sir~i*_Ta}H@wr#7k`2e=_3O0WCtYbU*zIzzBZKNn!Jcgw= z(ABo>m|pE+w!pS++qP{x8Qc6kw(Xhlo0;sw-^Hq(JXM_3DQ&Y$=~T{I$00$s?X;1! z|CKZ|GtbygF*7q-U#u_5foxk%D~l4Omk7xBgEiGa+BW{}jLF*{MsC|U;`FwA^CMuW zK!Ox>8g!=l(rkFCAOwXy3TnJ8K`Rs~mK*@Qp2tg&hfkY*g!Nk;|ib2jDY7IW;hQTPi`Hyrm7%gkD) z5C|w$9=RN|Nk|%OK$&tLNp6z&)Fs~}A~HMO=L;4coZT#AxSVH7{o@gb&Af%JCmw3E zSsi#ThM`UJ)Wj3m>3zFGHsZ#3DiDB1=z^6YbPQVuH!Lpt7?ZeTV zssEiKJ1y@ifa6HANr(xQEEWfgh`?W-+hVIVa3wdYw7=Aw+*%KAO-8qBvzO-3E!2t2 zKP*;JcLr*WUpmrmsHnOyP&qxU*N)^q`(j~z1L8e$p&2J<+5c|OR!Y=Vs7TO|SncC9 zH9OXyFQiseq4rURng*3w>WPJp?EgE#iUGQNvUr3SIxb6?ATyygR+^v_2krQ3wtk8S z>N|kN2|XF=2;CqK#MTG|jwSAkffePux7a}R4qGx3ff^x;Nvj4OWYjcSCZoFLQW_oV)ffeB-`R>j8MC@tZTU?ZPc4bJ=7!M1w1T5`B|Nmg^W(#Gq(KtCm z^Z>!p>PIpAZ^0}!rsSuABPriN1&)WMgA&^eYjK!){Zts6W$gi^&diVS7IP|i{v z1^q+Q)k*X3;NMumt$`*Rj2Z2qlQ%~NRxT4t!`p~613clOGjzH^q5i%QIpm;PPXMgu z%G%1t!(zd_(L^Wh*TuQ;M)6{Sev-o*FNp$1YDXyzaC$+AMmvT^^I{y&j%}V4corEd z4c4qCh-v_Uv8?I{s3F}jj3q~dt?Gsbz$~lS8w1uEkB?R;aDgWCfdQe<-Q0oEE=K%ipr>_8Z9t?KfL^4iEl~kOduvrtPAe~Y^uFnM8t3VQT+i~1}7Ji1XjpEMi| zL)zc&ZilVCV_(^ol2U@XijcpPwplFGL1)%<CQc&jO2cB=Q?-Jiud zutr(J-eaJNC;o_zIQZ~6oqu8NOjOn9M)rI&s8D}ybU$51(e1$gp1ps>dyZpd zu|dLC357ts6yM(V;F)H3SiD$9*62cd4y*pgDdE4)w_^L{uLs4;A!9gGr4U@cg;pBm zJcULp`Sam)DLF#+a>s9jO#d_*@C>9sVEEEDLjKV%WPafq2Da9d{y+MA0X!p*ck-e6 z8vHZMT+Mi=AG*KpNePesi-7Lq+4^x9b`_E=CvId2v5k;>1iq=mKmXugeEctYxV?1` zgM6UioUu|!?@!$0FaFt^_NU8e4j|_!jrb|OuPk=tkY3nZIIh$aUqAvyW} z#fR`l`|%?VldNL^0P=*2vx$FhT|Wo_a6Z9=E4xll3J57VX8jVS5<)c!4zg>HpdoL^gFLpF literal 2220 zcmV;d2vhe`Nk&Gb2mk@OiACIFFqDP8;j z6-ml{iQR=~aD4#Yz#U$Lvmq{#rHl1w|8 z&o;jeIoGxwl5~n=+je9lvu)eTZ2kb-6WccPRy>|rv#Op6kRxsXA}NP-k9H$h+}$B^ zcXxMpXdU-yZ>GmxvX;AS)h{NW>J+=e{}eb8DWT?MdkL;UaR5@ng zQ)9zXY{F3iOEGn|sR0s+1h5!Xl!jE$FeS<{3?xDsV6mX$h`h6UQpSp{2f3$XOHm9! zhbW4vfkg&^L?8eRhEx=jYcT?O3!_Wn`EQP&Es!L!W%;`Ema6?@XY!DdxZr>m#Sv9A z!XOZMY1mY&I3{;_q%L24ubh3wzLDA{5SKu;3^G}YE@%7qxN!Jy`{^IGD7N7ZP(dBE zhYtgft`w1bDLiby-BE15G~!RUKTXm=M^{3bOSa)1znyy9qf4F^xU55w%E9fA4}6yl zFu0$?ygqBOV@21T(P4iC;IlRI_M}qySfCi9(qi7(a5_ylpnu$O*fS|{I zhvNZamd|D3n3AHOAQqs{6V6XoODD4IMI}{%cm-6E0UEVrDhr+jcPjmTEX#dZ$6_AI z3hOZdT(Y8#_71F_SSMko9TlKKIj{%$&=8kVq&29aOTH)+Nj@=f0oT?bATN zY<$y@Ut^bCd!a4HFdtJL*_&Y}q2@!8&43256-EY660>-uEgPa8yJp2HEw(S+>HEu{ zg{75at<*=gykZ4uS!n5pc|Y}I9uiagaK%uB{^LNVB!G2OKNgQ8|8+tE zjbrhO5gLv|Cyg) zZk!Fik_9gmTg}wt8~Yt|lcNXoN}*4id3e8;BQecuZgea7D$P?FhN3*E;M+`iVNGZb z0fzD+98l`j#rgwe6=v(v9~Zv0${r0L)${KYTDUBcM~m1sp5l;()=)}2bIedf0R6{0 zRQ?!-jxLAnYwSH9v1@C^9=#b)-{drca%9fgx=Aq%kO0cmO%jOMQA}>t`!9Or-Vq!o zLLQ+WEJ^(z92&?l1}29iCKJwKd;@G(KVj|ohfPZlY+k$J=GAd=|ApOdDa2)O*;f3mBhSm=`g}&XmI@25*+oLnXEu|fKU>IW&CC?-KKbO! z=T}7!V34cpYbWQWgXf*Ar?)Z{Wq>%L8|7cPlc;y|3G1`QuzYI~D2OKA8CL7CSO=QZ z^GLAjkm1`4$o;&uxfZO(GQ+xi8RF&c4D-e5KY~BhCq2*0`@FOd9m=1D74ma=WYtpn|gl@`Ct)Ph6cl7K|1z8tf{%#f{#4o=4R6O z%ugr7@!?JZ3R>*W&~JzH>@;l6@u7UF2PRO>9K845d(G;{6At6cHXQ%^$cNpFD0EO{ zC@}OGb`udkH_hv?yaQMJAUT-FBX~RO_I;7;$kkgX3})E>|3Yy}NHk)`p&1NA_5Y_Y u9TwLy2^@={;`u*a`t<+Ty`?iWa!}qLw$ocLFq9Z7kDl|wTb=xJ0BHaLDJkv% diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp index 8b30a1a600ff738739c64bc5210ca74079989ef7..c099cdd47ffad72dfd2382ebc047281df10408eb 100644 GIT binary patch literal 5174 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 literal 3076 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 S>CevrdPVpD&ON6CO922I$>&)B diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp index 4240685dd489a18c65f9f5ba2bf9a9dfa5012952..c099cdd47ffad72dfd2382ebc047281df10408eb 100644 GIT binary patch literal 5174 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 literal 5604 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 literal 5264 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 WQgXg;-yj0++s8RoVopHXv{C@^mLfm^ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp index b75745ebbb2de750b4c788cf630ea313da2ef1e0..c4d2c93dad3a72a1783cc3b8211f8353ca316e92 100644 GIT binary patch literal 8754 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 literal 9176 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 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp index e07c0d6fd3d70057ce4a629df74282ff98905f1b..d74963fecb299310e046a59bc1a9da091bf2c311 100644 GIT binary patch literal 12560 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 literal 7710 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 literal 13266 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 Qu#zK+2rCifdbG?b04%LJy8r+H diff --git a/app/src/main/res/values-night-v31/themes.xml b/app/src/main/res/values-night-v31/themes.xml new file mode 100644 index 0000000..d04320c --- /dev/null +++ b/app/src/main/res/values-night-v31/themes.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 426b3be..0dcbbc1 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -4,6 +4,9 @@ Tính năng này đang được phát triển. Hãy kiểm tra lại sau. Bạn cần phải cho phép Quyền truy cập mọi tệp trong quyền hạn ứng dụng để sử dụng tùy chọn này. Bạn cần phải bật quyền Chuông báo và lời nhắc trong cài đặt Android để sử dụng tính năng này. + + Hôm nay + Hôm qua OK Mở @@ -37,7 +40,7 @@ Vào ngày %1$s với tiết học %2$s Phòng sẽ học bù: %1$s - Cài đặt + Cài đặt Thông báo Cài đặt cập nhật tin tức trong nền Cấu hình cài đặt thông báo tin tức cho bạn, bao gồm thời gian cập nhật, tin tức nào sẽ được bật, cài đặt bộ lọc tin tức và hơn thế nữa. @@ -185,4 +188,6 @@ Lớp học phần Lịch sử tìm kiếm Tìm kiếm %2$s theo %1$s + + Tài khoản \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f0cb02d..1887d6d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,6 +4,9 @@ This function is in development. Check back soon. You need to grant All files access in application permissions to use this option. You need to enable Alarms & Reminders in Android app settings to use this feature. + + Today + Yesterday OK Open @@ -37,7 +40,7 @@ On %1$s at lesson(s) %2$s Room will make up: %1$s - Settings + Settings Notifications News schedule in background settings Configure your news notification settings, include duration, which news is enabled, news filter settings, and more. @@ -185,4 +188,6 @@ News Subject Search history Search %1$s in %2$s + + Account \ No newline at end of file diff --git a/build.gradle b/build.gradle index 35fb10a..8de17f9 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '8.3.2' apply false + id 'com.android.application' version '8.4.0' apply false id 'org.jetbrains.kotlin.android' version '1.8.10' apply false id 'com.google.dagger.hilt.android' version '2.44' apply false } From a588b013c274b471fa138d2890d4c2ad95b9596a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BA=BF=20T=C3=B9ng?= <47247560+ZoeMeow1027@users.noreply.github.com> Date: Sun, 5 May 2024 13:02:29 +0700 Subject: [PATCH 17/21] Update project - Update Vietnamese strings for Home view, account main view, settings view. --- .../io/zoemeow/dutschedule/GlobalVariables.kt | 5 ++ .../dutschedule/activity/AccountActivity.kt | 20 ++++-- .../dutschedule/activity/NewsActivity.kt | 13 +++- .../dutschedule/activity/SettingsActivity.kt | 15 +++-- .../service/NewsBackgroundUpdateService.kt | 5 +- .../ui/component/account/AccountInfoBanner.kt | 11 ++-- .../account/AccountSubjectMoreInformation.kt | 17 ++--- .../ui/component/account/LoginBox.kt | 37 ++++++----- .../ui/component/account/LogoutDialog.kt | 16 +++-- .../ui/component/main/NotificationItem.kt | 3 +- .../dutschedule/ui/view/account/MainView.kt | 62 +++++++++---------- .../ui/view/account/SubjectInformation.kt | 17 +++-- .../ui/view/account/TrainingResult.kt | 7 +-- .../ui/view/account/TrainingSubjectResult.kt | 18 +++--- .../ui/view/main/MainViewDashboard.kt | 31 +++++----- .../ui/view/main/MainViewTabbed.kt | 17 ++--- .../ui/view/main/NotificationScaffold.kt | 4 +- .../dutschedule/ui/view/news/MainView.kt | 16 ++--- .../dutschedule/ui/view/news/NewsDetail.kt | 6 +- .../dutschedule/ui/view/news/NewsSearch.kt | 10 +-- .../ui/view/settings/ExperimentSettings.kt | 3 + .../dutschedule/ui/view/settings/MainView.kt | 21 ++++--- .../view/settings/NewsNotificationSettings.kt | 48 ++++++++++---- .../settings/ParseNewsSubjectNotification.kt | 1 - .../dutschedule/utils/NotificationsUtil.kt | 6 +- app/src/main/res/values-vi/strings.xml | 55 ++++++++++++++++ app/src/main/res/values/strings.xml | 55 ++++++++++++++++ 27 files changed, 352 insertions(+), 167 deletions(-) diff --git a/app/src/main/java/io/zoemeow/dutschedule/GlobalVariables.kt b/app/src/main/java/io/zoemeow/dutschedule/GlobalVariables.kt index ee4a8bf..f45b766 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/GlobalVariables.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/GlobalVariables.kt @@ -3,5 +3,10 @@ package io.zoemeow.dutschedule class GlobalVariables { companion object { const val REQUEST_EXPIRED_DURATION = 1000 * 60 * 5 + + const val LINK_FORGOT_PASSWORD = "https://www.facebook.com/ctsvdhbkdhdn/posts/pfbid02G5sza1p8x7tEJ7S1Cac6a66EW3exgxLNmR9L26RZ8sX8xjhbEnguoeAXms31i7oxl" + const val LINK_REPOSITORY = "https://github.com/ZoeMeow1027/DutSchedule" + const val LINK_REPOSITORY_RELEASE = "${LINK_REPOSITORY}/releases" + const val LINK_CHANGELOG = "https://github.com/ZoeMeow1027/DutSchedule/blob/stable/CHANGELOG.md" } } \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/activity/AccountActivity.kt b/app/src/main/java/io/zoemeow/dutschedule/activity/AccountActivity.kt index 42d416c..787286a 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/activity/AccountActivity.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/activity/AccountActivity.kt @@ -14,6 +14,14 @@ import io.zoemeow.dutschedule.ui.view.account.TrainingSubjectResult @AndroidEntryPoint class AccountActivity: BaseActivity() { + companion object { + const val INTENT_SUBJECTINFORMATION = "subject_information" + const val INTENT_SUBJECTFEE = "subject_fee" + const val INTENT_ACCOUNTINFORMATION = "acc_info" + const val INTENT_ACCOUNTTRAININGSTATUS = "acc_training_result" + const val INTENT_ACCOUNTSUBJECTRESULT = "acc_training_result_subjectresult" + } + @Composable override fun OnPreloadOnce() { @@ -27,28 +35,29 @@ class AccountActivity: BaseActivity() { contentColor: Color ) { when (intent.action) { - "subject_information" -> { + INTENT_SUBJECTINFORMATION -> { SubjectInformation( + context = context, snackBarHostState = snackBarHostState, containerColor = containerColor, contentColor = contentColor ) } - "subject_fee" -> { + INTENT_SUBJECTFEE -> { SubjectFee( snackBarHostState = snackBarHostState, containerColor = containerColor, contentColor = contentColor ) } - "acc_info" -> { + INTENT_ACCOUNTINFORMATION -> { AccountInformation( snackBarHostState = snackBarHostState, containerColor = containerColor, contentColor = contentColor ) } - "acc_training_result" -> { + INTENT_ACCOUNTTRAININGSTATUS -> { TrainingResult( context = context, snackBarHostState = snackBarHostState, @@ -56,8 +65,9 @@ class AccountActivity: BaseActivity() { contentColor = contentColor ) } - "acc_training_result_subjectresult" -> { + INTENT_ACCOUNTSUBJECTRESULT -> { TrainingSubjectResult( + context = context, snackBarHostState = snackBarHostState, containerColor = containerColor, contentColor = contentColor diff --git a/app/src/main/java/io/zoemeow/dutschedule/activity/NewsActivity.kt b/app/src/main/java/io/zoemeow/dutschedule/activity/NewsActivity.kt index cff3921..5168f4d 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/activity/NewsActivity.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/activity/NewsActivity.kt @@ -12,6 +12,13 @@ import io.zoemeow.dutschedule.ui.view.news.NewsSearch @AndroidEntryPoint class NewsActivity : BaseActivity() { + companion object { + const val INTENT_SEARCHACTIVITY = "activity_search" + const val INTENT_NEWSDETAILACTIVITY = "activity_detail" + const val NEWSTYPE_NEWSGLOBAL = "news_global" + const val NEWSTYPE_NEWSSUBJECT = "news_subject" + } + @Composable override fun OnPreloadOnce() { @@ -25,7 +32,7 @@ class NewsActivity : BaseActivity() { contentColor: Color ) { when (intent.action) { - "activity_search" -> { + INTENT_SEARCHACTIVITY -> { NewsSearch( context = context, snackBarHostState = snackBarHostState, @@ -34,7 +41,7 @@ class NewsActivity : BaseActivity() { ) } - "activity_detail" -> { + INTENT_NEWSDETAILACTIVITY -> { NewsDetail( context = context, snackBarHostState = snackBarHostState, @@ -51,7 +58,7 @@ class NewsActivity : BaseActivity() { contentColor = contentColor, searchRequested = { val intent = Intent(context, NewsActivity::class.java) - intent.action = "activity_search" + intent.action = INTENT_SEARCHACTIVITY context.startActivity(intent) } ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/activity/SettingsActivity.kt b/app/src/main/java/io/zoemeow/dutschedule/activity/SettingsActivity.kt index 8475626..c150d79 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/activity/SettingsActivity.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/activity/SettingsActivity.kt @@ -18,6 +18,13 @@ import io.zoemeow.dutschedule.utils.BackgroundImageUtil @AndroidEntryPoint class SettingsActivity : BaseActivity() { + companion object { + const val INTENT_PARSENEWSSUBJECTNOTIFICATION = "settings_newssubjectnewparse" + const val INTENT_EXPERIMENTSETTINGS = "settings_experimentsettings" + const val INTENT_LANGUAGESETTINGS = "settings_languagesettings" + const val INTENT_NEWSNOTIFICATIONSETTINGS = "settings_newsnotificaitonsettings" + } + @Composable override fun OnPreloadOnce() { } @@ -57,7 +64,7 @@ class SettingsActivity : BaseActivity() { contentColor: Color ) { when (intent.action) { - "settings_newssubjectnewparse" -> { + INTENT_PARSENEWSSUBJECTNOTIFICATION -> { ParseNewsSubjectNotification( context = context, snackBarHostState = snackBarHostState, @@ -66,7 +73,7 @@ class SettingsActivity : BaseActivity() { ) } - "settings_experimentsettings" -> { + INTENT_EXPERIMENTSETTINGS -> { ExperimentSettings( context = context, snackBarHostState = snackBarHostState, @@ -75,7 +82,7 @@ class SettingsActivity : BaseActivity() { ) } - "settings_languagesettings" -> { + INTENT_LANGUAGESETTINGS -> { LanguageSettings( context = context, snackBarHostState = snackBarHostState, @@ -84,7 +91,7 @@ class SettingsActivity : BaseActivity() { ) } - "settings_newsnotificaitonsettings" -> { + INTENT_NEWSNOTIFICATIONSETTINGS -> { NewsNotificationSettings( context = context, snackBarHostState = snackBarHostState, diff --git a/app/src/main/java/io/zoemeow/dutschedule/service/NewsBackgroundUpdateService.kt b/app/src/main/java/io/zoemeow/dutschedule/service/NewsBackgroundUpdateService.kt index 05e68fe..2152bb0 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/service/NewsBackgroundUpdateService.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/service/NewsBackgroundUpdateService.kt @@ -12,6 +12,7 @@ import io.dutwrapper.dutwrapper.model.enums.NewsType import io.dutwrapper.dutwrapper.model.news.NewsGlobalItem import io.dutwrapper.dutwrapper.model.news.NewsSubjectItem import io.zoemeow.dutschedule.R +import io.zoemeow.dutschedule.activity.NewsActivity import io.zoemeow.dutschedule.activity.PermissionRequestActivity import io.zoemeow.dutschedule.model.NotificationHistory import io.zoemeow.dutschedule.model.ProcessState @@ -563,8 +564,8 @@ class NewsBackgroundUpdateService : BaseService( timestamp = newsDate, parameters = mapOf( "type" to when (type) { - NewsType.Global -> "news_global" - NewsType.Subject -> "news_subject" + NewsType.Global -> NewsActivity.NEWSTYPE_NEWSGLOBAL + NewsType.Subject -> NewsActivity.NEWSTYPE_NEWSSUBJECT else -> "" }, "data" to jsonData diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountInfoBanner.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountInfoBanner.kt index e6644f7..280fdc1 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountInfoBanner.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountInfoBanner.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.component.account +import android.content.Context import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -17,10 +18,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp +import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.ui.component.base.OutlinedTextBox @Composable fun AccountInfoBanner( + context: Context, padding: PaddingValues, isLoading: Boolean = false, username: String? = null, @@ -65,7 +68,7 @@ fun AccountInfoBanner( verticalAlignment = Alignment.Top, content = { Text( - text = "Basic account information", + text = context.getString(R.string.account_dashboard_banner_title), style = MaterialTheme.typography.titleLarge, modifier = Modifier.padding(bottom = 10.dp), ) @@ -73,17 +76,17 @@ fun AccountInfoBanner( ) OutlinedTextBox( title = "Username", - value = username ?: "(unknown)", + value = username ?: context.getString(R.string.data_unknown), modifier = Modifier.fillMaxWidth().padding(bottom = 5.dp) ) OutlinedTextBox( title = "Class", - value = schoolClass ?: "(unknown)", + value = schoolClass ?: context.getString(R.string.data_unknown), modifier = Modifier.fillMaxWidth().padding(bottom = 5.dp) ) OutlinedTextBox( title = "Training program plan", - value = trainingProgramPlan ?: "(unknown)", + value = trainingProgramPlan ?: context.getString(R.string.data_unknown), modifier = Modifier.fillMaxWidth().padding(bottom = 5.dp) ) } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountSubjectMoreInformation.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountSubjectMoreInformation.kt index e8405da..91f28bb 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountSubjectMoreInformation.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountSubjectMoreInformation.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.component.account +import android.content.Context import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope @@ -18,12 +19,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import io.dutwrapper.dutwrapper.model.accounts.SubjectScheduleItem +import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.model.settings.SubjectCode import io.zoemeow.dutschedule.ui.component.base.DialogBase import io.zoemeow.dutschedule.utils.CustomDateUtil @Composable fun AccountSubjectMoreInformation( + context: Context, item: SubjectScheduleItem? = null, isVisible: Boolean = false, onAddToFilterRequested: ((SubjectCode) -> Unit)? = null, @@ -33,7 +36,7 @@ fun AccountSubjectMoreInformation( modifier = Modifier .fillMaxWidth() .padding(25.dp), - title = "${item?.name ?: "(unknown)"}\n${item?.lecturer ?: "(unknown)"}", + title = "${item?.name ?: context.getString(R.string.data_unknown)}\n${item?.lecturer ?: context.getString(R.string.data_unknown)}", isVisible = isVisible, isTitleCentered = true, canDismiss = true, @@ -44,10 +47,10 @@ fun AccountSubjectMoreInformation( Column( horizontalAlignment = Alignment.Start, ) { - CustomText("ID: ${item?.id?.toString(false) ?: "(unknown)"}") - CustomText("Credit: ${item?.credit ?: "(unknown)"}") - CustomText("Is high quality: ${item?.isHighQuality ?: "(unknown)"}") - CustomText("Final score formula: ${item?.pointFormula ?: "(unknown)"}") + CustomText("${context.getString(R.string.account_subjectinfo_data_id)}: ${item?.id?.toString(false) ?: context.getString(R.string.data_unknown)}") + CustomText("${context.getString(R.string.account_subjectinfo_data_credit)}: ${item?.credit ?: context.getString(R.string.data_unknown)}") + CustomText("${context.getString(R.string.account_subjectinfo_data_ishighquality)}: ${item?.isHighQuality ?: context.getString(R.string.data_unknown)}") + CustomText("${context.getString(R.string.account_subjectinfo_data_scoreformula)}: ${item?.pointFormula ?: context.getString(R.string.data_unknown)}") // Subject study Spacer(modifier = Modifier.size(15.dp)) ContentInBoxWithBorder( @@ -118,12 +121,12 @@ fun AccountSubjectMoreInformation( } } }, - content = { Text("Add to news filter") }, + content = { Text(context.getString(R.string.account_subjectinfo_addtofilter)) }, modifier = Modifier.padding(start = 8.dp), ) TextButton( onClick = { dismissClicked?.let { it() } }, - content = { Text("OK") }, + content = { Text(context.getString(R.string.action_ok)) }, modifier = Modifier.padding(start = 8.dp), ) } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LoginBox.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LoginBox.kt index 68874f6..cad4983 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LoginBox.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LoginBox.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.component.account +import android.content.Context import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -31,6 +32,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType @@ -42,6 +44,7 @@ import io.zoemeow.dutschedule.R @Composable fun LoginBox( + context: Context, modifier: Modifier = Modifier, isVisible: Boolean = true, isProcessing: Boolean = false, @@ -79,12 +82,12 @@ fun LoginBox( verticalArrangement = Arrangement.Center ) { Text( - "Login", + context.getString(R.string.account_login_title), style = MaterialTheme.typography.headlineMedium, modifier = Modifier.padding(bottom = 5.dp) ) Text( - "Use your account in sv.dut.udn.vn to login", + context.getString(R.string.account_login_description), style = MaterialTheme.typography.bodyLarge ) Spacer(modifier = Modifier.size(8.dp)) @@ -95,7 +98,7 @@ fun LoginBox( enabled = isControlEnabled && !isProcessing, value = username.value, onValueChange = { username.value = it }, - label = { Text("Username") }, + label = { Text(context.getString(R.string.account_login_username)) }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Text, imeAction = ImeAction.Next @@ -112,17 +115,20 @@ fun LoginBox( enabled = isControlEnabled && !isProcessing, value = password.value, onValueChange = { password.value = it }, - label = { Text("Password") }, + label = { Text(context.getString(R.string.account_login_password)) }, suffix = { IconButton( onClick = { passwordShow.value = !passwordShow.value }, content = { Icon( - painter = painterResource(id = when (passwordShow.value) { - false -> R.drawable.ic_baseline_visibility_24 - true -> R.drawable.ic_baseline_visibility_off_24 - }), - contentDescription = "" + painter = when (passwordShow.value) { + true -> painterResource(R.drawable.ic_baseline_visibility_off_24) + false -> painterResource(id = R.drawable.ic_baseline_visibility_24) + }, + contentDescription = when (passwordShow.value) { + true -> context.getString(R.string.action_hide) + false -> context.getString(R.string.action_display) + } ) }, modifier = Modifier.size(16.dp) @@ -162,7 +168,7 @@ fun LoginBox( onCheckedChange = { if (isControlEnabled) rememberLogin.value = it }, ) Spacer(modifier = Modifier.size(5.dp)) - Text("Remember this login") + Text(context.getString(R.string.account_login_rememberpassword)) } ElevatedButton( enabled = when { @@ -175,20 +181,20 @@ fun LoginBox( onSubmit(username.value, password.value, rememberLogin.value) }, content = { - Text("Login") + Text(context.getString(R.string.account_login_actionlogin)) } ) if (isLoggedInBefore) { ElevatedButton( enabled = !isProcessing, onClick = { onClearLogin?.let { it() } }, - content = { Text("Login with another account") } + content = { Text(context.getString(R.string.account_login_actionloginclearprevious)) } ) } TextButton( enabled = isControlEnabled && !isProcessing, onClick = { onForgotPass?.let { it() } }, - content = { Text("Forgot your password?") } + content = { Text(context.getString(R.string.account_login_actionforgot)) } ) if (isProcessing || isLoggedInBefore) { Surface( @@ -206,9 +212,9 @@ fun LoginBox( if (isProcessing) { CircularProgressIndicator() Spacer(modifier = Modifier.size(10.dp)) - Text("Processing...") + Text(context.getString(R.string.account_login_processing)) } else if (isLoggedInBefore) { - Text("You have logged in before. Click \"Login\" button to proceed.") + Text(context.getString(R.string.account_login_loggedinbefore)) } } ) @@ -225,6 +231,7 @@ fun LoginBox( @Composable private fun LoginBoxPreview() { LoginBox( + context = LocalContext.current, isProcessing = false, isControlEnabled = true, isLoggedInBefore = false, diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LogoutDialog.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LogoutDialog.kt index bd1dd30..2c946a5 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LogoutDialog.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LogoutDialog.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.component.account +import android.content.Context import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -9,10 +10,12 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.ui.component.base.DialogBase @Composable fun LogoutDialog( + context: Context, isVisible: Boolean = false, canDismiss: Boolean = false, logoutClicked: (() -> Unit)? = null, @@ -21,7 +24,7 @@ fun LogoutDialog( DialogBase( modifier = Modifier.fillMaxWidth().padding(25.dp), isVisible = isVisible, - title = "Logout", + title = context.getString(R.string.account_logout_title), canDismiss = canDismiss, dismissClicked = { dismissClicked?.let { it() } @@ -30,23 +33,18 @@ fun LogoutDialog( Column( horizontalAlignment = Alignment.Start, ) { - Text( - "Are you sure you want to logout?\n\n" + - "Note that:\n" + - "- You won't be received your any subject schedule anymore.\n" + - "- Your news filter settings won't be affected." - ) + Text(context.getString(R.string.account_logout_description)) } }, actionButtons = { TextButton( onClick = { dismissClicked?.let { it() } }, - content = { Text("Cancel") }, + content = { Text(context.getString(R.string.action_cancel)) }, modifier = Modifier.padding(start = 8.dp), ) TextButton( onClick = { logoutClicked?.let { it() } }, - content = { Text("Logout") }, + content = { Text(context.getString(R.string.account_logout_action_logout)) }, ) } ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/NotificationItem.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/NotificationItem.kt index 724a2cf..9e7d2d0 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/NotificationItem.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/main/NotificationItem.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.model.NotificationHistory import io.zoemeow.dutschedule.utils.CustomDateUtil import io.zoemeow.dutschedule.utils.getRandomString @@ -100,7 +101,7 @@ fun NotificationItem( modifier = Modifier.size(36.dp), onClick = { onClear?.let { it() } } ) { - Icon(Icons.Default.Clear, "") + Icon(Icons.Default.Clear, context.getString(R.string.action_delete)) } } ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt index 700cb6e..f67d429 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt @@ -1,9 +1,9 @@ package io.zoemeow.dutschedule.ui.view.account -import android.app.Activity.RESULT_OK +import android.accounts.Account +import android.app.Activity.RESULT_CANCELED import android.content.Context import android.content.Intent -import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -31,6 +31,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import io.zoemeow.dutschedule.GlobalVariables import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.AccountActivity import io.zoemeow.dutschedule.model.ProcessState @@ -63,7 +64,7 @@ fun AccountActivity.MainView( showSnackBar(text = text, clearPrevious = clearPrevious, actionText = actionText, action = action) }, onBack = { - setResult(RESULT_OK) + setResult(RESULT_CANCELED) finish() } ) @@ -103,7 +104,7 @@ fun AccountMainView( content = { Icon( Icons.AutoMirrored.Filled.ArrowBack, - "", + context.getString(R.string.action_back), modifier = Modifier.size(25.dp) ) } @@ -115,6 +116,7 @@ fun AccountMainView( content = { mainViewModel.accountSession.accountSession.processState.value.let { state -> LoginBox( + context = context, modifier = Modifier .padding(it) .padding(horizontal = 15.dp), @@ -126,7 +128,7 @@ fun AccountMainView( opacity = componentBackgroundAlpha, onForgotPass = { context.openLink( - url = "https://www.facebook.com/ctsvdhbkdhdn/posts/pfbid02G5sza1p8x7tEJ7S1Cac6a66EW3exgxLNmR9L26RZ8sX8xjhbEnguoeAXms31i7oxl", + url = GlobalVariables.LINK_FORGOT_PASSWORD, customTab = mainViewModel.appSettings.value.openLinkInsideApp ) }, @@ -136,10 +138,8 @@ fun AccountMainView( CoroutineScope(Dispatchers.IO).launch { loginDialogEnabled.value = false onShowSnackBar?.let { it( - "Logging you in...", - true, - null, - null + context.getString(R.string.account_login_loggingin), + true, null, null ) } } mainViewModel.accountSession.login( @@ -155,19 +155,15 @@ fun AccountMainView( loginDialogVisible.value = false mainViewModel.accountSession.reLogin() onShowSnackBar?.let { it( - "Successfully logged in!", - true, - null, - null + context.getString(R.string.account_login_successful), + true, null, null ) } } false -> { loginDialogEnabled.value = true onShowSnackBar?.let { it( - "Login failed! Please check your login information and try again.", - true, - null, - null + context.getString(R.string.account_login_failed), + true, null, null ) } } } @@ -185,12 +181,13 @@ fun AccountMainView( content = { mainViewModel.accountSession.accountInformation.let { accInfo -> AccountInfoBanner( + context = context, opacity = componentBackgroundAlpha, padding = PaddingValues(10.dp), isLoading = accInfo.processState.value == ProcessState.Running, - username = accInfo.data.value?.studentId ?: "(unknown)", - schoolClass = accInfo.data.value?.schoolClass ?: "(unknown)", - trainingProgramPlan = accInfo.data.value?.trainingProgramPlan ?: "(unknown)" + username = accInfo.data.value?.studentId, + schoolClass = accInfo.data.value?.schoolClass, + trainingProgramPlan = accInfo.data.value?.trainingProgramPlan ) } ButtonBase( @@ -198,12 +195,12 @@ fun AccountMainView( .fillMaxWidth() .padding(horizontal = 10.dp, vertical = 5.dp), modifierInside = Modifier.padding(vertical = 7.dp), - content = { Text("Subject Information") }, + content = { Text(context.getString(R.string.account_dashboard_button_subjectinfo)) }, horizontalArrangement = Arrangement.Start, opacity = componentBackgroundAlpha, clicked = { val intent = Intent(context, AccountActivity::class.java) - intent.action = "subject_information" + intent.action = AccountActivity.INTENT_SUBJECTINFORMATION context.startActivity(intent) } ) @@ -212,12 +209,12 @@ fun AccountMainView( .fillMaxWidth() .padding(horizontal = 10.dp, vertical = 5.dp), modifierInside = Modifier.padding(vertical = 7.dp), - content = { Text("Subject Fee") }, + content = { Text(context.getString(R.string.account_dashboard_button_subjectfee)) }, horizontalArrangement = Arrangement.Start, opacity = componentBackgroundAlpha, clicked = { val intent = Intent(context, AccountActivity::class.java) - intent.action = "subject_fee" + intent.action = AccountActivity.INTENT_SUBJECTFEE context.startActivity(intent) } ) @@ -226,12 +223,12 @@ fun AccountMainView( .fillMaxWidth() .padding(horizontal = 10.dp, vertical = 5.dp), modifierInside = Modifier.padding(vertical = 7.dp), - content = { Text("Account Information") }, + content = { Text(context.getString(R.string.account_dashboard_button_accountinfo)) }, horizontalArrangement = Arrangement.Start, opacity = componentBackgroundAlpha, clicked = { val intent = Intent(context, AccountActivity::class.java) - intent.action = "acc_info" + intent.action = AccountActivity.INTENT_ACCOUNTINFORMATION context.startActivity(intent) } ) @@ -240,12 +237,12 @@ fun AccountMainView( .fillMaxWidth() .padding(horizontal = 10.dp, vertical = 5.dp), modifierInside = Modifier.padding(vertical = 7.dp), - content = { Text("Account Training Result") }, + content = { Text(context.getString(R.string.account_dashboard_button_accounttrainstats)) }, horizontalArrangement = Arrangement.Start, opacity = componentBackgroundAlpha, clicked = { val intent = Intent(context, AccountActivity::class.java) - intent.action = "acc_training_result" + intent.action = AccountActivity.INTENT_ACCOUNTTRAININGSTATUS context.startActivity(intent) } ) @@ -254,7 +251,7 @@ fun AccountMainView( .fillMaxWidth() .padding(horizontal = 10.dp, vertical = 5.dp), modifierInside = Modifier.padding(vertical = 7.dp), - content = { Text("Logout") }, + content = { Text(context.getString(R.string.account_dashboard_button_logout)) }, horizontalArrangement = Arrangement.Start, opacity = componentBackgroundAlpha, clicked = { @@ -268,6 +265,7 @@ fun AccountMainView( } ) LogoutDialog( + context = context, isVisible = logoutDialogVisible.value, canDismiss = true, logoutClicked = { @@ -276,10 +274,8 @@ fun AccountMainView( mainViewModel.accountSession.logout( onCompleted = { onShowSnackBar?.let { it( - "Successfully logout!", - true, - null, - null + context.getString(R.string.account_logout_loggedout), + true, null, null ) } } ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt index 0bb6e88..51bb403 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt @@ -1,6 +1,7 @@ package io.zoemeow.dutschedule.ui.view.account import android.app.Activity.RESULT_CANCELED +import android.content.Context import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -33,6 +34,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import io.dutwrapper.dutwrapper.model.accounts.SubjectScheduleItem +import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.AccountActivity import io.zoemeow.dutschedule.model.ProcessState import io.zoemeow.dutschedule.ui.component.account.AccountSubjectMoreInformation @@ -41,6 +43,7 @@ import io.zoemeow.dutschedule.ui.component.account.SubjectInformation @OptIn(ExperimentalMaterial3Api::class) @Composable fun AccountActivity.SubjectInformation( + context: Context, snackBarHostState: SnackbarHostState, containerColor: Color, contentColor: Color @@ -55,7 +58,7 @@ fun AccountActivity.SubjectInformation( contentColor = contentColor, topBar = { TopAppBar( - title = { Text("Subject Information") }, + title = { Text(context.getString(R.string.account_subjectinfo_title)) }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), navigationIcon = { IconButton( @@ -66,7 +69,7 @@ fun AccountActivity.SubjectInformation( content = { Icon( Icons.AutoMirrored.Filled.ArrowBack, - "Back to previous screen", + context.getString(R.string.action_back), modifier = Modifier.size(25.dp) ) } @@ -81,7 +84,7 @@ fun AccountActivity.SubjectInformation( getMainViewModel().accountSession.fetchSubjectSchedule(force = true) }, content = { - Icon(Icons.Default.Refresh, "Refresh") + Icon(Icons.Default.Refresh, context.getString(R.string.action_refresh)) } ) } @@ -132,6 +135,7 @@ fun AccountActivity.SubjectInformation( } ) AccountSubjectMoreInformation( + context = context, item = subjectScheduleItem.value, isVisible = subjectDetailVisible.value, dismissClicked = { @@ -140,7 +144,7 @@ fun AccountActivity.SubjectInformation( onAddToFilterRequested = { item -> if (getMainViewModel().appSettings.value.newsBackgroundFilterList.any { it.isEquals(item) }) { showSnackBar( - text = "This subject has already exist in your news filter list!", + text = context.getString(R.string.account_subjectinfo_filter_alreadyadded), clearPrevious = true ) } else { @@ -151,7 +155,10 @@ fun AccountActivity.SubjectInformation( ) getMainViewModel().saveSettings() showSnackBar( - text = "Successfully added $item to your news filter list!", + text = context.getString( + R.string.account_subjectinfo_filter_added, + item + ), clearPrevious = true ) } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt index 0fa93d8..210e32e 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt @@ -1,9 +1,8 @@ package io.zoemeow.dutschedule.ui.view.account -import android.app.Activity.RESULT_OK +import android.app.Activity.RESULT_CANCELED import android.content.Context import android.content.Intent -import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -62,7 +61,7 @@ fun AccountActivity.TrainingResult( navigationIcon = { IconButton( onClick = { - setResult(RESULT_OK) + setResult(RESULT_CANCELED) finish() }, content = { @@ -172,7 +171,7 @@ fun AccountActivity.TrainingResult( }, clicked = { val intent = Intent(context, AccountActivity::class.java) - intent.action = "acc_training_result_subjectresult" + intent.action = AccountActivity.INTENT_ACCOUNTSUBJECTRESULT context.startActivity(intent) } ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingSubjectResult.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingSubjectResult.kt index d5beea1..a66f7c3 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingSubjectResult.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingSubjectResult.kt @@ -1,7 +1,7 @@ package io.zoemeow.dutschedule.ui.view.account import android.app.Activity.RESULT_CANCELED -import androidx.activity.ComponentActivity +import android.content.Context import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -56,18 +56,18 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.dutwrapper.dutwrapper.model.accounts.trainingresult.SubjectResult +import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.AccountActivity import io.zoemeow.dutschedule.model.ProcessState import io.zoemeow.dutschedule.ui.component.base.OutlinedTextBox import io.zoemeow.dutschedule.utils.TableCell import io.zoemeow.dutschedule.utils.toNonAccent -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import java.util.Locale @OptIn(ExperimentalMaterial3Api::class) @Composable fun AccountActivity.TrainingSubjectResult( + context: Context, snackBarHostState: SnackbarHostState, containerColor: Color, contentColor: Color @@ -104,12 +104,14 @@ fun AccountActivity.TrainingSubjectResult( "QT" to item.pointQT?.toString(), "TH" to item.pointTH?.toString(), "Point (T10 - T4 - By point char)" to String.format( + Locale.ROOT, "%s - %s - %s", if (item.resultT10 != null) String.format( + Locale.ROOT, "%.2f", item.resultT10 ) else "unscored", - if (item.resultT4 != null) String.format("%.2f", item.resultT4) else "unscored", + if (item.resultT4 != null) String.format(Locale.ROOT, "%.2f", item.resultT4) else "unscored", if (item.resultByCharacter.isNullOrEmpty()) "(unscored)" else item.resultByCharacter ) ) @@ -163,7 +165,7 @@ fun AccountActivity.TrainingSubjectResult( content = { Icon( Icons.AutoMirrored.Filled.ArrowBack, - "", + context.getString(R.string.action_back), modifier = Modifier.size(25.dp) ) } @@ -176,7 +178,7 @@ fun AccountActivity.TrainingSubjectResult( searchEnabled.value = true }, content = { - Icon(Icons.Default.Search, "Search") + Icon(Icons.Default.Search, context.getString(R.string.action_search)) } ) } @@ -191,7 +193,7 @@ fun AccountActivity.TrainingSubjectResult( getMainViewModel().accountSession.fetchAccountTrainingStatus(force = true) }, content = { - Icon(Icons.Default.Refresh, "Refresh") + Icon(Icons.Default.Refresh, context.getString(R.string.action_refresh)) } ) } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt index 0e1c151..270a889 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp +import io.zoemeow.dutschedule.GlobalVariables import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.AccountActivity import io.zoemeow.dutschedule.activity.MainActivity @@ -122,7 +123,7 @@ fun MainActivity.MainViewDashboard( ) { Icon( painter = painterResource(id = R.drawable.ic_baseline_newspaper_24), - "News", + context.getString(R.string.news_title), modifier = Modifier.size(27.dp) ) } @@ -138,7 +139,7 @@ fun MainActivity.MainViewDashboard( ) { Icon( Icons.Default.Settings, - "Settings", + context.getString(R.string.settings_title), modifier = Modifier.size(27.dp) ) } @@ -178,7 +179,7 @@ fun MainActivity.MainViewDashboard( ) { Icon( imageVector = Icons.Default.Notifications, - "Notifications", + context.getString(R.string.notification_panel_title), modifier = Modifier.size(27.dp), ) } @@ -194,14 +195,14 @@ fun MainActivity.MainViewDashboard( verticalArrangement = Arrangement.Center, content = { Text( - "Account", + context.getString(R.string.account_title), style = MaterialTheme.typography.titleSmall ) getMainViewModel().accountSession.accountSession.processState.value.let { Text( when (it) { - ProcessState.NotRunYet -> "Not logged in" - ProcessState.Running -> "Fetching..." + ProcessState.NotRunYet -> context.getString(R.string.main_account_notloggedin) + ProcessState.Running -> context.getString(R.string.main_account_fetching) else -> getMainViewModel().accountSession.accountSession.data.value?.accountAuth?.username ?: "unknown" }, style = MaterialTheme.typography.bodySmall @@ -225,7 +226,7 @@ fun MainActivity.MainViewDashboard( ) else -> Icon( Icons.Outlined.AccountCircle, - "Account", + context.getString(R.string.account_title), modifier = Modifier.size(26.dp) ) } @@ -264,7 +265,7 @@ fun MainActivity.MainViewDashboard( onCompleted = { if (it) { val intent = Intent(context, AccountActivity::class.java) - intent.action = "subject_schedule" + intent.action = AccountActivity.INTENT_SUBJECTINFORMATION context.startActivity(intent) } } @@ -303,7 +304,7 @@ fun MainActivity.MainViewDashboard( latestVersionString = "", clicked = { openLink( - url = "https://github.com/ZoeMeow1027/DutSchedule/releases", + url = GlobalVariables.LINK_REPOSITORY_RELEASE, context = context, customTab = false, ) @@ -335,7 +336,7 @@ fun MainActivity.MainViewDashboard( onClick = { item -> if (listOf(1, 2).contains(item.tag)) { Intent(context, NewsActivity::class.java).also { - it.action = "activity_detail" + it.action = NewsActivity.INTENT_NEWSDETAILACTIVITY for (map1 in item.parameters) { it.putExtra(map1.key, map1.value) } @@ -348,8 +349,8 @@ fun MainActivity.MainViewDashboard( getMainViewModel().notificationHistory.remove(item) getMainViewModel().saveSettings() showSnackBar( - text = "Deleted notifications!", - actionText = "Undo", + text = context.getString(R.string.notification_removed), + actionText = context.getString(R.string.action_undo), action = { getMainViewModel().notificationHistory.add(item1) getMainViewModel().saveSettings() @@ -358,13 +359,13 @@ fun MainActivity.MainViewDashboard( }, onClearAll = { showSnackBar( - text = "This action is undone! To confirm, click \"Confirm\" to clear all.", - actionText = "Confirm", + text = context.getString(R.string.notification_removeall_confirm), + actionText = context.getString(R.string.action_confirm), action = { getMainViewModel().notificationHistory.clear() getMainViewModel().saveSettings() showSnackBar( - text = "Successfully cleared all notifications!", + text = context.getString(R.string.notification_removeall_removed), clearPrevious = true ) }, diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewTabbed.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewTabbed.kt index 4924e6f..1289885 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewTabbed.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewTabbed.kt @@ -30,6 +30,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.MainActivity import io.zoemeow.dutschedule.activity.NewsActivity import io.zoemeow.dutschedule.model.NavBarItem @@ -111,7 +112,7 @@ fun MainActivity.MainViewTabbed( mainViewModel = getMainViewModel(), searchRequested = { val intent = Intent(context, NewsActivity::class.java) - intent.action = "activity_search" + intent.action = NewsActivity.INTENT_SEARCHACTIVITY context.startActivity(intent) } ) @@ -141,7 +142,7 @@ fun MainActivity.MainViewTabbed( onClick = { item -> if (listOf(1, 2).contains(item.tag)) { Intent(context, NewsActivity::class.java).also { intent -> - intent.action = "activity_detail" + intent.action = NewsActivity.INTENT_NEWSDETAILACTIVITY for (map1 in item.parameters) { intent.putExtra(map1.key, map1.value) } @@ -154,8 +155,8 @@ fun MainActivity.MainViewTabbed( getMainViewModel().notificationHistory.remove(item) getMainViewModel().saveSettings() showSnackBar( - text = "Deleted notifications!", - actionText = "Undo", + text = context.getString(R.string.notification_removed), + actionText = context.getString(R.string.action_undo), action = { getMainViewModel().notificationHistory.add(itemTemp) getMainViewModel().saveSettings() @@ -164,13 +165,13 @@ fun MainActivity.MainViewTabbed( }, onClearAll = { showSnackBar( - text = "This action is undone! To confirm, click \"Confirm\" to clear all.", - actionText = "Confirm", + text = context.getString(R.string.notification_removeall_confirm), + actionText = context.getString(R.string.action_confirm), action = { - getMainViewModel().notificationHistory.clear() getMainViewModel().saveSettings() + getMainViewModel().notificationHistory.clear() showSnackBar( - text = "Successfully cleared all notifications!", + text = context.getString(R.string.notification_removeall_removed), clearPrevious = true ) }, diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/NotificationScaffold.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/NotificationScaffold.kt index 703bf67..e61964d 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/NotificationScaffold.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/NotificationScaffold.kt @@ -89,7 +89,7 @@ fun NotificationScaffold( IconButton( onClick = { onClearAll?.let { it() } }, content = { - Icon(ImageVector.vectorResource(id = R.drawable.ic_baseline_clear_all_24), "") + Icon(ImageVector.vectorResource(id = R.drawable.ic_baseline_clear_all_24), context.getString(R.string.action_clear)) } ) Spacer(modifier = Modifier.size(3.dp)) @@ -98,7 +98,7 @@ fun NotificationScaffold( IconButton( onClick = { onDismiss() }, content = { - Icon(Icons.Default.Clear, "Close") + Icon(Icons.Default.Clear, context.getString(R.string.action_close)) } ) } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt index 4ef40a8..eb4fdfd 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt @@ -1,9 +1,9 @@ package io.zoemeow.dutschedule.ui.view.news +import android.app.Activity.RESULT_CANCELED import android.app.Activity.RESULT_OK import android.content.Context import android.content.Intent -import androidx.activity.ComponentActivity import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row @@ -71,7 +71,7 @@ fun NewsActivity.MainView( componentBackgroundAlpha = getControlBackgroundAlpha(), mainViewModel = getMainViewModel(), onBack = { - setResult(RESULT_OK) + setResult(RESULT_CANCELED) finish() } ) @@ -123,7 +123,7 @@ fun NewsMainView( searchRequested?.let { it() } }, content = { - Icon(Icons.Default.Search, "Search") + Icon(Icons.Default.Search, context.getString(R.string.action_search)) } ) } @@ -209,7 +209,7 @@ fun NewsMainView( } else if (pagerState.currentPage == 1 && mainViewModel.newsInstance.newsSubject.processState.value == ProcessState.Running) { CircularProgressIndicator(modifier = Modifier.size(24.dp)) } else { - Icon(Icons.Default.Refresh, "Refresh") + Icon(Icons.Default.Refresh, context.getString(R.string.action_refresh)) } } ) @@ -233,8 +233,8 @@ fun NewsMainView( context, NewsActivity::class.java ).also { - it.action = "activity_detail" - it.putExtra("type", "news_global") + it.action = NewsActivity.INTENT_NEWSDETAILACTIVITY + it.putExtra("type", NewsActivity.NEWSTYPE_NEWSGLOBAL) it.putExtra("data", Gson().toJson(newsItem)) }) }, @@ -263,8 +263,8 @@ fun NewsMainView( context, NewsActivity::class.java ).also { - it.action = "activity_detail" - it.putExtra("type", "news_subject") + it.action = NewsActivity.INTENT_NEWSDETAILACTIVITY + it.putExtra("type", NewsActivity.NEWSTYPE_NEWSSUBJECT) it.putExtra("data", Gson().toJson(newsItem)) }) }, diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsDetail.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsDetail.kt index 982f6ef..de146da 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsDetail.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsDetail.kt @@ -71,7 +71,7 @@ fun NewsActivity.NewsDetail( ) }, floatingActionButton = { - if (newsType == "news_subject") { + if (newsType == NewsActivity.NEWSTYPE_NEWSSUBJECT) { ExtendedFloatingActionButton( content = { Row { @@ -103,7 +103,7 @@ fun NewsActivity.NewsDetail( }, content = { when (newsType) { - "news_global" -> { + NewsActivity.NEWSTYPE_NEWSGLOBAL -> { NewsDetailScreen( context = context, padding = it, @@ -118,7 +118,7 @@ fun NewsActivity.NewsDetail( } ) } - "news_subject" -> { + NewsActivity.NEWSTYPE_NEWSSUBJECT -> { NewsDetailScreen( context = context, padding = it, diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsSearch.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsSearch.kt index 2f85a6d..3ace9ef 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsSearch.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/NewsSearch.kt @@ -1,5 +1,6 @@ package io.zoemeow.dutschedule.ui.view.news +import android.app.Activity.RESULT_CANCELED import android.app.Activity.RESULT_OK import android.content.Context import android.content.Intent @@ -42,6 +43,7 @@ import com.google.gson.Gson import io.dutwrapper.dutwrapper.model.enums.NewsType import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.NewsActivity +import io.zoemeow.dutschedule.activity.SettingsActivity import io.zoemeow.dutschedule.model.ProcessState import io.zoemeow.dutschedule.ui.component.news.NewsSearchOptionAndHistory import io.zoemeow.dutschedule.ui.component.news.NewsSearchResult @@ -125,7 +127,7 @@ fun NewsActivity.NewsSearch( if (isSearchFocused.targetState) { dismissFocus() } else { - setResult(RESULT_OK) + setResult(RESULT_CANCELED) finish() } }, @@ -147,7 +149,7 @@ fun NewsActivity.NewsSearch( }, enabled = newsSearchViewModel.progress.value != ProcessState.Running, content = { - Icon(Icons.Default.Search, "Search/Refresh search") + Icon(Icons.Default.Search, context.getString(R.string.action_search)) } ) } @@ -174,8 +176,8 @@ fun NewsActivity.NewsSearch( context, NewsActivity::class.java ).also { - it.action = "activity_detail" - it.putExtra("type", if (newsSearchViewModel.type.value == NewsType.Subject) "news_subject" else "news_global") + it.action = NewsActivity.INTENT_NEWSDETAILACTIVITY + it.putExtra("type", if (newsSearchViewModel.type.value == NewsType.Subject) NewsActivity.NEWSTYPE_NEWSSUBJECT else NewsActivity.NEWSTYPE_NEWSGLOBAL) it.putExtra("data", Gson().toJson(item)) }) } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt index ea0a712..81de122 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ExperimentSettings.kt @@ -39,6 +39,7 @@ import io.zoemeow.dutschedule.ui.component.base.OptionItem import io.zoemeow.dutschedule.ui.component.base.OptionSwitchItem import io.zoemeow.dutschedule.ui.component.settings.ContentRegion import io.zoemeow.dutschedule.ui.component.settings.dialog.DialogSchoolYearSettings +import java.util.Locale @OptIn(ExperimentalMaterial3Api::class) @@ -118,6 +119,7 @@ fun SettingsActivity.ExperimentSettings( modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), title = context.getString(R.string.settings_experiment_option_bgopacity), description = String.format( + Locale.ROOT, "%2.0f%% %s", (getMainViewModel().appSettings.value.backgroundImageOpacity * 100), if (getMainViewModel().appSettings.value.backgroundImage == BackgroundImageOption.None) { @@ -133,6 +135,7 @@ fun SettingsActivity.ExperimentSettings( modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), title = context.getString(R.string.settings_experiment_option_componentopacity), description = String.format( + Locale.ROOT, "%2.0f%% %s", (getMainViewModel().appSettings.value.componentOpacity * 100), if (getMainViewModel().appSettings.value.backgroundImage == BackgroundImageOption.None) { diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt index b00979c..1b34ad3 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/MainView.kt @@ -1,6 +1,6 @@ package io.zoemeow.dutschedule.ui.view.settings -import android.app.Activity.RESULT_OK +import android.app.Activity.RESULT_CANCELED import android.content.Context import android.content.Intent import android.net.Uri @@ -36,6 +36,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import io.zoemeow.dutschedule.BuildConfig +import io.zoemeow.dutschedule.GlobalVariables import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.PermissionRequestActivity import io.zoemeow.dutschedule.activity.SettingsActivity @@ -71,7 +72,7 @@ fun SettingsActivity.MainView( showSnackBar(text = text, clearPrevious = clearPrevious, actionText = actionText, action = action) }, onBack = { - setResult(RESULT_OK) + setResult(RESULT_CANCELED) finish() } ) @@ -145,7 +146,7 @@ fun SettingsMainView( description = context.getString(R.string.settings_option_newsschedule_description), onClick = { Intent(context, SettingsActivity::class.java).apply { - action = "settings_newsnotificaitonsettings" + action = SettingsActivity.INTENT_NEWSNOTIFICATIONSETTINGS }.also { intent -> context.startActivity(intent) } } ) @@ -261,7 +262,7 @@ fun SettingsMainView( context.startActivity(intent) } else { val intent = Intent(context, SettingsActivity::class.java) - intent.action = "settings_languagesettings" + intent.action = SettingsActivity.INTENT_LANGUAGESETTINGS context.startActivity(intent) } } @@ -319,7 +320,7 @@ fun SettingsMainView( description = context.getString(R.string.settings_option_experiemntsettings_description), onClick = { val intent = Intent(context, SettingsActivity::class.java) - intent.action = "settings_experimentsettings" + intent.action = SettingsActivity.INTENT_EXPERIMENTSETTINGS context.startActivity(intent) } ) @@ -365,7 +366,7 @@ fun SettingsMainView( description = context.getString(R.string.settings_option_changelog_description), onClick = { context.openLink( - url = "https://github.com/ZoeMeow1027/DutSchedule/blob/stable/CHANGELOG.md", + url = GlobalVariables.LINK_CHANGELOG, customTab = mainViewModel.appSettings.value.openLinkInsideApp, ) } @@ -375,15 +376,15 @@ fun SettingsMainView( leadingIcon = { Icon( imageVector = ImageVector.vectorResource(R.drawable.github_mark_24), - "", + "repository", modifier = Modifier.padding(end = 15.dp) ) }, title = context.getString(R.string.settings_option_github), - description = "https://github.com/ZoeMeow1027/DutSchedule", + description = GlobalVariables.LINK_REPOSITORY, onClick = { context.openLink( - url = "https://github.com/ZoeMeow1027/DutSchedule", + url = GlobalVariables.LINK_REPOSITORY, customTab = mainViewModel.appSettings.value.openLinkInsideApp, ) } @@ -447,7 +448,7 @@ fun SettingsMainView( BackgroundImageOption.PickFileFromMedia -> { // Launch the photo picker and let the user choose only images. - mediaRequest.let { it() } + mediaRequest() } } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt index 6be5bd0..990ce40 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt @@ -3,7 +3,6 @@ package io.zoemeow.dutschedule.ui.view.settings import android.app.Activity.RESULT_OK import android.content.Context import android.content.Intent -import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -156,9 +155,9 @@ fun SettingsActivity.NewsNotificationSettings( } }, isNewSubjectNotificationParseEnabled = getMainViewModel().appSettings.value.newsBackgroundParseNewsSubject, - onNewSubjectNotificationParseStateChanged = { + onNewSubjectNotificationParseClick = { Intent(context, SettingsActivity::class.java).apply { - action = "settings_newssubjectnewparse" + action = SettingsActivity.INTENT_PARSENEWSSUBJECTNOTIFICATION }.also { intent -> context.startActivity(intent) } }, isNewsGlobalEnabled = getMainViewModel().appSettings.value.newsBackgroundGlobalEnabled, @@ -308,7 +307,7 @@ private fun MainView( fetchNewsInBackgroundDuration: Int = 0, onFetchNewsStateChanged: ((Int) -> Unit)? = null, isNewSubjectNotificationParseEnabled: Boolean = false, - onNewSubjectNotificationParseStateChanged: (() -> Unit)? = null, + onNewSubjectNotificationParseClick: (() -> Unit)? = null, isNewsGlobalEnabled: Boolean = false, onNewsGlobalStateChanged: ((Boolean) -> Unit)? = null, isNewsSubjectEnabled: Int = -1, @@ -361,12 +360,17 @@ private fun MainView( horizontalAlignment = Alignment.CenterHorizontally ) { Text( - "Current duration settings: ${ when (fetchNewsInBackgroundDuration) { - 0 -> "Disabled" - 1 -> "1 minute" - else -> "$fetchNewsInBackgroundDuration minutes" - } - }", + context.getString( + R.string.settings_newsnotify_fetchnewsinbackground_value, + when (fetchNewsInBackgroundDuration) { + 0 -> context.getString(R.string.settings_newsnotify_fetchnewsinbackground_value_disabled) + 1 -> context.getString(R.string.settings_newsnotify_fetchnewsinbackground_value_enabled1) + else -> context.getString( + R.string.settings_newsnotify_fetchnewsinbackground_value_enabled2, + fetchNewsInBackgroundDuration + ) + } + ), modifier = Modifier.padding(bottom = 10.dp) ) Slider( @@ -388,7 +392,16 @@ private fun MainView( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, content = { - Text("${durationTemp.intValue} minute${if (durationTemp.intValue != 1) "s" else ""}") + Text( + when (durationTemp.intValue) { + 0 -> context.getString(R.string.settings_newsnotify_fetchnewsinbackground_modifiedvalue_disabled) + 1 -> context.getString(R.string.settings_newsnotify_fetchnewsinbackground_modifiedvalue_enabled1) + else -> context.getString( + R.string.settings_newsnotify_fetchnewsinbackground_modifiedvalue_enabled2, + fetchNewsInBackgroundDuration + ) + } + ) } ) FlowRow( @@ -415,7 +428,16 @@ private fun MainView( durationTemp.intValue = min } }, - label = { Text(if (min == 0) "Turn off" else "$min min") } + label = { + Text(when (min) { + 0 -> context.getString(R.string.settings_newsnotify_fetchnewsinbackground_option_turnoff) + 1 -> context.getString(R.string.settings_newsnotify_fetchnewsinbackground_option_value1) + else -> context.getString( + R.string.settings_newsnotify_fetchnewsinbackground_option_value2, + min + ) + }) + } ) } } @@ -441,7 +463,7 @@ private fun MainView( true -> context.getString(R.string.settings_newsnotify_parsenewssubject_enabled) false -> context.getString(R.string.settings_newsnotify_parsenewssubject_disabled) }, - onClick = { onNewSubjectNotificationParseStateChanged?.let { it() } } + onClick = { onNewSubjectNotificationParseClick?.let { it() } } ) } DividerItem(padding = PaddingValues(top = 5.dp, bottom = 15.dp)) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ParseNewsSubjectNotification.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ParseNewsSubjectNotification.kt index 32253bd..774f839 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ParseNewsSubjectNotification.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/ParseNewsSubjectNotification.kt @@ -2,7 +2,6 @@ package io.zoemeow.dutschedule.ui.view.settings import android.app.Activity.RESULT_OK import android.content.Context -import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize diff --git a/app/src/main/java/io/zoemeow/dutschedule/utils/NotificationsUtil.kt b/app/src/main/java/io/zoemeow/dutschedule/utils/NotificationsUtil.kt index 6eda7c5..46dc310 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/utils/NotificationsUtil.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/utils/NotificationsUtil.kt @@ -95,10 +95,10 @@ class NotificationsUtil { jsonData: String ) { val notificationIntent = Intent(context, NewsActivity::class.java).also { - it.action = "activity_detail" + it.action = NewsActivity.INTENT_NEWSDETAILACTIVITY it.putExtra("type", when (channelId) { - "notification.id.news.global" -> "news_global" - "notification.id.news.subject" -> "news_subject" + "notification.id.news.global" -> NewsActivity.NEWSTYPE_NEWSGLOBAL + "notification.id.news.subject" -> NewsActivity.NEWSTYPE_NEWSSUBJECT else -> "" }) it.putExtra("data", jsonData) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 0dcbbc1..1e33db0 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -18,9 +18,17 @@ Cho phép Xác nhận Làm mới + Tìm kiếm + Hoàn tác + Đóng + Hiển thị + Ẩn Đã chọn Thông tin + + Chưa đăng nhập + Đang tải… Thông báo ứng dụng Kênh này sẽ gửi thông báo cập nhật quan trọng của ứng dụng cho bạn. @@ -129,6 +137,16 @@ Đã vô hiệu hóa (thông báo như tin tức chung) Cài đặt thông báo Tần suất làm mới tin tức + Giá trị tần suất làm mới hiện tại: %1$s + Đã vô hiệu hóa + 1 phút + %1$d phút + Đã vô hiệu hóa + 1 phút + %1$d phút + Vô hiệu hóa + 1 phút + %1$d phút Thông báo tin tức chung Bật thông báo tin tức chung Đã kích hoạt thông báo tin tức chung! @@ -166,6 +184,9 @@ Thông báo Không có thông báo + Đã xóa thông báo! + Hành động này không thể hoàn tác! Để tiếp tục xóa tất cả, nhấn "Xác nhận". + Đã xóa tất cả thông báo! Tin tức Chung @@ -190,4 +211,38 @@ Tìm kiếm %2$s theo %1$s Tài khoản + Đăng nhập + Sử dụng tài khoản của bạn trên sv.dut.udn.vn để đăng nhập + Tên đăng nhập (ID sinh viên) + Mật khẩu + Duy trì đăng nhập này + Đăng nhập + Xóa đăng nhập cũ + Bạn đã quên mật khẩu? + Đang trong tiến trình… + Bạn đã đăng nhập trước đây. Nhấn vào nút \"Đăng nhập\" để thử lại. Nếu bạn đã thay đổi mật khẩu trước đây, nhấp vào \"Xóa đăng nhập cũ\". + Đang đăng nhâp cho bạn… + Đã đăng nhập thành công! + Chúng tôi không thể đăng nhập với tài khoản của bạn! Nếu bạn đã nhập đúng và sự cố này vẫn tồn tại, hãy mở một issue mới trên GitHub. + Thông tin cơ bản về tài khoản + Thông tin học phần + Học phí + Thông tin tài khoản + Kết quả rèn luyện + Đăng xuất + Đăng xuất + Bạn có chắc chắn muốn đăng xuất?\n\Lưu ý rằng:\n- Bạn sẽ không thể nhận bất kì thông báo liên quan đến lịch học của bạn.\n- Bộ lọc tin tức của bạn sẽ không bị ảnh hưởng. + Đăng xuất + Đã đăng xuất! + + Thông tin học phần + Đã thêm %1$s vào danh sách bộ lọc của bạn! + Học phần này đã có trong danh sách bộ lọc của bạn! + Thêm vào bộ lọc tin tức + Mã học phần + Số tín chỉ + Chất lượng cao + Công thức điểm + + (không rõ) \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1887d6d..63e0cc4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -18,9 +18,17 @@ Grant Confirm Refresh + Search + Undo + Close + Display + Hide Selected Information + + Not logged in + Fetching… App updates This will send you app recommend and important updates. @@ -129,6 +137,16 @@ Disabled (notify like news global) Notification settings Fetch news duration + Current duration value: %1$s + Disabled + 1 minute + %1$d minute + Disabled + 1 minute + %1$d minute + Turn off + 1 minute + %1$d minutes Global news notification Enable global news notification Successfully enabled global news notifications! @@ -166,6 +184,9 @@ Notifications No notifications + Notification removed! + This action is undone! To confirm clear all, click "Confirm". + Successfully cleared all notifications! News Global @@ -190,4 +211,38 @@ Search %1$s in %2$s Account + Login + Use your account in sv.dut.udn.vn to login + Username (Student ID) + Password + Remember this login + Login + Clear previous login + Forgot your password? + Processing… + You have logged in before. Click \"Login\" button to try again. If you have changed your password before, click \"Clear previous login\". + Logging you in… + Successfully logged in! + We can\'t log in with your account! If you entered carefully and this issue is still persists, feel free to open issue on GitHub. + Basic account information + Subject Information + Subject Fee + Account Information + Account Training Result + Logout + Logout + Are you sure you want to logout?\n\nNote that:\n- You won\'t be received your any subject schedule anymore.\n- Your news filter settings won\'t be affected. + Logout + Successfully logout! + + Subject Information + Successfully added %1$s to your news filter list! + This subject has already exist in your news filter list! + Add to news filter + ID + Credit(s) + High Quality + Final score formula + + (unknown) \ No newline at end of file From 8c1238a71e272756aad3947d800fcef7eba2a885 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BA=BF=20T=C3=B9ng?= <47247560+ZoeMeow1027@users.noreply.github.com> Date: Thu, 23 May 2024 00:14:08 +0700 Subject: [PATCH 18/21] Fixed #24 and adjust progress bar location --- .gitignore | 1 + .idea/deploymentTargetSelector.xml | 10 -- .../model/account/DUTAccountInstance.kt | 5 + .../ui/component/news/NewsListPage.kt | 3 - .../dialog/DialogSchoolYearSettings.kt | 3 + .../ui/view/account/AccountInformation.kt | 40 +++--- .../dutschedule/ui/view/account/SubjectFee.kt | 51 ++++---- .../ui/view/account/SubjectInformation.kt | 51 ++++---- .../ui/view/account/TrainingResult.kt | 40 +++--- .../ui/view/account/TrainingSubjectResult.kt | 116 +++++++++--------- .../dutschedule/ui/view/news/MainView.kt | 59 +++++---- .../dutschedule/viewmodel/MainViewModel.kt | 6 + 12 files changed, 216 insertions(+), 169 deletions(-) delete mode 100644 .idea/deploymentTargetSelector.xml diff --git a/.gitignore b/.gitignore index 38d628e..4996023 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ local.properties *.log .idea/deploymentTargetDropDown.xml +.idea/deploymentTargetSelector.xml diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml deleted file mode 100644 index b268ef3..0000000 --- a/.idea/deploymentTargetSelector.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/account/DUTAccountInstance.kt b/app/src/main/java/io/zoemeow/dutschedule/model/account/DUTAccountInstance.kt index fe4dbbd..32f84bc 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/account/DUTAccountInstance.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/account/DUTAccountInstance.kt @@ -3,6 +3,7 @@ package io.zoemeow.dutschedule.model.account import android.util.Log import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf +import com.google.gson.Gson import io.dutwrapper.dutwrapper.model.accounts.AccountInformation import io.dutwrapper.dutwrapper.model.accounts.SubjectFeeItem import io.dutwrapper.dutwrapper.model.accounts.SubjectScheduleItem @@ -295,6 +296,7 @@ class DUTAccountInstance( } }, onCompleted = { + it?.printStackTrace() subjectSchedule.processState.value = when { (it != null) -> ProcessState.Failed else -> ProcessState.Successful @@ -338,6 +340,7 @@ class DUTAccountInstance( } }, onCompleted = { + it?.printStackTrace() subjectFee.processState.value = when { (it != null) -> ProcessState.Failed else -> ProcessState.Successful @@ -374,6 +377,7 @@ class DUTAccountInstance( } }, onCompleted = { + it?.printStackTrace() accountInformation.processState.value = when { (it != null) -> ProcessState.Failed else -> ProcessState.Successful @@ -410,6 +414,7 @@ class DUTAccountInstance( } }, onCompleted = { + it?.printStackTrace() accountTrainingStatus.processState.value = when { (it != null) -> ProcessState.Failed else -> ProcessState.Successful diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/news/NewsListPage.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/news/NewsListPage.kt index faec90a..9502dd6 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/news/NewsListPage.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/news/NewsListPage.kt @@ -36,9 +36,6 @@ fun NewsListPage( Column( modifier = Modifier.fillMaxSize(), content = { - if (processState == ProcessState.Running) { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) - } LazyColumn( modifier = Modifier .fillMaxSize() diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogSchoolYearSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogSchoolYearSettings.kt index 3d0d440..9b79557 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogSchoolYearSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogSchoolYearSettings.kt @@ -23,6 +23,7 @@ import io.zoemeow.dutschedule.activity.SettingsActivity import io.zoemeow.dutschedule.model.account.SchoolYearItem import io.zoemeow.dutschedule.ui.component.base.DialogBase import io.zoemeow.dutschedule.ui.component.base.OutlinedTextBox +import java.util.Locale @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -104,6 +105,7 @@ fun DialogSchoolYearSettings( .menuAnchor(), title = context.getString(R.string.settings_dialog_schyear_choice_semester), value = String.format( + Locale.ROOT, "%s %d%s", context.getString(R.string.settings_dialog_schyear_choice_semester), if (currentSettings.value.semester <= 2) currentSettings.value.semester else 2, @@ -117,6 +119,7 @@ fun DialogSchoolYearSettings( 1.rangeTo(3).forEach { DropdownMenuItem( text = { Text(String.format( + Locale.ROOT, "%s %d%s", context.getString(R.string.settings_dialog_schyear_choice_semester), if (it <= 2) it else 2, diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/AccountInformation.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/AccountInformation.kt index f906731..ea20679 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/AccountInformation.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/AccountInformation.kt @@ -3,6 +3,7 @@ package io.zoemeow.dutschedule.ui.view.account import android.app.Activity.RESULT_OK import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -47,23 +48,31 @@ fun AccountActivity.AccountInformation( containerColor = containerColor, contentColor = contentColor, topBar = { - TopAppBar( - title = { Text("Basic Information") }, - colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), - navigationIcon = { - IconButton( - onClick = { - setResult(RESULT_OK) - finish() - }, - content = { - Icon( - Icons.AutoMirrored.Filled.ArrowBack, - "", - modifier = Modifier.size(25.dp) + Box( + contentAlignment = Alignment.BottomCenter, + content = { + TopAppBar( + title = { Text("Basic Information") }, + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), + navigationIcon = { + IconButton( + onClick = { + setResult(RESULT_OK) + finish() + }, + content = { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + "", + modifier = Modifier.size(25.dp) + ) + } ) } ) + if (getMainViewModel().accountSession.accountInformation.processState.value == ProcessState.Running) { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } } ) }, @@ -85,9 +94,6 @@ fun AccountActivity.AccountInformation( .fillMaxSize() .padding(padding), content = { - if (getMainViewModel().accountSession.accountInformation.processState.value == ProcessState.Running) { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) - } Column( modifier = Modifier .fillMaxSize() diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectFee.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectFee.kt index 454e33f..7dab93b 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectFee.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectFee.kt @@ -3,11 +3,14 @@ package io.zoemeow.dutschedule.ui.view.account import android.app.Activity.RESULT_OK import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -48,23 +51,31 @@ fun AccountActivity.SubjectFee( containerColor = containerColor, contentColor = contentColor, topBar = { - TopAppBar( - title = { Text("Subject fee") }, - colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), - navigationIcon = { - IconButton( - onClick = { - setResult(RESULT_OK) - finish() - }, - content = { - Icon( - Icons.AutoMirrored.Filled.ArrowBack, - "", - modifier = Modifier.size(25.dp) + Box( + contentAlignment = Alignment.BottomCenter, + content = { + TopAppBar( + title = { Text("Subject fee") }, + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), + navigationIcon = { + IconButton( + onClick = { + setResult(RESULT_OK) + finish() + }, + content = { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + "", + modifier = Modifier.size(25.dp) + ) + } ) } ) + if (getMainViewModel().accountSession.subjectFee.processState.value == ProcessState.Running) { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } } ) }, @@ -86,12 +97,9 @@ fun AccountActivity.SubjectFee( .fillMaxSize() .padding(padding), content = { - if (getMainViewModel().accountSession.subjectFee.processState.value == ProcessState.Running) { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) - } Column( modifier = Modifier - .fillMaxSize() + .fillMaxWidth() .padding(horizontal = 15.dp) .padding(vertical = 3.dp), horizontalAlignment = Alignment.CenterHorizontally, @@ -99,16 +107,15 @@ fun AccountActivity.SubjectFee( Text(getMainViewModel().appSettings.value.currentSchoolYear.toString()) } ) - Column( + LazyColumn( modifier = Modifier .fillMaxSize() .padding(horizontal = 15.dp) - .padding(bottom = 7.dp) - .verticalScroll(rememberScrollState()), + .padding(bottom = 7.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, content = { - getMainViewModel().accountSession.subjectFee.data.forEach { item -> + items(getMainViewModel().accountSession.subjectFee.data) { item -> AccountSubjectFeeInformation( modifier = Modifier.padding(bottom = 10.dp), item = item, diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt index 51bb403..47d3e19 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt @@ -4,11 +4,14 @@ import android.app.Activity.RESULT_CANCELED import android.content.Context import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -57,23 +60,31 @@ fun AccountActivity.SubjectInformation( containerColor = containerColor, contentColor = contentColor, topBar = { - TopAppBar( - title = { Text(context.getString(R.string.account_subjectinfo_title)) }, - colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), - navigationIcon = { - IconButton( - onClick = { - setResult(RESULT_CANCELED) - finish() - }, - content = { - Icon( - Icons.AutoMirrored.Filled.ArrowBack, - context.getString(R.string.action_back), - modifier = Modifier.size(25.dp) + Box( + contentAlignment = Alignment.BottomCenter, + content = { + TopAppBar( + title = { Text(context.getString(R.string.account_subjectinfo_title)) }, + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), + navigationIcon = { + IconButton( + onClick = { + setResult(RESULT_CANCELED) + finish() + }, + content = { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + context.getString(R.string.action_back), + modifier = Modifier.size(25.dp) + ) + } ) } ) + if (getMainViewModel().accountSession.subjectSchedule.processState.value == ProcessState.Running) { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } } ) }, @@ -95,12 +106,9 @@ fun AccountActivity.SubjectInformation( .fillMaxSize() .padding(padding), content = { - if (getMainViewModel().accountSession.subjectSchedule.processState.value == ProcessState.Running) { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) - } Column( modifier = Modifier - .fillMaxSize() + .fillMaxWidth() .padding(horizontal = 15.dp) .padding(vertical = 3.dp), horizontalAlignment = Alignment.CenterHorizontally, @@ -108,16 +116,15 @@ fun AccountActivity.SubjectInformation( Text(getMainViewModel().appSettings.value.currentSchoolYear.toString()) } ) - Column( + LazyColumn( modifier = Modifier .fillMaxSize() .padding(horizontal = 15.dp) - .padding(bottom = 7.dp) - .verticalScroll(rememberScrollState()), + .padding(bottom = 7.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, content = { - getMainViewModel().accountSession.subjectSchedule.data.forEach { item -> + items(getMainViewModel().accountSession.subjectSchedule.data) { item -> SubjectInformation( modifier = Modifier.padding(bottom = 7.dp), item = item, diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt index 210e32e..64cd381 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt @@ -4,6 +4,7 @@ import android.app.Activity.RESULT_CANCELED import android.content.Context import android.content.Intent import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer @@ -55,23 +56,31 @@ fun AccountActivity.TrainingResult( containerColor = containerColor, contentColor = contentColor, topBar = { - TopAppBar( - title = { Text("Account Training Result") }, - colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), - navigationIcon = { - IconButton( - onClick = { - setResult(RESULT_CANCELED) - finish() - }, - content = { - Icon( - Icons.AutoMirrored.Filled.ArrowBack, - context.getString(R.string.action_back), - modifier = Modifier.size(25.dp) + Box( + contentAlignment = Alignment.BottomCenter, + content = { + TopAppBar( + title = { Text("Account Training Result") }, + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), + navigationIcon = { + IconButton( + onClick = { + setResult(RESULT_CANCELED) + finish() + }, + content = { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + context.getString(R.string.action_back), + modifier = Modifier.size(25.dp) + ) + } ) } ) + if (getMainViewModel().accountSession.accountTrainingStatus.processState.value == ProcessState.Running) { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } } ) }, @@ -93,9 +102,6 @@ fun AccountActivity.TrainingResult( .fillMaxSize() .padding(padding), content = { - if (getMainViewModel().accountSession.accountTrainingStatus.processState.value == ProcessState.Running) { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) - } Column( modifier = Modifier .fillMaxSize() diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingSubjectResult.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingSubjectResult.kt index a66f7c3..b09ba34 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingSubjectResult.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingSubjectResult.kt @@ -6,6 +6,7 @@ import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row @@ -123,64 +124,72 @@ fun AccountActivity.TrainingSubjectResult( containerColor = containerColor, contentColor = contentColor, topBar = { - TopAppBar( - title = { - if (!searchEnabled.value) { - Text("Your subject result list") - } else { - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester), - value = searchQuery.value, - onValueChange = { - if (searchEnabled.value) { - searchQuery.value = it - } - }, - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Done - ), - keyboardActions = KeyboardActions( - onDone = { - clearAllFocusAndHideKeyboard() - } - ), - trailingIcon = { - }, - ) - } - }, - colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), - navigationIcon = { - IconButton( - onClick = { - if (searchEnabled.value) { - dismissSearchBar() + Box( + contentAlignment = Alignment.BottomCenter, + content = { + TopAppBar( + title = { + if (!searchEnabled.value) { + Text("Your subject result list") } else { - setResult(RESULT_CANCELED) - finish() + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + value = searchQuery.value, + onValueChange = { + if (searchEnabled.value) { + searchQuery.value = it + } + }, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + clearAllFocusAndHideKeyboard() + } + ), + trailingIcon = { + }, + ) } }, - content = { - Icon( - Icons.AutoMirrored.Filled.ArrowBack, - context.getString(R.string.action_back), - modifier = Modifier.size(25.dp) + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), + navigationIcon = { + IconButton( + onClick = { + if (searchEnabled.value) { + dismissSearchBar() + } else { + setResult(RESULT_CANCELED) + finish() + } + }, + content = { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + context.getString(R.string.action_back), + modifier = Modifier.size(25.dp) + ) + } ) + }, + actions = { + if (!searchEnabled.value) { + IconButton( + onClick = { + searchEnabled.value = true + }, + content = { + Icon(Icons.Default.Search, context.getString(R.string.action_search)) + } + ) + } } ) - }, - actions = { - if (!searchEnabled.value) { - IconButton( - onClick = { - searchEnabled.value = true - }, - content = { - Icon(Icons.Default.Search, context.getString(R.string.action_search)) - } - ) + if (getMainViewModel().accountSession.accountTrainingStatus.processState.value == ProcessState.Running) { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) } } ) @@ -206,9 +215,6 @@ fun AccountActivity.TrainingSubjectResult( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, content = { - if (getMainViewModel().accountSession.accountTrainingStatus.processState.value == ProcessState.Running) { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) - } Column( modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt index eb4fdfd..01592b1 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/news/MainView.kt @@ -6,6 +6,7 @@ import android.content.Context import android.content.Intent import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -25,6 +26,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Scaffold import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults @@ -98,34 +100,45 @@ fun NewsMainView( containerColor = containerColor, contentColor = contentColor, topBar = { - TopAppBar( - title = { Text(text = context.getString(R.string.news_title)) }, - colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), - navigationIcon = { - if (onBack != null) { - IconButton( - onClick = { - onBack() - }, - content = { - Icon( - Icons.AutoMirrored.Filled.ArrowBack, - context.getString(R.string.action_back), - modifier = Modifier.size(25.dp) + Box( + contentAlignment = Alignment.BottomCenter, + content = { + TopAppBar( + title = { Text(text = context.getString(R.string.news_title)) }, + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), + navigationIcon = { + if (onBack != null) { + IconButton( + onClick = { + onBack() + }, + content = { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + context.getString(R.string.action_back), + modifier = Modifier.size(25.dp) + ) + } ) } - ) - } - }, - actions = { - IconButton( - onClick = { - searchRequested?.let { it() } }, - content = { - Icon(Icons.Default.Search, context.getString(R.string.action_search)) + actions = { + IconButton( + onClick = { + searchRequested?.let { it() } + }, + content = { + Icon(Icons.Default.Search, context.getString(R.string.action_search)) + } + ) } ) + if (mainViewModel.newsInstance.newsGlobal.processState.value == ProcessState.Running && pagerState.currentPage == 0) { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } + if (mainViewModel.newsInstance.newsSubject.processState.value == ProcessState.Running && pagerState.currentPage == 1) { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } } ) }, diff --git a/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt b/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt index 589da38..bcf5014 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt @@ -84,6 +84,9 @@ class MainViewModel @Inject constructor( data = currentSchoolWeek.data.value, lastRequest = currentSchoolWeek.lastRequest.longValue ) + + // Reload school year in Account + accountSession.setSchoolYear(appSettings.value.currentSchoolYear) } val notificationHistory = mutableStateListOf() @@ -103,6 +106,9 @@ class MainViewModel @Inject constructor( if (!saveSettingsOnly) { fileModuleRepository.saveAccountSubjectScheduleCache(ArrayList(accountSession.getSubjectScheduleCache())) fileModuleRepository.saveNotificationHistory(ArrayList(notificationHistory.toList())) + + // Reload school year in Account + accountSession.setSchoolYear(appSettings.value.currentSchoolYear) } }, invokeOnCompleted = { onCompleted?.let { it() } } From b355e17f4d66e8764102f3a9cf72871cee6ff79d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BA=BF=20T=C3=B9ng?= <47247560+ZoeMeow1027@users.noreply.github.com> Date: Fri, 24 May 2024 19:49:54 +0700 Subject: [PATCH 19/21] Get school year from Internet and optimize code --- .../model/account/SchoolYearItem.kt | 2 + .../repository/FileModuleRepository.kt | 4 +- .../dialog/DialogSchoolYearSettings.kt | 96 ++++++++++++++++++- .../ui/view/main/MainViewDashboard.kt | 4 +- .../dutschedule/viewmodel/MainViewModel.kt | 83 +++++++++------- app/src/main/res/values-vi/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 7 files changed, 149 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/account/SchoolYearItem.kt b/app/src/main/java/io/zoemeow/dutschedule/model/account/SchoolYearItem.kt index 873a36a..7f973cc 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/account/SchoolYearItem.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/account/SchoolYearItem.kt @@ -1,6 +1,7 @@ package io.zoemeow.dutschedule.model.account import java.io.Serializable +import java.util.Locale data class SchoolYearItem( // School year (ex. 21 is for 2021-2022). @@ -20,6 +21,7 @@ data class SchoolYearItem( override fun toString(): String { return String.format( + Locale.ROOT, "School year: 20%2d-20%2d - Semester: %s", year, year + 1, diff --git a/app/src/main/java/io/zoemeow/dutschedule/repository/FileModuleRepository.kt b/app/src/main/java/io/zoemeow/dutschedule/repository/FileModuleRepository.kt index d6b9d5d..13c4159 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/repository/FileModuleRepository.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/repository/FileModuleRepository.kt @@ -226,7 +226,7 @@ class FileModuleRepository( file.writeText(Gson().toJson(data)) } - fun getSchoolYearCache(): Map? { + fun getCurrentSchoolYearCache(): Map? { val file = File(PATH_SCHOOLYEAR_CACHE) try { file.bufferedReader().apply { @@ -244,7 +244,7 @@ class FileModuleRepository( } } - fun saveSchoolYearCache(data: DutSchoolYearItem?, lastRequest: Long) { + fun saveCurrentSchoolYearCache(data: DutSchoolYearItem?, lastRequest: Long) { val file = File(PATH_SCHOOLYEAR_CACHE) val dataMap = mapOf("data" to Gson().toJson(data), "lastrequest" to lastRequest) file.writeText(Gson().toJson(dataMap)) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogSchoolYearSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogSchoolYearSettings.kt index 9b79557..066a5b8 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogSchoolYearSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/settings/dialog/DialogSchoolYearSettings.kt @@ -1,14 +1,26 @@ package io.zoemeow.dutschedule.ui.component.settings.dialog import android.content.Context +import android.util.Log 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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ElevatedButton import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -18,11 +30,17 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import io.dutwrapper.dutwrapper.Utils +import io.dutwrapper.dutwrapper.model.utils.DutSchoolYearItem import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.SettingsActivity +import io.zoemeow.dutschedule.model.ProcessState import io.zoemeow.dutschedule.model.account.SchoolYearItem import io.zoemeow.dutschedule.ui.component.base.DialogBase import io.zoemeow.dutschedule.ui.component.base.OutlinedTextBox +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import java.util.Locale @OptIn(ExperimentalMaterial3Api::class) @@ -44,6 +62,35 @@ fun DialogSchoolYearSettings( dropDownSemester.value = false } + val fetchProcess = remember { mutableStateOf(ProcessState.NotRunYet) } + fun fetchProcess() { + CoroutineScope(Dispatchers.IO).launch { + if (fetchProcess.value == ProcessState.Running) { + return@launch + } + fetchProcess.value = ProcessState.Running + + try { + Log.d("SchoolYearCurrent", "Getting from internet...") + val data = Utils.getCurrentSchoolWeek() + val schYear = SchoolYearItem( + year = data.schoolYearVal, + semester = when { + data.week >= 48 -> 3 + data.week >= 27 -> 2 + else -> 1 + } + ) + currentSettings.value = schYear + Log.d("SchoolYearCurrent", "Successful! Data from internet: $schYear") + fetchProcess.value = ProcessState.Successful + } catch (_: Exception) { + Log.d("SchoolYearCurrent", "Failed while getting from internet!") + fetchProcess.value = ProcessState.Failed + } + } + } + DialogBase( modifier = Modifier .fillMaxWidth() @@ -74,7 +121,12 @@ fun DialogSchoolYearSettings( .fillMaxWidth() .menuAnchor(), title = context.getString(R.string.settings_dialog_schyear_choice_schyear), - value = String.format("20%d-20%d", currentSettings.value.year, currentSettings.value.year+1) + value = String.format( + Locale.ROOT, + "20%d-20%d", + currentSettings.value.year, + currentSettings.value.year+1 + ) ) DropdownMenu( expanded = dropDownSchoolYear.value, @@ -82,7 +134,12 @@ fun DialogSchoolYearSettings( content = { 27.downTo(10).forEach { DropdownMenuItem( - text = { Text(String.format("20%2d-20%2d", it, it+1)) }, + text = { Text(String.format( + Locale.ROOT, + "20%2d-20%2d", + it, + it+1 + )) }, onClick = { currentSettings.value = currentSettings.value.clone( year = it @@ -137,6 +194,41 @@ fun DialogSchoolYearSettings( ) } ) + Row( + modifier = Modifier.fillMaxWidth().padding(top = 7.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + content = { + ElevatedButton( + onClick = { + fetchProcess() + }, + content = { + if (fetchProcess.value == ProcessState.Running) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + strokeWidth = 3.dp + ) + } else { + Row( + content = { + Text(context.getString(R.string.settings_dialog_schyear_action_fetch)) + Spacer(modifier = Modifier.size(5.dp)) + Icon( + imageVector = when (fetchProcess.value) { + ProcessState.Successful -> Icons.Default.Check + ProcessState.Failed -> Icons.Default.Close + else -> Icons.Default.Refresh + }, + contentDescription = "" + ) + } + ) + } + } + ) + } + ) } }, actionButtons = { diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt index 270a889..cd7e6d7 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/main/MainViewDashboard.kt @@ -252,8 +252,8 @@ fun MainActivity.MainViewDashboard( content = { DateAndTimeSummaryItem( padding = PaddingValues(bottom = 10.dp, start = 15.dp, end = 15.dp), - isLoading = getMainViewModel().currentSchoolWeek.processState.value == ProcessState.Running, - currentSchoolWeek = getMainViewModel().currentSchoolWeek.data.value, + isLoading = getMainViewModel().currentSchoolYearWeek.processState.value == ProcessState.Running, + currentSchoolWeek = getMainViewModel().currentSchoolYearWeek.data.value, opacity = getControlBackgroundAlpha() ) LessonTodaySummaryItem( diff --git a/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt b/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt index bcf5014..90d9720 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/viewmodel/MainViewModel.kt @@ -11,7 +11,8 @@ import dagger.hilt.android.lifecycle.HiltViewModel import io.dutwrapper.dutwrapper.Utils import io.dutwrapper.dutwrapper.model.utils.DutSchoolYearItem import io.zoemeow.dutschedule.model.NotificationHistory -import io.zoemeow.dutschedule.model.ProcessVariable +import io.zoemeow.dutschedule.model.ProcessState +import io.zoemeow.dutschedule.model.VariableState import io.zoemeow.dutschedule.model.account.AccountSession import io.zoemeow.dutschedule.model.account.DUTAccountInstance import io.zoemeow.dutschedule.model.news.DUTNewsInstance @@ -62,35 +63,54 @@ class MainViewModel @Inject constructor( } ) - // TODO: Change this to VariableState - /** - * Get current school week if possible. - */ - val currentSchoolWeek = ProcessVariable( - onRefresh = { _, _ -> - try { - return@ProcessVariable Utils.getCurrentSchoolWeek() - } catch (_: Exception) { - return@ProcessVariable null - } - }, - onAfterRefresh = { - saveCurrentSchoolWeekCache() - } + val currentSchoolYearWeek = VariableState( + data = mutableStateOf(null) ) - private fun saveCurrentSchoolWeekCache() { - fileModuleRepository.saveSchoolYearCache( - data = currentSchoolWeek.data.value, - lastRequest = currentSchoolWeek.lastRequest.longValue + private fun refreshCurrentSchoolYearWeek() { + launchOnScope( + script = { + if (currentSchoolYearWeek.processState.value == ProcessState.Running) { + return@launchOnScope + } + currentSchoolYearWeek.processState.value = ProcessState.Running + + currentSchoolYearWeek.data.value = Utils.getCurrentSchoolWeek() + }, + invokeOnCompleted = { + when { + it != null -> { + currentSchoolYearWeek.processState.value = ProcessState.Failed + } + else -> { + currentSchoolYearWeek.processState.value = ProcessState.Successful + saveCurrentSchoolYearWeekCache() + } + } + currentSchoolYearWeek.lastRequest.longValue = System.currentTimeMillis() + } ) + } - // Reload school year in Account - accountSession.setSchoolYear(appSettings.value.currentSchoolYear) + private fun saveCurrentSchoolYearWeekCache() { + fileModuleRepository.saveCurrentSchoolYearCache( + data = currentSchoolYearWeek.data.value, + lastRequest = currentSchoolYearWeek.lastRequest.longValue + ) } val notificationHistory = mutableStateListOf() + private fun loadCacheNotification() { + launchOnScope( + script = { + notificationHistory.clear() + notificationHistory.addAll(fileModuleRepository.getNotificationHistory()) + } + ) + } + + /** * Save all current settings to file in storage. */ @@ -115,15 +135,6 @@ class MainViewModel @Inject constructor( ) } - fun reloadNotification() { - launchOnScope( - script = { - notificationHistory.clear() - notificationHistory.addAll(fileModuleRepository.getNotificationHistory()) - } - ) - } - /** * Load all cache if possible for offline reading. */ @@ -142,14 +153,14 @@ class MainViewModel @Inject constructor( accountSession.setSubjectScheduleCache(it) } // Get school year cache - fileModuleRepository.getSchoolYearCache().also { + fileModuleRepository.getCurrentSchoolYearCache().also { if (it != null) { try { - currentSchoolWeek.data.value = Gson().fromJson( + currentSchoolYearWeek.data.value = Gson().fromJson( it["data"] ?: "", (object : TypeToken() {}.type) ) - currentSchoolWeek.lastRequest.longValue = (it["lastrequest"] ?: "0").toLong() + currentSchoolYearWeek.lastRequest.longValue = (it["lastrequest"] ?: "0").toLong() } catch (_: Exception) { } } } @@ -188,8 +199,8 @@ class MainViewModel @Inject constructor( runOnStartup( invokeOnCompleted = { loadCache() - currentSchoolWeek.refreshData(force = true) - reloadNotification() + refreshCurrentSchoolYearWeek() + loadCacheNotification() accountSession.reLogin(force = true) launchOnScope(script = { newsInstance.fetchGlobalNews( diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 1e33db0..2507491 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -128,6 +128,7 @@ Năm học Học kỳ học kỳ hè + Tải từ Internet Cài đặt thông báo tin tức Làm mới tin tức trong nền diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 63e0cc4..96cd393 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -128,6 +128,7 @@ School year Semester in summer + Fetch from Internet News Notification Settings Refresh news in background From e6972e0c1b6145fb6950046db1f6f136f2ad7bdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BA=BF=20T=C3=B9ng?= <47247560+ZoeMeow1027@users.noreply.github.com> Date: Mon, 27 May 2024 18:38:45 +0700 Subject: [PATCH 20/21] Update project - Updated AGP to 8.4.1. - Updated dependencies to latest to fix issues about Account feature. - Rework UI for Basic Account Information. - News filter settings will now only shown when enabled (this mean it will hidden at default). --- app/build.gradle | 2 +- .../ui/component/account/AccountInfoBanner.kt | 41 ++++-- .../account/AccountSubjectMoreInformation.kt | 80 ++++++------ .../dutschedule/ui/view/account/MainView.kt | 3 +- .../dutschedule/ui/view/account/SubjectFee.kt | 2 +- .../ui/view/account/SubjectInformation.kt | 2 +- .../ui/view/account/TrainingResult.kt | 2 +- .../ui/view/account/TrainingSubjectResult.kt | 20 +-- .../view/settings/NewsNotificationSettings.kt | 123 +++++++++--------- .../dutschedule/utils/FunctionExtension.kt | 6 + app/src/main/res/values-vi/strings.xml | 11 ++ app/src/main/res/values/strings.xml | 13 +- build.gradle | 2 +- 13 files changed, 177 insertions(+), 130 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index fbb9fc9..0d54f8f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -123,7 +123,7 @@ dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-datetime:0.5.0' - implementation 'com.github.dutwrapper:dutwrapper-java:v1.9.0' + implementation 'com.github.dutwrapper:dutwrapper-java:484d2f6f4a' implementation 'com.google.android.material:material:1.12.0' diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountInfoBanner.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountInfoBanner.kt index 280fdc1..4ced207 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountInfoBanner.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountInfoBanner.kt @@ -7,9 +7,14 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.outlined.AccountCircle import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -18,17 +23,20 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.ui.component.base.OutlinedTextBox +import io.zoemeow.dutschedule.utils.capitalized @Composable fun AccountInfoBanner( context: Context, padding: PaddingValues, isLoading: Boolean = false, + name: String? = null, username: String? = null, schoolClass: String? = null, - trainingProgramPlan: String? = null, + specialization: String? = null, opacity: Float = 1.0f ) { Surface( @@ -42,7 +50,7 @@ fun AccountInfoBanner( .fillMaxWidth() .wrapContentHeight() .padding(10.dp), - horizontalAlignment = Alignment.Start, + horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, content = { if (isLoading) { @@ -74,20 +82,25 @@ fun AccountInfoBanner( ) } ) - OutlinedTextBox( - title = "Username", - value = username ?: context.getString(R.string.data_unknown), - modifier = Modifier.fillMaxWidth().padding(bottom = 5.dp) + Icon( + Icons.Outlined.AccountCircle, + "Account Icon", + modifier = Modifier.size(64.dp) ) - OutlinedTextBox( - title = "Class", - value = schoolClass ?: context.getString(R.string.data_unknown), - modifier = Modifier.fillMaxWidth().padding(bottom = 5.dp) + Text( + name?.capitalized() ?: "(unknown name)", + fontSize = 20.sp, + modifier = Modifier.padding(top = 7.dp) ) - OutlinedTextBox( - title = "Training program plan", - value = trainingProgramPlan ?: context.getString(R.string.data_unknown), - modifier = Modifier.fillMaxWidth().padding(bottom = 5.dp) + Text( + "${username ?: "(unknown student ID)"} - ${schoolClass ?: "(unknown class)"}", + fontSize = 17.sp, + modifier = Modifier.padding(top = 5.dp) + ) + Text( + specialization ?: "(unknown specialization)", + fontSize = 17.sp, + modifier = Modifier.padding(top = 5.dp) ) } } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountSubjectMoreInformation.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountSubjectMoreInformation.kt index 91f28bb..eb77258 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountSubjectMoreInformation.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountSubjectMoreInformation.kt @@ -54,53 +54,57 @@ fun AccountSubjectMoreInformation( // Subject study Spacer(modifier = Modifier.size(15.dp)) ContentInBoxWithBorder( - title = "Schedule Study", + title = context.getString(R.string.account_subjectinfo_data_schedulestudy_title), content = { - var schList = "" - item?.let { - schList = it.subjectStudy.scheduleList.joinToString( - separator = "; ", - transform = { item1 -> - "${CustomDateUtil.dayOfWeekInString(item1.dayOfWeek + 1)},${item1.lesson.start}-${item1.lesson.end},${item1.room}" - } - ) - } - CustomText("Day of week: $schList") - var schWeek = "" - item?.let { - schWeek = it.subjectStudy.weekList.joinToString( - separator = "; ", - transform = { item1 -> - "${item1.start}-${item1.end}" - } - ) - } - CustomText("Week range: $schWeek") + CustomText(context.getString( + R.string.account_subjectinfo_data_schedulestudy_dayofweek, + item?.let { + it.subjectStudy.scheduleList.joinToString( + separator = "; ", + transform = { item1 -> + "${CustomDateUtil.dayOfWeekInString(item1.dayOfWeek + 1)},${item1.lesson.start}-${item1.lesson.end},${item1.room}" + } + ) + } ?: "" + )) + CustomText(context.getString( + R.string.account_subjectinfo_data_schedulestudy_weekrange, + item?.let { + it.subjectStudy.weekList.joinToString( + separator = "; ", + transform = { item1 -> + "${item1.start}-${item1.end}" + } + ) + } ?: "" + )) }, ) // Subject examination Spacer(modifier = Modifier.size(15.dp)) ContentInBoxWithBorder( - title = "Schedule Examination", + title = context.getString(R.string.account_subjectinfo_data_scheduleexam_title), content = { if (item != null) { - CustomText( - "Group: ${item.subjectExam.group}" + - if (item.subjectExam.isGlobal) " (global exam)" else "" - ) - CustomText( - "Date: ${ - CustomDateUtil.dateUnixToString( - item.subjectExam.date, - "dd/MM/yyyy HH:mm", - "GMT+7" - ) - }" - ) - CustomText("Room: ${item.subjectExam.room}") - + CustomText(context.getString( + R.string.account_subjectinfo_data_scheduleexam_group, + item.subjectExam.group, + if (item.subjectExam.isGlobal) context.getString(R.string.account_subjectinfo_data_scheduleexam_groupglobal) else "" + )) + CustomText(context.getString( + R.string.account_subjectinfo_data_scheduleexam_date, + CustomDateUtil.dateUnixToString( + item.subjectExam.date, + "dd/MM/yyyy HH:mm", + "GMT+7" + ) + )) + CustomText(context.getString( + R.string.account_subjectinfo_data_scheduleexam_room, + item.subjectExam.room + )) } else { - CustomText("Currently no examination schedule yet for this subject.") + CustomText(context.getString(R.string.account_subjectinfo_data_scheduleexam_noexamdate)) } } ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt index f67d429..6779796 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt @@ -185,9 +185,10 @@ fun AccountMainView( opacity = componentBackgroundAlpha, padding = PaddingValues(10.dp), isLoading = accInfo.processState.value == ProcessState.Running, + name = accInfo.data.value?.name, username = accInfo.data.value?.studentId, schoolClass = accInfo.data.value?.schoolClass, - trainingProgramPlan = accInfo.data.value?.trainingProgramPlan + specialization = accInfo.data.value?.specialization ) } ButtonBase( diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectFee.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectFee.kt index 7dab93b..d3626ee 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectFee.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectFee.kt @@ -101,7 +101,7 @@ fun AccountActivity.SubjectFee( modifier = Modifier .fillMaxWidth() .padding(horizontal = 15.dp) - .padding(vertical = 3.dp), + .padding(vertical = 2.dp), horizontalAlignment = Alignment.CenterHorizontally, content = { Text(getMainViewModel().appSettings.value.currentSchoolYear.toString()) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt index 47d3e19..29259a3 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt @@ -110,7 +110,7 @@ fun AccountActivity.SubjectInformation( modifier = Modifier .fillMaxWidth() .padding(horizontal = 15.dp) - .padding(vertical = 3.dp), + .padding(vertical = 2.dp), horizontalAlignment = Alignment.CenterHorizontally, content = { Text(getMainViewModel().appSettings.value.currentSchoolYear.toString()) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt index 64cd381..419ca3b 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingResult.kt @@ -60,7 +60,7 @@ fun AccountActivity.TrainingResult( contentAlignment = Alignment.BottomCenter, content = { TopAppBar( - title = { Text("Account Training Result") }, + title = { Text(context.getString(R.string.account_trainingstatus_title)) }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), navigationIcon = { IconButton( diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingSubjectResult.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingSubjectResult.kt index b09ba34..34f9ada 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingSubjectResult.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/TrainingSubjectResult.kt @@ -93,7 +93,7 @@ fun AccountActivity.TrainingSubjectResult( fun subjectResultToMap(item: SubjectResult): Map { return mapOf( - "Subject Year" to (item.schoolYear ?: "(unknown)"), + "Subject Year" to "${item.schoolYear ?: "(unknown)"}${ if (item.isExtendedSemester) "in summer" else "" }", "Subject Code" to (item.id ?: "(unknown)"), "Credit" to item.credit.toString(), "Point formula" to (item.pointFormula ?: "(unknown)"), @@ -291,21 +291,21 @@ fun AccountActivity.TrainingSubjectResult( backgroundColor = MaterialTheme.colorScheme.background.copy(alpha = getControlBackgroundAlpha()), text = "Index", textAlign = TextAlign.Center, - weight = 0.2f + weight = 0.17f ) TableCell( modifier = Modifier.fillMaxHeight(), backgroundColor = MaterialTheme.colorScheme.background.copy(alpha = getControlBackgroundAlpha()), text = "Subject name", textAlign = TextAlign.Center, - weight = 0.6f + weight = 0.58f ) TableCell( modifier = Modifier.fillMaxHeight(), backgroundColor = MaterialTheme.colorScheme.background.copy(alpha = getControlBackgroundAlpha()), - text = "Result (T4/C)", + text = "Result T10(T4)", textAlign = TextAlign.Center, - weight = 0.2f + weight = 0.25f ) } ) @@ -337,7 +337,7 @@ fun AccountActivity.TrainingSubjectResult( backgroundColor = MaterialTheme.colorScheme.background.copy(alpha = getControlBackgroundAlpha()), text = "${subjectItem.index}", textAlign = TextAlign.Center, - weight = 0.2f + weight = 0.17f ) TableCell( modifier = Modifier.fillMaxHeight(), @@ -345,18 +345,18 @@ fun AccountActivity.TrainingSubjectResult( text = subjectItem.name, contentAlign = Alignment.CenterStart, textAlign = TextAlign.Start, - weight = 0.6f + weight = 0.58f ) TableCell( modifier = Modifier.fillMaxHeight(), backgroundColor = MaterialTheme.colorScheme.background.copy(alpha = getControlBackgroundAlpha()), text = String.format( "%s (%s)", - if (subjectItem.resultT4 != null) "${subjectItem.resultT4}" else "---", - if (subjectItem.resultByCharacter != null) "${subjectItem.resultByCharacter}" else "-" + subjectItem.resultT10?.toString() ?: "---", + subjectItem.resultT4?.toString() ?: "---" ), textAlign = TextAlign.Center, - weight = 0.2f + weight = 0.25f ) } ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt index 990ce40..f6023fb 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/settings/NewsNotificationSettings.kt @@ -256,7 +256,7 @@ fun SettingsActivity.NewsNotificationSettings( getMainViewModel().saveSettings(saveSettingsOnly = true) showSnackBar( text = context.getString( - R.string.settings_newsnotify_newsfilter_notify_add, + R.string.settings_newsnotify_newsfilter_notify_delete, tempDeleteItem.value.subjectName, tempDeleteItem.value.studentYearId, ".Nh", @@ -398,7 +398,7 @@ private fun MainView( 1 -> context.getString(R.string.settings_newsnotify_fetchnewsinbackground_modifiedvalue_enabled1) else -> context.getString( R.string.settings_newsnotify_fetchnewsinbackground_modifiedvalue_enabled2, - fetchNewsInBackgroundDuration + durationTemp.intValue ) } ) @@ -553,68 +553,69 @@ private fun MainView( clicked = { }, opacity = opacity ) - } - SimpleCardItem( - padding = PaddingValues(horizontal = 20.4.dp, vertical = 5.dp), - title = context.getString(R.string.settings_newsnotify_newsfilter_list_title), - clicked = { }, - opacity = opacity, - content = { - Column( - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(horizontal = 15.dp) - .padding(bottom = 15.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.Start - ) { - if (subjectFilterList.size == 0) { - Text(context.getString(R.string.settings_newsnotify_newsfilter_list_nofilters)) - } - subjectFilterList.forEach { code -> - OptionItem( - modifier = Modifier.padding(vertical = 3.dp), - modifierInside = Modifier, - title = "${code.subjectName} [${code.studentYearId}.Nh${code.classId}]", - onClick = { }, - trailingIcon = { - IconButton( - onClick = { - if (fetchNewsInBackgroundDuration > 0) { - onSubjectFilterDelete?.let { it(code) } + } else { + SimpleCardItem( + padding = PaddingValues(horizontal = 20.4.dp, vertical = 5.dp), + title = context.getString(R.string.settings_newsnotify_newsfilter_list_title), + clicked = { }, + opacity = opacity, + content = { + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(horizontal = 15.dp) + .padding(bottom = 15.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + if (subjectFilterList.size == 0) { + Text(context.getString(R.string.settings_newsnotify_newsfilter_list_nofilters)) + } + subjectFilterList.forEach { code -> + OptionItem( + modifier = Modifier.padding(vertical = 3.dp), + modifierInside = Modifier, + title = "${code.subjectName} [${code.studentYearId}.Nh${code.classId}]", + onClick = { }, + trailingIcon = { + IconButton( + onClick = { + if (fetchNewsInBackgroundDuration > 0) { + onSubjectFilterDelete?.let { it(code) } + } + }, + content = { + Icon(Icons.Default.Delete, context.getString(R.string.action_delete)) } - }, - content = { - Icon(Icons.Default.Delete, context.getString(R.string.action_delete)) - } - ) - } - ) + ) + } + ) + } } } - } - ) - OptionItem( - modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), - title = context.getString(R.string.settings_newsnotify_newsfilter_add), - leadingIcon = { Icon(Icons.Default.Add, context.getString(R.string.settings_newsnotify_newsfilter_add)) }, - isEnabled = isNewsSubjectEnabled == 2, - onClick = { - // Add a subject news filter - onSubjectFilterAdd?.let { it() } - } - ) - OptionItem( - modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), - title = context.getString(R.string.settings_newsnotify_newsfilter_deleteall), - leadingIcon = { Icon(Icons.Default.Delete, context.getString(R.string.settings_newsnotify_newsfilter_deleteall)) }, - isEnabled = isNewsSubjectEnabled == 2, - onClick = { - // Clear all subject news filter list - onSubjectFilterClear?.let { it() } - } - ) + ) + OptionItem( + modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), + title = context.getString(R.string.settings_newsnotify_newsfilter_add), + leadingIcon = { Icon(Icons.Default.Add, context.getString(R.string.settings_newsnotify_newsfilter_add)) }, + isEnabled = isNewsSubjectEnabled == 2, + onClick = { + // Add a subject news filter + onSubjectFilterAdd?.let { it() } + } + ) + OptionItem( + modifierInside = Modifier.padding(horizontal = 20.dp, vertical = 15.dp), + title = context.getString(R.string.settings_newsnotify_newsfilter_deleteall), + leadingIcon = { Icon(Icons.Default.Delete, context.getString(R.string.settings_newsnotify_newsfilter_deleteall)) }, + isEnabled = isNewsSubjectEnabled == 2, + onClick = { + // Clear all subject news filter list + onSubjectFilterClear?.let { it() } + } + ) + } } } } diff --git a/app/src/main/java/io/zoemeow/dutschedule/utils/FunctionExtension.kt b/app/src/main/java/io/zoemeow/dutschedule/utils/FunctionExtension.kt index afde51d..6b17971 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/utils/FunctionExtension.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/utils/FunctionExtension.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import java.math.BigInteger import java.security.MessageDigest import java.text.Normalizer +import java.util.Locale fun Context.openLink( url: String, @@ -51,6 +52,11 @@ fun Context.openLink( } } +fun String.capitalized(): String { + return this.split(" ") + .joinToString(separator = " ") { it.lowercase().replaceFirstChar(Char::uppercase) } +} + @Composable fun Modifier.endOfListReached( lazyListState: LazyListState, diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 2507491..07046e8 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -244,6 +244,17 @@ Số tín chỉ Chất lượng cao Công thức điểm + Lịch học + Lịch học trong tuần: %1$s + Tuần: %1$s + Lịch thi + Nhóm: %1$s %2$s + (thi chung) + Ngày thi: %1$s + Phòng: %1$s + Hiện tại không có lịch thi cho môn học này. + + Kết quả rèn luyện (không rõ) \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 96cd393..d6184a1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -229,7 +229,7 @@ Subject Information Subject Fee Account Information - Account Training Result + Training Result Logout Logout Are you sure you want to logout?\n\nNote that:\n- You won\'t be received your any subject schedule anymore.\n- Your news filter settings won\'t be affected. @@ -244,6 +244,17 @@ Credit(s) High Quality Final score formula + Schedule Study + Schedule in a week: %1$s + Week(s): %1$s + Schedule Examination + Group: %1$s %2$s + (global group) + Exam date: %1$s + Room: %1$s + Currently no examination schedule yet for this subject. + + Training Result (unknown) \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8de17f9..f317c5f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '8.4.0' apply false + id 'com.android.application' version '8.4.1' apply false id 'org.jetbrains.kotlin.android' version '1.8.10' apply false id 'com.google.dagger.hilt.android' version '2.44' apply false } From 892d2e9f5c5480eb61f465ed3ef3b5bba3626d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Qu=E1=BA=BF=20T=C3=B9ng?= <47247560+ZoeMeow1027@users.noreply.github.com> Date: Wed, 29 May 2024 22:28:29 +0700 Subject: [PATCH 21/21] Bumped to 2.0-draft17 (1404) What's new - News notification settings: Change your preferences about news notifications. - Changed UI for Subject Fee (not final yet). Changes and fixes - Fixed [#24](https://github.com/ZoeMeow1027/DutSchedule/issues/24). - Add a option to get current student year from Internet. - To access that, please go to `Settings` -> `Experiment Settings` -> `Current School Year`. - Some settings won't load after update. You might need to reconfig them in settings. I'm sorry about that. - Login screen will let you relogin if previous attempt was failed. - You can clear previous login to attempt with another account. - Now you can turn off notifications about news global and news subject. - In news subject notifications, you can choose about "All", "Match your filter" and "Off". - News filter settings will now only shown when enabled (this mean it will hidden at default). - Update Vietnamese strings for most screens, but not done yet. - Optimized codes and updated dependencies to latest. - Updated dependencies to latest to fix issues about Account feature. --- CHANGELOG.md | 19 +++++ CREDIT.md | 25 ------ README.md | 48 ++++++----- app/build.gradle | 4 +- .../dutschedule/activity/AccountActivity.kt | 2 + .../model/account/SchoolYearItem.kt | 15 ++++ .../account/AccountSubjectFeeInformation.kt | 62 +++++++++++--- .../account/AccountSubjectInformation.kt | 25 ++++-- .../account/AccountSubjectMoreInformation.kt | 3 +- .../ui/component/account/LoginBox.kt | 2 +- .../ui/component/base/OutlinedTextBox.kt | 4 +- .../dutschedule/ui/component/base/Tag.kt | 55 +++++++++++++ .../ui/view/account/AccountInformation.kt | 67 ++++++++++----- .../dutschedule/ui/view/account/MainView.kt | 81 +++++++++++++------ .../dutschedule/ui/view/account/SubjectFee.kt | 20 ++++- .../ui/view/account/SubjectInformation.kt | 2 +- .../dutschedule/utils/CustomDateUtil.kt | 33 ++++---- .../drawable/ic_baseline_content_copy_24.xml | 5 ++ app/src/main/res/values-vi/strings.xml | 54 ++++++++++++- app/src/main/res/values/strings.xml | 55 ++++++++++++- 20 files changed, 439 insertions(+), 142 deletions(-) delete mode 100644 CREDIT.md create mode 100644 app/src/main/java/io/zoemeow/dutschedule/ui/component/base/Tag.kt create mode 100644 app/src/main/res/drawable/ic_baseline_content_copy_24.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 78fc6ba..63fdd1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,25 @@ ## Known issues: - `Your current wallpaper` option in app background settings will be disabled on Android 14. You can check why in `Issue` tab in repository. +## 2.0-draft17 (1404) +- What's new + - News notification settings: Change your preferences about news notifications. + - Changed UI for Subject Fee (not final yet). + +- Changes and fixes + - Fixed [#24](https://github.com/ZoeMeow1027/DutSchedule/issues/24). + - Add a option to get current student year from Internet. + - To access that, please go to `Settings` -> `Experiment Settings` -> `Current School Year`. + - Some settings won't load after update. You might need to reconfig them in settings. I'm sorry about that. + - Login screen will let you relogin if previous attempt was failed. + - You can clear previous login to attempt with another account. + - Now you can turn off notifications about news global and news subject. + - In news subject notifications, you can choose about "All", "Match your filter" and "Off". + - News filter settings will now only shown when enabled (this mean it will hidden at default). + - Update Vietnamese strings for most screens, but not done yet. + - Optimized codes and updated dependencies to latest. + - Updated dependencies to latest to fix issues about Account feature. + ## 2.0-draft16 (1063) - AccountActivity: - Add option to refresh login when previous is failed. diff --git a/CREDIT.md b/CREDIT.md deleted file mode 100644 index 3e89518..0000000 --- a/CREDIT.md +++ /dev/null @@ -1,25 +0,0 @@ -# DutSchedule - CREDIT AND COPYRIGHT - -## DISCLAIMER -- This project - DutSchedule - is not affiliated with Da Nang University of Technology. -- DUT, Da Nang University of Technology, web materials and web contents are trademarks and copyrights of Da Nang University of Technology school. -- GitHub, GitHub mark and icon are trademarks and copyrights of GitHub, Inc. - -## Google and Android -- Google, Android and its icon are trademarks and copyrights of Google LLC. - -## Google Accompanist -- https://github.com/google/accompanist -- Licensed under the [Apache License 2.0](https://github.com/google/accompanist/blob/main/LICENSE). - -## Google Gson -- https://github.com/google/gson -- Licensed under the [Apache License 2.0](https://github.com/google/gson/blob/main/LICENSE). - -## Jsoup -- https://github.com/jhy/jsoup/ -- Licensed under the [MIT license](https://github.com/jhy/jsoup/blob/master/LICENSE). - -## timeago -- https://github.com/marlonlom/timeago -- Licensed under the [Apache License 2.0](https://github.com/marlonlom/timeago/blob/master/LICENSE). \ No newline at end of file diff --git a/README.md b/README.md index 82474d3..c84d6bd 100644 --- a/README.md +++ b/README.md @@ -3,38 +3,24 @@ A unofficial Android app to provide better UI from [sv.dut.udn.vn](http://sv.dut.udn.vn). # Version - - Release version: [![https://github.com/ZoeMeow1027/DutSchedule](https://img.shields.io/github/v/release/ZoeMeow1027/DutSchedule)](https://github.com/ZoeMeow1027/DutSchedule/releases) - Pre-release version: [![https://github.com/ZoeMeow1027/DutSchedule/tree/draft](https://img.shields.io/github/v/tag/ZoeMeow1027/DutSchedule?label=pre-release%20tag)](https://github.com/ZoeMeow1027/DutSchedule/tree/draft) - Badge provided by [https://shields.io/](https://shields.io/). +- If you want to view changelog, [click here](#where-can-i-found-app-changelog). # Features & Screenshots? - - These screenshot will get you to app summary. Just navigate to [screenshot](SCREENSHOT.md) and open images to view details. # Downloads - - Navigate to release (at right of this README) or click [here](https://github.com/ZoeMeow1027/DutSchedule/releases) to download app. -# Build app yourself - -- Required Gradle: 8.5 - - Older version of Gradle may be failed while building. -- If you open project with Android Studio, make sure your IDE support Gradle [Gradle](https://gradle.org/releases/) above, which can be fixed by upgrading your IDE. After that, just build and run app normally as you do with another Android project. -- If you want to build app without IDE, just type command as you build another gradle project (note that you still need to [Gradle](https://gradle.org/releases/) installed first): - -``` -Build: gradlew build -For Powershell: ./gradlew build -``` - # FAQ ### Where can I found app changelog? If you want to: - View major changes: [Click here](CHANGELOG.md). -- View entire source code changes, [click here](https://github.com/ZoeMeow1027/DutSchedule/commits). +- View entire source code changes: [Click here](https://github.com/ZoeMeow1027/DutSchedule/commits). - You will need to change branch if you want to view changelog for stable/draft version. ### Why some news in application is different from sv.dut.udn.vn? @@ -53,7 +39,29 @@ If you found a issue, you can report this via [issue tab](https://github.com/Zoe - Can't get current wallpaper as my app background wallpaper. - On Android 14, Google is restricted for getting current wallpaper on Android 14 or later. This issue will be delayed very loong until a posible fix. You can [see why here](https://github.com/ZoeMeow1027/DutSchedule/issues/19). -# Credits, changelog and license? -- [Changelog](CHANGELOG.md) -- [Credit](CREDIT.md) -- License: [MIT](LICENSE) +# Developing +- Required Gradle: 8.7 + - Older version of Gradle may be failed while building. +- Build with Android Studio: + - Make sure your IDE support [Gradle](https://gradle.org/releases/) above, which can be fixed by upgrading your IDE. + - After that, just build and run app normally as you do with another Android project. +- Build with command line (without IDE): + - Ensure you have installed [Gradle](https://gradle.org/releases/) and Java JDK 17 first. + - Type command as you build another gradle project. +``` +Build: gradlew build +In Powershell: ./gradlew build +``` + +# Credits and license? +- License: [**MIT**](LICENSE) +- DISCLAIMER: + - This project - DutSchedule - is not affiliated with Da Nang University of Technology. + - DUT, Da Nang University of Technology, web materials and web contents are trademarks and copyrights of Da Nang University of Technology school. + - GitHub, GitHub mark and its icon are trademarks and copyrights of GitHub, Inc. + - Google, Android and its icon are trademarks and copyrights of Google LLC. +- Used third-party dependencies: + - [Google Accompanist](https://github.com/google/accompanist): Licensed under the [Apache License 2.0](https://github.com/google/accompanist/blob/main/LICENSE). + - [Google Gson](https://github.com/google/gson): Licensed under the [Apache License 2.0](https://github.com/google/gson/blob/main/LICENSE). + - [Jsoup](https://github.com/jhy/jsoup): Licensed under the [MIT license](https://github.com/jhy/jsoup/blob/master/LICENSE). + - [timeago](https://github.com/marlonlom/timeago): Licensed under the [Apache License 2.0](https://github.com/marlonlom/timeago/blob/master/LICENSE). diff --git a/app/build.gradle b/app/build.gradle index 0d54f8f..edc38aa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,8 +14,8 @@ android { applicationId "io.zoemeow.dutschedule" minSdk 21 targetSdkVersion 34 - versionCode 1064 - versionName "2.0-draft16" + versionCode 1404 + versionName "2.0-draft17" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/io/zoemeow/dutschedule/activity/AccountActivity.kt b/app/src/main/java/io/zoemeow/dutschedule/activity/AccountActivity.kt index 787286a..fdf8dcd 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/activity/AccountActivity.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/activity/AccountActivity.kt @@ -45,6 +45,7 @@ class AccountActivity: BaseActivity() { } INTENT_SUBJECTFEE -> { SubjectFee( + context = context, snackBarHostState = snackBarHostState, containerColor = containerColor, contentColor = contentColor @@ -52,6 +53,7 @@ class AccountActivity: BaseActivity() { } INTENT_ACCOUNTINFORMATION -> { AccountInformation( + context = context, snackBarHostState = snackBarHostState, containerColor = containerColor, contentColor = contentColor diff --git a/app/src/main/java/io/zoemeow/dutschedule/model/account/SchoolYearItem.kt b/app/src/main/java/io/zoemeow/dutschedule/model/account/SchoolYearItem.kt index 7f973cc..36cb478 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/model/account/SchoolYearItem.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/model/account/SchoolYearItem.kt @@ -1,5 +1,8 @@ package io.zoemeow.dutschedule.model.account +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import io.zoemeow.dutschedule.R import java.io.Serializable import java.util.Locale @@ -28,4 +31,16 @@ data class SchoolYearItem( if (semester == 3) "Summer semester" else semester.toString() ) } + + @Composable + fun composeToString(): String { + val context = LocalContext.current + return context.getString( + R.string.account_schoolyear_main, + year, + year + 1, + if (semester == 3) 2 else semester, + if (semester == 3) context.getString(R.string.account_schoolyear_summer) else "" + ).trim() + } } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountSubjectFeeInformation.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountSubjectFeeInformation.kt index 05b1eff..cbac081 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountSubjectFeeInformation.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountSubjectFeeInformation.kt @@ -1,19 +1,31 @@ package io.zoemeow.dutschedule.ui.component.account import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import io.dutwrapper.dutwrapper.model.accounts.SubjectFeeItem +import io.zoemeow.dutschedule.R +import io.zoemeow.dutschedule.ui.component.base.Tag +@OptIn(ExperimentalLayoutApi::class) @Composable fun AccountSubjectFeeInformation( modifier: Modifier = Modifier, @@ -21,6 +33,7 @@ fun AccountSubjectFeeInformation( onClick: (() -> Unit)? = null, opacity: Float = 1f ) { + val context = LocalContext.current Surface( modifier = modifier .fillMaxWidth() @@ -35,19 +48,46 @@ fun AccountSubjectFeeInformation( Column( modifier = Modifier.padding(10.dp), content = { - Text( - text = item.name, - style = MaterialTheme.typography.titleLarge, + FlowRow( + verticalArrangement = Arrangement.Center, + horizontalArrangement = Arrangement.Start, + content = { + Text( + text = item.name, + style = MaterialTheme.typography.titleLarge, + ) + Spacer(modifier = Modifier.size(7.dp)) + if (item.debt) { + Tag( + text = context.getString(R.string.account_subjectfee_status_notdoneyet), + backColor = Color.Red, + textColor = Color.White + ) + } else { + Tag( + text = context.getString(R.string.account_subjectfee_status_completed), + backColor = Color.Green + ) + } + } ) + Spacer(modifier = Modifier.size(5.dp)) Text( - text = String.format( - "%d credit%s\nPrice: %.0f VND\nStatus: %s", - item.credit, - if (item.credit != 1) "s" else "", - item.price, - if (item.debt) "Not completed yet" else "Completed" - ), - style = MaterialTheme.typography.bodyMedium + text = if (item.credit == 1) { + context.getString( + R.string.account_subjectfee_summary_1credit, + item.credit, + item.price, + "VND" + ) + } else { + context.getString( + R.string.account_subjectfee_summary_manycredit, + item.credit, + item.price, + "VND" + ) + } ) } ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountSubjectInformation.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountSubjectInformation.kt index 4d6bf6c..4f00c38 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountSubjectInformation.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountSubjectInformation.kt @@ -11,8 +11,10 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import io.dutwrapper.dutwrapper.model.accounts.SubjectScheduleItem +import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.utils.CustomDateUtil @Composable @@ -22,6 +24,7 @@ fun SubjectInformation( onClick: (() -> Unit)? = null, opacity: Float = 1f ) { + val context = LocalContext.current Surface( modifier = modifier .fillMaxWidth() @@ -41,12 +44,22 @@ fun SubjectInformation( style = MaterialTheme.typography.titleLarge, ) Text( - text = "${item.lecturer}\n\nSchedule:\n${item.subjectStudy.scheduleList.joinToString( - separator = "\n", - transform = { schItem -> - "- ${CustomDateUtil.dayOfWeekInString(schItem.dayOfWeek, true)} - Lesson ${schItem.lesson.start}-${schItem.lesson.end} - Room ${schItem.room}" - } - )}", + text = context.getString( + R.string.account_subjectinfo_summary_schinfo, + item.lecturer, + item.subjectStudy.scheduleList.joinToString( + separator = "\n", + transform = { schItem -> + context.getString( + R.string.account_subjectinfo_summary_schitem, + CustomDateUtil.dayOfWeekInString(context, schItem.dayOfWeek, true), + schItem.lesson.start, + schItem.lesson.end, + schItem.room + ) + } + ) + ), style = MaterialTheme.typography.bodyMedium ) } diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountSubjectMoreInformation.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountSubjectMoreInformation.kt index eb77258..d9fc370 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountSubjectMoreInformation.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/AccountSubjectMoreInformation.kt @@ -17,6 +17,7 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import io.dutwrapper.dutwrapper.model.accounts.SubjectScheduleItem import io.zoemeow.dutschedule.R @@ -62,7 +63,7 @@ fun AccountSubjectMoreInformation( it.subjectStudy.scheduleList.joinToString( separator = "; ", transform = { item1 -> - "${CustomDateUtil.dayOfWeekInString(item1.dayOfWeek + 1)},${item1.lesson.start}-${item1.lesson.end},${item1.room}" + "${CustomDateUtil.dayOfWeekInString(context, item1.dayOfWeek + 1)},${item1.lesson.start}-${item1.lesson.end},${item1.room}" } ) } ?: "" diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LoginBox.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LoginBox.kt index cad4983..3438579 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LoginBox.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/account/LoginBox.kt @@ -234,7 +234,7 @@ private fun LoginBoxPreview() { context = LocalContext.current, isProcessing = false, isControlEnabled = true, - isLoggedInBefore = false, + isLoggedInBefore = true, onSubmit = { _, _, _ -> } ) } \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/OutlinedTextBox.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/OutlinedTextBox.kt index 4459d28..1b540e1 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/OutlinedTextBox.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/OutlinedTextBox.kt @@ -9,13 +9,15 @@ import androidx.compose.ui.Modifier fun OutlinedTextBox( modifier: Modifier = Modifier, title: String, - value: String? = null + value: String? = null, + trailingIcon: @Composable (() -> Unit)? = null ) { OutlinedTextField( modifier = modifier, value = if (value.isNullOrEmpty()) "(no information)" else value, readOnly = true, onValueChange = { }, + trailingIcon = trailingIcon, label = { Text(title) } ) } \ No newline at end of file diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/Tag.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/Tag.kt new file mode 100644 index 0000000..78c856f --- /dev/null +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/component/base/Tag.kt @@ -0,0 +1,55 @@ +package io.zoemeow.dutschedule.ui.component.base + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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 androidx.compose.ui.unit.sp + +@Composable +fun Tag( + text: String = "", + textColor: Color = Color.Black, + backColor: Color = Color.White +) { + Surface( + shape = RoundedCornerShape(20.dp), + color = backColor, + content = { + Text( + text = text, + color = textColor, + fontSize = 13.sp, + modifier = Modifier.padding( + horizontal = 10.dp, + vertical = 2.dp + ) + ) + } + ) +} + +@Preview +@Composable +private fun TagPreview() { + Tag( + "Hello", + textColor = Color.Black, + backColor = Color.Green + ) +} + +@Preview +@Composable +private fun TagPreview2() { + Tag( + "Failed tag!", + textColor = Color.White, + backColor = Color.Red + ) +} diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/AccountInformation.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/AccountInformation.kt index ea20679..f76c96a 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/AccountInformation.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/AccountInformation.kt @@ -1,7 +1,7 @@ package io.zoemeow.dutschedule.ui.view.account import android.app.Activity.RESULT_OK -import androidx.activity.ComponentActivity +import android.content.Context import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -30,7 +30,13 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.ClipboardManager +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp +import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.AccountActivity import io.zoemeow.dutschedule.model.ProcessState import io.zoemeow.dutschedule.ui.component.base.OutlinedTextBox @@ -38,10 +44,12 @@ import io.zoemeow.dutschedule.ui.component.base.OutlinedTextBox @OptIn(ExperimentalMaterial3Api::class) @Composable fun AccountActivity.AccountInformation( + context: Context, snackBarHostState: SnackbarHostState, containerColor: Color, contentColor: Color ) { + val clipboardManager: ClipboardManager = LocalClipboardManager.current Scaffold( modifier = Modifier.fillMaxSize(), snackbarHost = { SnackbarHost(hostState = snackBarHostState) }, @@ -52,7 +60,7 @@ fun AccountActivity.AccountInformation( contentAlignment = Alignment.BottomCenter, content = { TopAppBar( - title = { Text("Basic Information") }, + title = { Text(context.getString(R.string.account_accinfo_title)) }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), navigationIcon = { IconButton( @@ -63,7 +71,7 @@ fun AccountActivity.AccountInformation( content = { Icon( Icons.AutoMirrored.Filled.ArrowBack, - "", + context.getString(R.string.action_back), modifier = Modifier.size(25.dp) ) } @@ -83,7 +91,7 @@ fun AccountActivity.AccountInformation( getMainViewModel().accountSession.fetchAccountInformation(force = true) }, content = { - Icon(Icons.Default.Refresh, "Refresh") + Icon(Icons.Default.Refresh, context.getString(R.string.action_refresh)) } ) } @@ -104,23 +112,23 @@ fun AccountActivity.AccountInformation( content = { getMainViewModel().accountSession.accountInformation.data.value?.let { data -> val mapPersonalInfo = mapOf( - "Name" to (data.name ?: "(unknown)"), - "Date of birth" to (data.dateOfBirth ?: "(unknown)"), - "Place of birth" to (data.birthPlace ?: "(unknown)"), - "Gender" to (data.gender ?: "(unknown)"), - "National ID card" to (data.nationalIdCard ?: "(unknown)"), - "National card issue place and date" to ("${data.nationalIdCardIssuePlace ?: "(unknown)"} on ${data.nationalIdCardIssueDate ?: "(unknown)"}"), - "Citizen card date" to (data.citizenIdCardIssueDate ?: "(unknown)"), - "Citizen ID card" to (data.citizenIdCard ?: "(unknown)"), - "Bank card ID" to ("${data.accountBankId ?: "(unknown)"} (${data.accountBankName ?: "(unknown)"})"), - "Personal email" to (data.personalEmail ?: "(unknown)"), - "Phone number" to (data.phoneNumber ?: "(unknown)"), - "Class" to (data.schoolClass ?: "(unknown)"), - "Specialization" to (data.specialization ?: "(unknown)"), - "Training program plan" to (data.trainingProgramPlan ?: "(unknown)"), - "School email" to (data.schoolEmail ?: "(unknown)"), + context.getString(R.string.account_accinfo_item_name) to (data.name ?: context.getString(R.string.data_unknown)), + context.getString(R.string.account_accinfo_item_dateofbirth) to (data.dateOfBirth ?: context.getString(R.string.data_unknown)), + context.getString(R.string.account_accinfo_item_placeofbirth) to (data.birthPlace ?: context.getString(R.string.data_unknown)), + context.getString(R.string.account_accinfo_item_gender) to (data.gender ?: context.getString(R.string.data_unknown)), + context.getString(R.string.account_accinfo_item_nationalcardid) to (data.nationalIdCard ?: context.getString(R.string.data_unknown)), + context.getString(R.string.account_accinfo_item_nationalcardplaceanddate) to ("${data.nationalIdCardIssuePlace ?: context.getString(R.string.data_unknown)} on ${data.nationalIdCardIssueDate ?: context.getString(R.string.data_unknown)}"), + context.getString(R.string.account_accinfo_item_citizencardid) to (data.citizenIdCard ?: context.getString(R.string.data_unknown)), + context.getString(R.string.account_accinfo_item_citizencarddate) to (data.citizenIdCardIssueDate ?: context.getString(R.string.data_unknown)), + context.getString(R.string.account_accinfo_item_bankcardid) to ("${data.accountBankId ?: context.getString(R.string.data_unknown)} (${data.accountBankName ?: context.getString(R.string.data_unknown)})"), + context.getString(R.string.account_accinfo_item_personalemail) to (data.personalEmail ?: context.getString(R.string.data_unknown)), + context.getString(R.string.account_accinfo_item_phonenumber) to (data.phoneNumber ?: context.getString(R.string.data_unknown)), + context.getString(R.string.account_accinfo_item_class) to (data.schoolClass ?: context.getString(R.string.data_unknown)), + context.getString(R.string.account_accinfo_item_specialization) to (data.specialization ?: context.getString(R.string.data_unknown)), + context.getString(R.string.account_accinfo_item_trainingprogramplan) to (data.trainingProgramPlan ?: context.getString(R.string.data_unknown)), + context.getString(R.string.account_accinfo_item_schoolemail) to (data.schoolEmail ?: context.getString(R.string.data_unknown)), ) - Text("Click and hold a text field, select all and click Copy to copy it.\nIf you want to edit any information below, you need do it in DUT Information System web.") + Text(context.getString(R.string.account_accinfo_description)) Spacer(modifier = Modifier.size(5.dp)) Column( modifier = Modifier @@ -132,7 +140,24 @@ fun AccountActivity.AccountInformation( mapPersonalInfo.keys.forEach { title -> OutlinedTextBox( title = title, - value = mapPersonalInfo[title] ?: "(unknown)", + value = mapPersonalInfo[title] ?: context.getString(R.string.data_unknown), + trailingIcon = { + IconButton( + onClick = { + clipboardManager.setText(AnnotatedString(mapPersonalInfo[title] ?: "")) + showSnackBar( + context.getString(R.string.account_accinfo_snackbar_copied), + clearPrevious = true + ) + }, + content = { + Icon( + ImageVector.vectorResource(R.drawable.ic_baseline_content_copy_24), + context.getString(R.string.action_copy) + ) + } + ) + }, modifier = Modifier .fillMaxWidth() .padding(bottom = 5.dp) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt index 6779796..1beddc9 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/MainView.kt @@ -132,7 +132,10 @@ fun AccountMainView( customTab = mainViewModel.appSettings.value.openLinkInsideApp ) }, - onClearLogin = { }, + onClearLogin = { + // Just logout and this will clear all this session. + mainViewModel.accountSession.logout() + }, onSubmit = { username, password, rememberLogin -> run { CoroutineScope(Dispatchers.IO).launch { @@ -142,33 +145,61 @@ fun AccountMainView( true, null, null ) } } - mainViewModel.accountSession.login( - accountAuth = AccountAuth( - username = username, - password = password, - rememberLogin = rememberLogin - ), - onCompleted = {loggedIn -> - when (loggedIn) { - true -> { - loginDialogEnabled.value = true - loginDialogVisible.value = false - mainViewModel.accountSession.reLogin() - onShowSnackBar?.let { it( - context.getString(R.string.account_login_successful), - true, null, null - ) } + // If previous login has failed, second chance to login + if (state == ProcessState.Failed) { + mainViewModel.accountSession.login( + onCompleted = { loggedIn -> + when (loggedIn) { + true -> { + loginDialogEnabled.value = true + loginDialogVisible.value = false + mainViewModel.accountSession.reLogin() + onShowSnackBar?.let { it( + context.getString(R.string.account_login_successful), + true, null, null + ) } + } + false -> { + loginDialogEnabled.value = true + onShowSnackBar?.let { it( + context.getString(R.string.account_login_failed), + true, null, null + ) } + } } - false -> { - loginDialogEnabled.value = true - onShowSnackBar?.let { it( - context.getString(R.string.account_login_failed), - true, null, null - ) } + } + ) + } + // New login + else { + mainViewModel.accountSession.login( + accountAuth = AccountAuth( + username = username, + password = password, + rememberLogin = rememberLogin + ), + onCompleted = { loggedIn -> + when (loggedIn) { + true -> { + loginDialogEnabled.value = true + loginDialogVisible.value = false + mainViewModel.accountSession.reLogin() + onShowSnackBar?.let { it( + context.getString(R.string.account_login_successful), + true, null, null + ) } + } + false -> { + loginDialogEnabled.value = true + onShowSnackBar?.let { it( + context.getString(R.string.account_login_failed), + true, null, null + ) } + } } } - } - ) + ) + } } } ) diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectFee.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectFee.kt index d3626ee..6ddfaf6 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectFee.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectFee.kt @@ -1,6 +1,7 @@ package io.zoemeow.dutschedule.ui.view.account import android.app.Activity.RESULT_OK +import android.content.Context import androidx.activity.ComponentActivity import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -34,6 +35,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import io.zoemeow.dutschedule.R import io.zoemeow.dutschedule.activity.AccountActivity import io.zoemeow.dutschedule.model.ProcessState import io.zoemeow.dutschedule.ui.component.account.AccountSubjectFeeInformation @@ -41,6 +43,7 @@ import io.zoemeow.dutschedule.ui.component.account.AccountSubjectFeeInformation @OptIn(ExperimentalMaterial3Api::class) @Composable fun AccountActivity.SubjectFee( + context: Context, snackBarHostState: SnackbarHostState, containerColor: Color, contentColor: Color @@ -55,7 +58,7 @@ fun AccountActivity.SubjectFee( contentAlignment = Alignment.BottomCenter, content = { TopAppBar( - title = { Text("Subject fee") }, + title = { Text(context.getString(R.string.account_subjectfee_title)) }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), navigationIcon = { IconButton( @@ -66,7 +69,7 @@ fun AccountActivity.SubjectFee( content = { Icon( Icons.AutoMirrored.Filled.ArrowBack, - "", + context.getString(R.string.action_back), modifier = Modifier.size(25.dp) ) } @@ -86,7 +89,7 @@ fun AccountActivity.SubjectFee( getMainViewModel().accountSession.fetchSubjectFee(force = true) }, content = { - Icon(Icons.Default.Refresh, "Refresh") + Icon(Icons.Default.Refresh, context.getString(R.string.action_refresh)) } ) } @@ -104,7 +107,7 @@ fun AccountActivity.SubjectFee( .padding(vertical = 2.dp), horizontalAlignment = Alignment.CenterHorizontally, content = { - Text(getMainViewModel().appSettings.value.currentSchoolYear.toString()) + Text(getMainViewModel().appSettings.value.currentSchoolYear.composeToString()) } ) LazyColumn( @@ -115,6 +118,15 @@ fun AccountActivity.SubjectFee( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Top, content = { + item { + if (getMainViewModel().accountSession.subjectFee.data.size > 0) { + Text(context.getString( + R.string.account_subjectfee_summary_main, + getMainViewModel().accountSession.subjectFee.data.sumOf { it.credit }, + getMainViewModel().accountSession.subjectFee.data.sumOf { it.price } + )) + } + } items(getMainViewModel().accountSession.subjectFee.data) { item -> AccountSubjectFeeInformation( modifier = Modifier.padding(bottom = 10.dp), diff --git a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt index 29259a3..eeb5c44 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/ui/view/account/SubjectInformation.kt @@ -113,7 +113,7 @@ fun AccountActivity.SubjectInformation( .padding(vertical = 2.dp), horizontalAlignment = Alignment.CenterHorizontally, content = { - Text(getMainViewModel().appSettings.value.currentSchoolYear.toString()) + Text(getMainViewModel().appSettings.value.currentSchoolYear.composeToString()) } ) LazyColumn( diff --git a/app/src/main/java/io/zoemeow/dutschedule/utils/CustomDateUtil.kt b/app/src/main/java/io/zoemeow/dutschedule/utils/CustomDateUtil.kt index c106054..18a2994 100644 --- a/app/src/main/java/io/zoemeow/dutschedule/utils/CustomDateUtil.kt +++ b/app/src/main/java/io/zoemeow/dutschedule/utils/CustomDateUtil.kt @@ -28,29 +28,30 @@ class CustomDateUtil { } fun dayOfWeekInString( + context: Context, value: Int = 1, fullString: Boolean = false ): String { return if (fullString) { when (value) { - 1 -> "Sunday" - 2 -> "Monday" - 3 -> "Tuesday" - 4 -> "Wednesday" - 5 -> "Thursday" - 6 -> "Friday" - 7 -> "Saturday" + 1 -> context.getString(R.string.date_dow_8) + 2 -> context.getString(R.string.date_dow_2) + 3 -> context.getString(R.string.date_dow_3) + 4 -> context.getString(R.string.date_dow_4) + 5 -> context.getString(R.string.date_dow_5) + 6 -> context.getString(R.string.date_dow_6) + 7 -> context.getString(R.string.date_dow_7) else -> throw Exception("Invalid value: Must between 1 and 7!") } } else { when (value) { - 1 -> "Sun" - 2 -> "Mon" - 3 -> "Tue" - 4 -> "Wed" - 5 -> "Thu" - 6 -> "Fri" - 7 -> "Sat" + 1 -> context.getString(R.string.date_dow_8_short) + 2 -> context.getString(R.string.date_dow_2_short) + 3 -> context.getString(R.string.date_dow_3_short) + 4 -> context.getString(R.string.date_dow_4_short) + 5 -> context.getString(R.string.date_dow_5_short) + 6 -> context.getString(R.string.date_dow_6_short) + 7 -> context.getString(R.string.date_dow_7_short) else -> throw Exception("Invalid value: Must between 1 and 7!") } } @@ -69,10 +70,10 @@ class CustomDateUtil { return when (duration.inWholeHours) { in 0..23 -> { - context.getString(R.string.time_today) + context.getString(R.string.date_duration_today) } in 24..47 -> { - context.getString(R.string.time_yesterday) + context.getString(R.string.date_duration_yesterday) } else -> { val localeByLangTag = Locale.forLanguageTag(langTag) diff --git a/app/src/main/res/drawable/ic_baseline_content_copy_24.xml b/app/src/main/res/drawable/ic_baseline_content_copy_24.xml new file mode 100644 index 0000000..942aeb9 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_content_copy_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 07046e8..715c7d6 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -4,12 +4,10 @@ Tính năng này đang được phát triển. Hãy kiểm tra lại sau. Bạn cần phải cho phép Quyền truy cập mọi tệp trong quyền hạn ứng dụng để sử dụng tùy chọn này. Bạn cần phải bật quyền Chuông báo và lời nhắc trong cài đặt Android để sử dụng tính năng này. - - Hôm nay - Hôm qua OK Mở + Sao chép Lưu Huỷ bỏ Quay lại @@ -26,6 +24,23 @@ Đã chọn Thông tin + + Thứ hai + Thứ ba + Thứ tư + Thứ năm + Thứ sáu + Thứ bảy + Chủ nhật + T2 + T3 + T4 + T5 + T6 + T7 + CN + Hôm nay + Hôm qua Chưa đăng nhập Đang tải… @@ -221,7 +236,7 @@ Xóa đăng nhập cũ Bạn đã quên mật khẩu? Đang trong tiến trình… - Bạn đã đăng nhập trước đây. Nhấn vào nút \"Đăng nhập\" để thử lại. Nếu bạn đã thay đổi mật khẩu trước đây, nhấp vào \"Xóa đăng nhập cũ\". + Bạn đã đăng nhập trước đây. Nhấn vào nút \"Đăng nhập\" để thử lại. Nếu bạn đã thay đổi mật khẩu hoặc muốn đăng nhập tài khoản khác, nhấp vào \"Xóa đăng nhập cũ\". Đang đăng nhâp cho bạn… Đã đăng nhập thành công! Chúng tôi không thể đăng nhập với tài khoản của bạn! Nếu bạn đã nhập đúng và sự cố này vẫn tồn tại, hãy mở một issue mới trên GitHub. @@ -235,8 +250,13 @@ Bạn có chắc chắn muốn đăng xuất?\n\Lưu ý rằng:\n- Bạn sẽ không thể nhận bất kì thông báo liên quan đến lịch học của bạn.\n- Bộ lọc tin tức của bạn sẽ không bị ảnh hưởng. Đăng xuất Đã đăng xuất! + + Năm học: 20%1$d-20%2$d - Học kỳ %3$d %4$s + (học kỳ hè) Thông tin học phần + %1$s\n\nLịch học:\n%2$s + - %1$s - Tiết %2$d-%3$d - Phòng %4$s Đã thêm %1$s vào danh sách bộ lọc của bạn! Học phần này đã có trong danh sách bộ lọc của bạn! Thêm vào bộ lọc tin tức @@ -253,6 +273,32 @@ Ngày thi: %1$s Phòng: %1$s Hiện tại không có lịch thi cho môn học này. + + Học phí + %1$d tín chỉ - %2$.0f%3$s + %1$d tín chỉ - %2$.0f%3$s + Số tín chỉ: %1$d - Số tiền: %2$.0f VND + Đã thanh toán + Chưa thanh toán + + Thông tin tài khoản + Để sao chép, hãy nhấn và giữ trường văn bản, chọn \"Chọn tất cả\" và nhấn \"Sao chép\".\nNếu bạn muốn thay đổi thông tin nào đó, bạn cần thay đổi nó trong trang web sinh viên của trường. + Tên + Ngày sinh + Nơi sinh + Giới tính + Số chứng minh nhân dân + Nơi cấp và ngày cấp của CMND + Số căn cước công dân + Ngày cấp CCCD + Số thẻ ngân hàng + Email cá nhân + Số điện thoại + Lớp + Chuyên ngành + Chương trình đào tạo + Email trường cấp + Đã sao chép vào khay nhớ tạm! Kết quả rèn luyện diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d6184a1..b741df7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,12 +4,10 @@ This function is in development. Check back soon. You need to grant All files access in application permissions to use this option. You need to enable Alarms & Reminders in Android app settings to use this feature. - - Today - Yesterday OK Open + Copy Save Cancel Back @@ -26,6 +24,23 @@ Selected Information + + Monday + Tuesday + Wednesday + Thursday + Friday + Saturday + Sunday + Mon + Tue + Wed + Thu + Fri + Sat + Sun + Today + Yesterday Not logged in Fetching… @@ -221,7 +236,7 @@ Clear previous login Forgot your password? Processing… - You have logged in before. Click \"Login\" button to try again. If you have changed your password before, click \"Clear previous login\". + You have logged in before. Click \"Login\" button to try again. If you have changed your password or want to login another account, click \"Clear previous login\". Logging you in… Successfully logged in! We can\'t log in with your account! If you entered carefully and this issue is still persists, feel free to open issue on GitHub. @@ -235,8 +250,13 @@ Are you sure you want to logout?\n\nNote that:\n- You won\'t be received your any subject schedule anymore.\n- Your news filter settings won\'t be affected. Logout Successfully logout! + + School year: 20%1$d-20%2$d - Semester %3$d %4$s + (summer semester) Subject Information + %1$s\n\nSchedule:\n%2$s + - %1$s - Lesson %2$d-%3$d - Room %4$s Successfully added %1$s to your news filter list! This subject has already exist in your news filter list! Add to news filter @@ -253,8 +273,35 @@ Exam date: %1$s Room: %1$s Currently no examination schedule yet for this subject. + + Subject Fee + %1$d credit - %2$.0f%3$s + %1$d credits - %2$.0f%3$s + Credit(s): %1$d - Total price: %2$.0f VND + Completed + Not completed yet + + Account Information + To copy, click and hold a text field, select \"Select all\" and click \"Copy\".\nIf you want to edit any information below, you need do it in DUT Information System web. + Name + Date of birth + Place of birth + Gender + National card ID + National card issue place and date + Citizen card ID + Citizen issue date + Bank card ID + Personal email + Phone number + Class + Specialization + Training program plan + School email + Copied to clipboard! Training Result (unknown) + \ No newline at end of file