diff --git a/.circleci/config.yml b/.circleci/config.yml index ce2922ba3..2cb2e1473 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -162,7 +162,7 @@ jobs: openssl aes-256-cbc -d -in ./app/upload-key-encrypted.jks -k $ENCRYPT_KEY >> ./app/upload-key.jks - run: name: Publish release - command: ./gradlew publishPlayRelease --no-daemon --stacktrace --console=plain -PenableCrashlytics -PdisablePreDex + command: ./gradlew publishPlayRelease --no-daemon --stacktrace --console=plain -PdisablePreDex workflows: version: 2 diff --git a/.github/workflows/deploy-store.yml b/.github/workflows/deploy-store.yml index e8237a381..0195f3e56 100644 --- a/.github/workflows/deploy-store.yml +++ b/.github/workflows/deploy-store.yml @@ -40,7 +40,7 @@ jobs: SINGLE_SUPPORT_AD_ID: ${{ secrets.SINGLE_SUPPORT_AD_ID }} DASHBOARD_TILE_AD_ID: ${{ secrets.DASHBOARD_TILE_AD_ID }} SET_BUILD_TIMESTAMP: ${{ secrets.SET_BUILD_TIMESTAMP }} - run: ./gradlew publishPlayReleaseApps -PenableFirebase --stacktrace; + run: ./gradlew publishPlayReleaseApps --stacktrace; deploy-app-gallery: name: AppGallery diff --git a/.github/workflows/deploy-test.yml b/.github/workflows/deploy-test.yml index c4f55e6af..42c1f8e7a 100644 --- a/.github/workflows/deploy-test.yml +++ b/.github/workflows/deploy-test.yml @@ -36,8 +36,7 @@ jobs: - name: Prepare build configuration run: | sed -i -e "s#applicationIdSuffix \".dev\"#applicationIdSuffix \".${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/build.gradle - sed -i -e "s#.dev\"#.${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/src/debug/google-services.json - sed -i -e "s#.dev\"#.${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/src/debug/agconnect-services.json + sed -i -e "s#.dev\"#.${GITHUB_HEAD_REF//[-.\/]/_}\"#" app/google-services.json sed -i -e '/versionNameSuffix/d' app/build.gradle - name: Add signing config run: | @@ -131,7 +130,7 @@ jobs: BITRISE_KEYSTORE_PASSWORD: ${{ secrets.BITRISE_KEYSTORE_PASSWORD }} BITRISE_KEY_ALIAS: ${{ secrets.BITRISE_KEY_ALIAS }} BITRISE_KEY_PASSWORD: ${{ secrets.BITRISE_KEY_PASSWORD }} - run: ./gradlew assemblePlayDebug -PenableFirebase --stacktrace + run: ./gradlew assemblePlayDebug --stacktrace - name: Upload apk to github artifacts uses: actions/upload-artifact@v3 with: diff --git a/.gitignore b/.gitignore index 69d3ad5d2..ad83ced8d 100644 --- a/.gitignore +++ b/.gitignore @@ -117,12 +117,13 @@ Thumbs.db *.ear ### AndroidStudio Patch ### - !/gradle/wrapper/gradle-wrapper.jar .idea/jarRepositories.xml +### Services config files +agconnect-services.json +agconnect-credentials.json +google-services.json +!app/google-services.json + -app/src/release/agconnect-services.json -app/src/release/agconnect-credentials.json -.idea/deploymentTargetDropDown.xml -.idea/kotlinc.xml diff --git a/.travis.yml b/.travis.yml index 04db3a616..e0b0be978 100644 --- a/.travis.yml +++ b/.travis.yml @@ -61,7 +61,7 @@ script: gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/agconnect-services.json.gpg; gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/key.p12.gpg; gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg; - ./gradlew publishPlayRelease -PenableFirebase --stacktrace; + ./gradlew publishPlayRelease --stacktrace; fi after_success: diff --git a/app/build.gradle b/app/build.gradle index 7a2f0ca57..245daab05 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,15 +27,12 @@ android { testApplicationId "io.github.tests.wulkanowy" minSdkVersion 21 targetSdkVersion 34 - versionCode 157 - versionName "2.5.8" + versionCode 160 + versionName "2.6.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" resValue "string", "app_name", "Wulkanowy" - manifestPlaceholders = [ - firebase_enabled: project.hasProperty("enableFirebase"), - admob_project_id: "" - ] + manifestPlaceholders = [admob_project_id: ""] buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null" buildConfigField "String", "DASHBOARD_TILE_AD_ID", "null" @@ -76,7 +73,6 @@ android { resValue "string", "app_name", "Wulkanowy DEV" applicationIdSuffix ".dev" versionNameSuffix "-dev" - ext.enableCrashlytics = project.hasProperty("enableFirebase") buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\"" buildConfigField "String", "SCHOOLS_BASE_URL", '"https://schools.wulkanowy.net.pl"' } @@ -164,8 +160,8 @@ play { defaultToAppBundles = false track = 'production' releaseStatus = ReleaseStatus.IN_PROGRESS - userFraction = 0.99d - updatePriority = 4 + userFraction = 0.25d + updatePriority = 1 enabled.set(false) } @@ -195,16 +191,16 @@ ext { } dependencies { - implementation 'io.github.wulkanowy:sdk:2.5.8' + implementation 'io.github.wulkanowy:sdk:2.6.0' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" - implementation 'androidx.core:core-ktx:1.12.0' + implementation 'androidx.core:core-ktx:1.13.0' implementation 'androidx.core:core-splashscreen:1.0.1' - implementation "androidx.activity:activity-ktx:1.8.2" + implementation "androidx.activity:activity-ktx:1.9.0" implementation "androidx.appcompat:appcompat:1.6.1" implementation "androidx.fragment:fragment-ktx:1.6.2" implementation "androidx.annotation:annotation:1.7.1" @@ -237,7 +233,7 @@ dependencies { implementation 'com.github.ncapdevi:FragNav:3.3.0' implementation "com.github.YarikSOffice:lingver:1.3.0" - implementation 'com.squareup.retrofit2:retrofit:2.9.0' + implementation 'com.squareup.retrofit2:retrofit:2.11.0' implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0" implementation "com.squareup.okhttp3:logging-interceptor:4.12.0" implementation "com.squareup.okhttp3:okhttp-urlconnection:4.12.0" @@ -250,15 +246,15 @@ dependencies { implementation "io.github.wulkanowy:AppKillerManager:3.0.1" implementation 'me.xdrop:fuzzywuzzy:1.4.0' implementation 'com.fredporciuncula:flow-preferences:1.9.1' - implementation 'org.apache.commons:commons-text:1.11.0' + implementation 'org.apache.commons:commons-text:1.12.0' - playImplementation platform('com.google.firebase:firebase-bom:32.7.3') + playImplementation platform('com.google.firebase:firebase-bom:32.8.1') playImplementation 'com.google.firebase:firebase-analytics' playImplementation 'com.google.firebase:firebase-messaging' playImplementation 'com.google.firebase:firebase-crashlytics:' playImplementation 'com.google.firebase:firebase-config' - playImplementation 'com.google.android.gms:play-services-ads:22.6.0' + playImplementation 'com.google.android.gms:play-services-ads:23.0.0' playImplementation "com.google.android.play:integrity:1.3.0" playImplementation 'com.google.android.play:app-update-ktx:2.1.0' playImplementation 'com.google.android.play:review-ktx:2.0.1' @@ -278,7 +274,7 @@ dependencies { testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines" testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" - testImplementation 'org.robolectric:robolectric:4.11.1' + testImplementation 'org.robolectric:robolectric:4.12.1' testImplementation "androidx.test:runner:1.5.2" testImplementation "androidx.test.ext:junit:1.1.5" testImplementation "androidx.test:core:1.5.0" diff --git a/app/src/debug/google-services.json b/app/google-services.json similarity index 100% rename from app/src/debug/google-services.json rename to app/google-services.json diff --git a/app/play-publish-lint.sh b/app/play-publish-lint.sh index d3354b1ad..5f0391de3 100755 --- a/app/play-publish-lint.sh +++ b/app/play-publish-lint.sh @@ -1,7 +1,8 @@ #!/bin/bash - content=$(cat < "app/src/main/play/release-notes/pl-PL/default.txt") || exit -if [[ "${#content}" -gt 500 ]]; then +content2=echo "$content" | dos2unix +if [[ "${#content2}" -gt 500 ]]; then echo >&2 "Release notes content has reached the limit of 500 characters" exit 1 fi diff --git a/app/src/debug/agconnect-services.json b/app/src/debug/agconnect-services.json deleted file mode 100644 index 52426f54e..000000000 --- a/app/src/debug/agconnect-services.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "agcgw": { - "backurl": "connect-dre.hispace.hicloud.com", - "url": "connect-dre.dbankcloud.cn", - "websocketbackurl": "connect-ws-dre.hispace.dbankcloud.com", - "websocketurl": "connect-ws-dre.hispace.dbankcloud.cn" - }, - "agcgw_all": { - "CN": "connect-drcn.dbankcloud.cn", - "CN_back": "connect-drcn.hispace.hicloud.com", - "DE": "connect-dre.dbankcloud.cn", - "DE_back": "connect-dre.hispace.hicloud.com", - "RU": "connect-drru.hispace.dbankcloud.ru", - "RU_back": "connect-drru.hispace.dbankcloud.cn", - "SG": "connect-dra.dbankcloud.cn", - "SG_back": "connect-dra.hispace.hicloud.com" - }, - "websocketgw_all": { - "CN": "connect-ws-drcn.hispace.dbankcloud.cn", - "CN_back": "connect-ws-drcn.hispace.dbankcloud.com", - "DE": "connect-ws-dre.hispace.dbankcloud.cn", - "DE_back": "connect-ws-dre.hispace.dbankcloud.com", - "RU": "connect-ws-drru.hispace.dbankcloud.ru", - "RU_back": "connect-ws-drru.hispace.dbankcloud.cn", - "SG": "connect-ws-dra.hispace.dbankcloud.cn", - "SG_back": "connect-ws-dra.hispace.dbankcloud.com" - }, - "client": { - "cp_id": "890048000024105546", - "product_id": "736430079244736562", - "client_id": "514530959291319360", - "client_secret": "C42522DBF17D3D4BBE9D9C1783A54484B7E6844B388B7A67502D36A633A4186B", - "project_id": "736430079244736562", - "app_id": "106552551", - "api_key": "CgB6e3x9BUNiq+r8ebCHNojjjYsMT4pJSjjNDOkm9owtBb6rVI6LjnASoZBRxbjjhObcrV5gANo99fI/eKZDTbWS", - "package_name": "io.github.wulkanowy.dev" - }, - "oauth_client": { - "client_id": "106552551", - "client_type": 1 - }, - "app_info": { - "app_id": "106552551", - "package_name": "io.github.wulkanowy.dev" - }, - "service": { - "analytics": { - "collector_url": "datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn", - "collector_url_ru": "datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com", - "collector_url_sg": "datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn", - "collector_url_de": "datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn", - "collector_url_cn": "datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn", - "resource_id": "p1", - "channel_id": "" - }, - "search":{ - "url":"https://search-dre.cloud.huawei.com" - }, - "cloudstorage": { - "storage_url_sg_back": "https://agc-storage-dra.cloud.huawei.asia", - "storage_url_ru_back": "https://agc-storage-drru.cloud.huawei.ru", - "storage_url_ru": "https://agc-storage-drru.cloud.huawei.ru", - "storage_url_de_back": "https://agc-storage-dre.cloud.huawei.eu", - "storage_url_de": "https://ops-dre.agcstorage.link", - "storage_url": "https://agc-storage-drcn.platform.dbankcloud.cn", - "storage_url_sg": "https://ops-dra.agcstorage.link", - "storage_url_cn_back": "https://agc-storage-drcn.cloud.huawei.com.cn", - "storage_url_cn": "https://agc-storage-drcn.platform.dbankcloud.cn" - }, - "ml": { - "mlservice_url": "ml-api-dre.ai.dbankcloud.com,ml-api-dre.ai.dbankcloud.cn" - } - }, - "region": "DE", - "configuration_version": "3.0", - "appInfos": [ - { - "package_name": "io.github.wulkanowy.dev", - "client": { - "app_id": "106552551" - }, - "app_info": { - "package_name": "io.github.wulkanowy.dev", - "app_id": "106552551" - }, - "oauth_client": { - "client_type": 1, - "client_id": "106552551" - } - } - ] -} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f43dfdd2c..79d75bc01 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -51,7 +51,7 @@ android:exported="true" android:screenOrientation="portrait" android:theme="@style/WulkanowyTheme.SplashScreen" - tools:ignore="LockedOrientationActivity"> + tools:ignore="DiscouragedApi,LockedOrientationActivity"> @@ -155,33 +155,9 @@ android:resource="@xml/provider_paths" /> - - - - - - - - - diff --git a/app/src/main/java/io/github/wulkanowy/data/Resource.kt b/app/src/main/java/io/github/wulkanowy/data/Resource.kt index 108b0d58e..712a946f3 100644 --- a/app/src/main/java/io/github/wulkanowy/data/Resource.kt +++ b/app/src/main/java/io/github/wulkanowy/data/Resource.kt @@ -1,11 +1,17 @@ package io.github.wulkanowy.data +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map @@ -14,16 +20,39 @@ import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import timber.log.Timber +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds -sealed class Resource { - - open class Loading : Resource() +sealed interface Resource { + /** + * The initial value of a resource flow. Indicates no data that is currently available to be shown, + * however with the expectation that the state will transition to another one soon. + */ + open class Loading : Resource + /** + * A semi-loading state with some data available to be displayed (usually cached data loaded from + * the database). Still not the target state and it's expected to transition into another one soon. + */ data class Intermediate(val data: T) : Loading() - data class Success(val data: T) : Resource() + /** + * The happy-path target state. Data can either be: + * - loaded from the database - while it may seem like this case is already handled by the + * Intermediate state, the difference here is semantic. Cached data is returned as Intermediate + * when there's a API request in progress (or soon expected to be), however when there is no + * intention of immediately querying the API, the cached data is returned as a Success. + * - fetched from the API. + */ + data class Success(val data: T) : Resource - data class Error(val error: Throwable) : Resource() + /** + * Something bad happened and we were unable to get the requested data. This can be caused by + * a database error, a network error, or really just any other error. Upon receiving this state + * the UI can either: display a full screen error, or, when it has received any data previously, + * display a snack bar informing of the problem. + */ + data class Error(val error: Throwable) : Resource } val Resource.dataOrNull: T? @@ -64,6 +93,22 @@ fun Resource.mapData(block: (T) -> U) = when (this) { is Resource.Error -> Resource.Error(this.error) } +/** + * Injects another flow into this flow's resource data. + */ +inline fun Flow>.combineWithResourceData( + flow: Flow, + crossinline block: suspend (T1, T2) -> R +): Flow> = + combine(flow) { resource, inject -> + when (resource) { + is Resource.Success -> Resource.Success(block(resource.data, inject)) + is Resource.Intermediate -> Resource.Intermediate(block(resource.data, inject)) + is Resource.Loading -> Resource.Loading() + is Resource.Error -> Resource.Error(resource.error) + } + } + fun Flow>.logResourceStatus(name: String, showData: Boolean = false) = onEach { val description = when (it) { is Resource.Intermediate -> "intermediate data received" + if (showData) " (data: `${it.data}`)" else "" @@ -74,8 +119,29 @@ fun Flow>.logResourceStatus(name: String, showData: Boolean = fa Timber.i("$name: $description") } -fun Flow>.mapResourceData(block: (T) -> U) = map { - it.mapData(block) +inline fun Flow>.mapResourceData(crossinline block: suspend (T) -> U) = map { + when (it) { + is Resource.Success -> Resource.Success(block(it.data)) + is Resource.Intermediate -> Resource.Intermediate(block(it.data)) + is Resource.Loading -> Resource.Loading() + is Resource.Error -> Resource.Error(it.error) + } +} + +@OptIn(ExperimentalCoroutinesApi::class) +fun Flow>.flatMapResourceData( + inheritIntermediate: Boolean = true, block: suspend (T) -> Flow> +) = flatMapLatest { + when (it) { + is Resource.Success -> block(it.data) + is Resource.Intermediate -> block(it.data).map { newRes -> + if (inheritIntermediate && newRes is Resource.Success) Resource.Intermediate(newRes.data) + else newRes + } + + is Resource.Loading -> flowOf(Resource.Loading()) + is Resource.Error -> flowOf(Resource.Error(it.error)) + } } fun Flow>.onResourceData(block: suspend (T) -> Unit) = onEach { @@ -105,13 +171,13 @@ fun Flow>.onResourceSuccess(block: suspend (T) -> Unit) = onEach } } -fun Flow>.onResourceError(block: (Throwable) -> Unit) = onEach { +fun Flow>.onResourceError(block: suspend (Throwable) -> Unit) = onEach { if (it is Resource.Error) { block(it.error) } } -fun Flow>.onResourceNotLoading(block: () -> Unit) = onEach { +fun Flow>.onResourceNotLoading(block: suspend () -> Unit) = onEach { if (it !is Resource.Loading) { block() } @@ -121,70 +187,99 @@ suspend fun Flow>.toFirstResult() = filter { it !is Resource.Loa suspend fun Flow>.waitForResult() = takeWhile { it is Resource.Loading }.collect() -inline fun networkBoundResource( - mutex: Mutex = Mutex(), - showSavedOnLoading: Boolean = true, - crossinline isResultEmpty: (ResultType) -> Boolean, - crossinline query: () -> Flow, - crossinline fetch: suspend (ResultType) -> RequestType, - crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit, - crossinline onFetchFailed: (Throwable) -> Unit = { }, - crossinline shouldFetch: (ResultType) -> Boolean = { true }, - crossinline filterResult: (ResultType) -> ResultType = { it } -) = flow { - emit(Resource.Loading()) +// Can cause excessive amounts of `Resource.Intermediate` to be emitted. Unless that is desired, +// use `debounceIntermediates` to alleviate this behavior. +inline fun combineResourceFlows(flows: Iterable>>): Flow>> = + combine(flows) { items -> + var isIntermediate = false + val data = mutableListOf() + for (item in items) { + when (item) { + is Resource.Success -> data.add(item.data) + is Resource.Intermediate -> { + isIntermediate = true + data.add(item.data) + } - val data = query().first() - emitAll(if (shouldFetch(data)) { - val filteredResult = filterResult(data) - - if (showSavedOnLoading && !isResultEmpty(filteredResult)) { - emit(Resource.Intermediate(filteredResult)) + is Resource.Loading -> return@combine Resource.Loading() + is Resource.Error -> continue + } } - - try { - val newData = fetch(data) - mutex.withLock { saveFetchResult(query().first(), newData) } - query().map { Resource.Success(filterResult(it)) } - } catch (throwable: Throwable) { - onFetchFailed(throwable) - flowOf(Resource.Error(throwable)) + if (data.isEmpty()) { + // All items have to be errors for this to happen, so just return the first one. + // mapData is functionally useless and exists only to satisfy the type checker + items.first().mapData { listOf(it) } + } else if (isIntermediate) { + Resource.Intermediate(data) + } else { + Resource.Success(data) + } + } + +@OptIn(FlowPreview::class) +fun Flow>.debounceIntermediates(timeout: Duration = 5.seconds) = flow { + var wasIntermediate = false + + emitAll(this@debounceIntermediates.debounce { + if (it is Resource.Intermediate) { + if (!wasIntermediate) { + wasIntermediate = true + Duration.ZERO + } else { + timeout + } + } else { + wasIntermediate = false + Duration.ZERO } - } else { - query().map { Resource.Success(filterResult(it)) } }) } + +inline fun networkBoundResource( + mutex: Mutex = Mutex(), + crossinline isResultEmpty: (OutputType) -> Boolean, + crossinline query: () -> Flow, + crossinline fetch: suspend () -> ApiType, + crossinline saveFetchResult: suspend (old: OutputType, new: ApiType) -> Unit, + crossinline shouldFetch: (OutputType) -> Boolean = { true }, + crossinline filterResult: (OutputType) -> OutputType = { it } +) = networkBoundResource( + mutex = mutex, + isResultEmpty = isResultEmpty, + query = query, + fetch = fetch, + saveFetchResult = saveFetchResult, + shouldFetch = shouldFetch, + mapResult = filterResult +) + @JvmName("networkBoundResourceWithMap") -inline fun networkBoundResource( +inline fun networkBoundResource( mutex: Mutex = Mutex(), - showSavedOnLoading: Boolean = true, - crossinline isResultEmpty: (T) -> Boolean, - crossinline query: () -> Flow, - crossinline fetch: suspend (ResultType) -> RequestType, - crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit, - crossinline onFetchFailed: (Throwable) -> Unit = { }, - crossinline shouldFetch: (ResultType) -> Boolean = { true }, - crossinline mapResult: (ResultType) -> T, + crossinline isResultEmpty: (OutputType) -> Boolean, + crossinline query: () -> Flow, + crossinline fetch: suspend () -> ApiType, + crossinline saveFetchResult: suspend (old: DatabaseType, new: ApiType) -> Unit, + crossinline shouldFetch: (DatabaseType) -> Boolean = { true }, + crossinline mapResult: (DatabaseType) -> OutputType, ) = flow { emit(Resource.Loading()) val data = query().first() - emitAll(if (shouldFetch(data)) { - val mappedResult = mapResult(data) + if (shouldFetch(data)) { + emit(Resource.Intermediate(data)) - if (showSavedOnLoading && !isResultEmpty(mappedResult)) { - emit(Resource.Intermediate(mappedResult)) - } try { - val newData = fetch(data) + val newData = fetch() mutex.withLock { saveFetchResult(query().first(), newData) } - query().map { Resource.Success(mapResult(it)) } } catch (throwable: Throwable) { - onFetchFailed(throwable) - flowOf(Resource.Error(throwable)) + emit(Resource.Error(throwable)) + return@flow } - } else { - query().map { Resource.Success(mapResult(it)) } - }) + } + + emitAll(query().map { Resource.Success(it) }) } + .mapResourceData { mapResult(it) } + .filterNot { it is Resource.Intermediate && isResultEmpty(it.data) } diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt index 0c8f1a5d1..a8604c5c1 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/AdminMessage.kt @@ -4,6 +4,8 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import io.github.wulkanowy.data.enums.MessageType +import io.github.wulkanowy.data.serializers.SafeMessageTypeEnumListSerializer +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable @@ -34,6 +36,8 @@ data class AdminMessage( val priority: String, + @SerialName("messageTypes") + @Serializable(with = SafeMessageTypeEnumListSerializer::class) @ColumnInfo(name = "types", defaultValue = "[]") val types: List = emptyList(), diff --git a/app/src/main/java/io/github/wulkanowy/data/enums/AttendanceCalculatorSortingMode.kt b/app/src/main/java/io/github/wulkanowy/data/enums/AttendanceCalculatorSortingMode.kt new file mode 100644 index 000000000..77dd5fc4b --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/enums/AttendanceCalculatorSortingMode.kt @@ -0,0 +1,13 @@ +package io.github.wulkanowy.data.enums + +enum class AttendanceCalculatorSortingMode(private val value: String) { + ALPHABETIC("alphabetic"), + ATTENDANCE("attendance_percentage"), + LESSON_BALANCE("lesson_balance"); + + companion object { + fun getByValue(value: String) = + AttendanceCalculatorSortingMode.values() + .find { it.value == value } ?: ALPHABETIC + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/enums/MessageType.kt b/app/src/main/java/io/github/wulkanowy/data/enums/MessageType.kt index 531684e4e..ecd8d916f 100644 --- a/app/src/main/java/io/github/wulkanowy/data/enums/MessageType.kt +++ b/app/src/main/java/io/github/wulkanowy/data/enums/MessageType.kt @@ -4,6 +4,8 @@ enum class MessageType { GENERAL_MESSAGE, DASHBOARD_MESSAGE, LOGIN_MESSAGE, + LOGIN_STUDENT_SELECT_MESSAGE, + LOGIN_SYMBOL_MESSAGE, PASS_RESET_MESSAGE, ERROR_OVERRIDE, } diff --git a/app/src/main/java/io/github/wulkanowy/data/enums/ShowAdditionalLessonsMode.kt b/app/src/main/java/io/github/wulkanowy/data/enums/ShowAdditionalLessonsMode.kt new file mode 100644 index 000000000..3e7cdef5b --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/enums/ShowAdditionalLessonsMode.kt @@ -0,0 +1,11 @@ +package io.github.wulkanowy.data.enums + +enum class ShowAdditionalLessonsMode(val value: String) { + NONE("none"), + INLINE("inline"), + BELOW("below"); + + companion object { + fun getByValue(value: String) = entries.find { it.value == value } ?: INLINE + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/pojos/AttendanceData.kt b/app/src/main/java/io/github/wulkanowy/data/pojos/AttendanceData.kt new file mode 100644 index 000000000..5810363c6 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/pojos/AttendanceData.kt @@ -0,0 +1,14 @@ +package io.github.wulkanowy.data.pojos + +data class AttendanceData( + val subjectName: String, + val lessonBalance: Int, + val presences: Int, + val absences: Int, +) { + val total: Int + get() = presences + absences + + val presencePercentage: Double + get() = if (total == 0) 0.0 else (presences.toDouble() / total) * 100 +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt index b831ee755..aa0022b08 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/AdminMessageRepository.kt @@ -6,6 +6,7 @@ import io.github.wulkanowy.data.db.dao.AdminMessageDao import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.networkBoundResource import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.sync.Mutex import javax.inject.Inject import javax.inject.Singleton @@ -28,6 +29,6 @@ class AdminMessageRepository @Inject constructor( saveFetchResult = { oldItems, newItems -> adminMessageDao.removeOldAndSaveNew(oldItems, newItems) }, - showSavedOnLoading = false, ) + .filterNot { it is Resource.Intermediate } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt index 3636cb51e..dfafe6c8a 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/LuckyNumberRepository.kt @@ -6,6 +6,8 @@ import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.mappers.mapToEntity import io.github.wulkanowy.data.networkBoundResource +import io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetProvider +import io.github.wulkanowy.utils.AppWidgetUpdater import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex @@ -18,6 +20,7 @@ import javax.inject.Singleton class LuckyNumberRepository @Inject constructor( private val luckyNumberDb: LuckyNumberDao, private val wulkanowySdkFactory: WulkanowySdkFactory, + private val appWidgetUpdater: AppWidgetUpdater, ) { private val saveFetchResultMutex = Mutex() @@ -26,6 +29,7 @@ class LuckyNumberRepository @Inject constructor( student: Student, forceRefresh: Boolean, notify: Boolean = false, + isFromAppWidget: Boolean = false ) = networkBoundResource( mutex = saveFetchResultMutex, isResultEmpty = { it == null }, @@ -44,6 +48,9 @@ class LuckyNumberRepository @Inject constructor( oldItems = listOfNotNull(oldLuckyNumber), newItems = listOf(newLuckyNumber.apply { if (notify) isNotified = false }), ) + if (!isFromAppWidget) { + appWidgetUpdater.updateAllAppWidgetsByProvider(LuckyNumberWidgetProvider::class) + } } } ) diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt index ede2a0fde..f91dc63e3 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MessageRepository.kt @@ -122,7 +122,7 @@ class MessageRepository @Inject constructor( fetch = { wulkanowySdkFactory.create(student) .getMessageDetails( - messageKey = it!!.message.messageGlobalKey, + messageKey = message.messageGlobalKey, markAsRead = message.unread && markAsRead, ) }, diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt index 64e60a60b..8082068c5 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/PreferencesRepository.kt @@ -10,9 +10,11 @@ import com.fredporciuncula.flow.preferences.Serializer import dagger.hilt.android.qualifiers.ApplicationContext import io.github.wulkanowy.R import io.github.wulkanowy.data.enums.AppTheme +import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode import io.github.wulkanowy.data.enums.GradeColorTheme import io.github.wulkanowy.data.enums.GradeExpandMode import io.github.wulkanowy.data.enums.GradeSortingMode +import io.github.wulkanowy.data.enums.ShowAdditionalLessonsMode import io.github.wulkanowy.data.enums.TimetableGapsMode import io.github.wulkanowy.data.enums.TimetableMode import io.github.wulkanowy.ui.modules.dashboard.DashboardItem @@ -41,6 +43,27 @@ class PreferencesRepository @Inject constructor( R.bool.pref_default_attendance_present ) + val targetAttendanceFlow: Flow + get() = flowSharedPref.getInt( + context.getString(R.string.pref_key_attendance_target), + context.resources.getInteger(R.integer.pref_default_attendance_target) + ).asFlow() + + val attendanceCalculatorSortingModeFlow: Flow + get() = flowSharedPref.getString( + context.getString(R.string.pref_key_attendance_calculator_sorting_mode), + context.resources.getString(R.string.pref_default_attendance_calculator_sorting_mode) + ).asFlow().map(AttendanceCalculatorSortingMode::getByValue) + + /** + * Subjects are empty when they don't have any attendances (total = 0, attendances = 0, absences = 0). + */ + val attendanceCalculatorShowEmptySubjects: Flow + get() = flowSharedPref.getBoolean( + context.getString(R.string.pref_key_attendance_calculator_show_empty_subjects), + context.resources.getBoolean(R.bool.pref_default_attendance_calculator_show_empty_subjects) + ).asFlow() + private val gradeAverageModePref: Preference get() = getObjectFlow( R.string.pref_key_grade_average_mode, @@ -191,6 +214,12 @@ class PreferencesRepository @Inject constructor( ) ) + val showAdditionalLessonsInPlan: ShowAdditionalLessonsMode + get() = getString( + R.string.pref_key_timetable_show_additional_lessons, + R.string.pref_default_timetable_show_additional_lessons + ).let { ShowAdditionalLessonsMode.getByValue(it) } + val gradeSortingMode: GradeSortingMode get() = GradeSortingMode.getByValue( getString( diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt index df47d7a63..575ca89f2 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/StudentRepository.kt @@ -199,7 +199,7 @@ class StudentRepository @Inject constructor( suspend fun refreshStudentAfterAuthorize(student: Student, semester: Semester) { val wulkanowySdk = wulkanowySdkFactory.create(student, semester) - val newCurrentApiStudent = runCatching { wulkanowySdk.getCurrentStudent() } + val newCurrentApiStudent = runCatching { wulkanowySdk.getCurrentStudent() } .onFailure { Timber.e(it, "Can't find student with id ${student.studentId}") } .getOrNull() ?: return diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt index 335789991..60c562e12 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/TimetableRepository.kt @@ -13,6 +13,8 @@ import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.data.pojos.TimetableFull import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper +import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider +import io.github.wulkanowy.utils.AppWidgetUpdater import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.monday @@ -26,6 +28,7 @@ import java.time.LocalDate import javax.inject.Inject import javax.inject.Singleton + @Singleton class TimetableRepository @Inject constructor( private val timetableDb: TimetableDao, @@ -34,6 +37,7 @@ class TimetableRepository @Inject constructor( private val wulkanowySdkFactory: WulkanowySdkFactory, private val schedulerHelper: TimetableNotificationSchedulerHelper, private val refreshHelper: AutoRefreshHelper, + private val appWidgetUpdater: AppWidgetUpdater, ) { private val saveFetchResultMutex = Mutex() @@ -52,7 +56,8 @@ class TimetableRepository @Inject constructor( forceRefresh: Boolean, refreshAdditional: Boolean = false, notify: Boolean = false, - timetableType: TimetableType = TimetableType.NORMAL + timetableType: TimetableType = TimetableType.NORMAL, + isFromAppWidget: Boolean = false ) = networkBoundResource( mutex = saveFetchResultMutex, isResultEmpty = { @@ -83,6 +88,9 @@ class TimetableRepository @Inject constructor( refreshDayHeaders(timetableOld.headers, timetableNew.headers) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end)) + if (!isFromAppWidget) { + appWidgetUpdater.updateAllAppWidgetsByProvider(TimetableWidgetProvider::class) + } }, filterResult = { (timetable, additional, headers) -> TimetableFull( diff --git a/app/src/main/java/io/github/wulkanowy/data/serializers/SafeMessageTypeEnumListSerializer.kt b/app/src/main/java/io/github/wulkanowy/data/serializers/SafeMessageTypeEnumListSerializer.kt new file mode 100644 index 000000000..a95eab807 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/serializers/SafeMessageTypeEnumListSerializer.kt @@ -0,0 +1,27 @@ +package io.github.wulkanowy.data.serializers + +import io.github.wulkanowy.data.enums.MessageType +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +@OptIn(ExperimentalSerializationApi::class) +object SafeMessageTypeEnumListSerializer : KSerializer> { + + private val serializer = ListSerializer(String.serializer()) + + override val descriptor = serializer.descriptor + + override fun serialize(encoder: Encoder, value: List) { + encoder.encodeNotNullMark() + serializer.serialize(encoder, value.map { it.name }) + } + + override fun deserialize(decoder: Decoder): List = + serializer.deserialize(decoder).mapNotNull { enumName -> + MessageType.entries.find { it.name == enumName } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/domain/attendance/GetAttendanceCalculatorDataUseCase.kt b/app/src/main/java/io/github/wulkanowy/domain/attendance/GetAttendanceCalculatorDataUseCase.kt new file mode 100644 index 000000000..294abd1be --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/domain/attendance/GetAttendanceCalculatorDataUseCase.kt @@ -0,0 +1,106 @@ +package io.github.wulkanowy.domain.attendance + +import io.github.wulkanowy.data.* +import io.github.wulkanowy.data.db.entities.AttendanceSummary +import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.db.entities.Student +import io.github.wulkanowy.data.db.entities.Subject +import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode +import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode.* +import io.github.wulkanowy.data.pojos.AttendanceData +import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository +import io.github.wulkanowy.data.repositories.PreferencesRepository +import io.github.wulkanowy.data.repositories.SubjectRepository +import io.github.wulkanowy.utils.allAbsences +import io.github.wulkanowy.utils.allPresences +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject +import kotlin.math.ceil +import kotlin.math.floor + +class GetAttendanceCalculatorDataUseCase @Inject constructor( + private val subjectRepository: SubjectRepository, + private val attendanceSummaryRepository: AttendanceSummaryRepository, + private val preferencesRepository: PreferencesRepository, +) { + + operator fun invoke( + student: Student, + semester: Semester, + forceRefresh: Boolean, + ): Flow>> = + subjectRepository.getSubjects(student, semester, forceRefresh) + .mapResourceData { subjects -> subjects.sortedBy(Subject::name) } + .combineWithResourceData(preferencesRepository.targetAttendanceFlow, ::Pair) + .flatMapResourceData { (subjects, targetFreq) -> + combineResourceFlows(subjects.map { subject -> + attendanceSummaryRepository.getAttendanceSummary( + student = student, + semester = semester, + subjectId = subject.realId, + forceRefresh = forceRefresh + ).mapResourceData { summaries -> + summaries.toAttendanceData(subject.name, targetFreq) + } + }) + // Every individual combined flow causes separate network requests to update data. + // When there is N child flows, they can cause up to N-1 items to be emitted. Since all + // requests are usually completed in less than 5s, there is no need to emit multiple + // intermediates that will be visible for barely any time. + .debounceIntermediates() + } + .combineWithResourceData(preferencesRepository.attendanceCalculatorShowEmptySubjects) { attendanceDataList, showEmptySubjects -> + attendanceDataList.filter { it.total != 0 || showEmptySubjects } + } + .combineWithResourceData(preferencesRepository.attendanceCalculatorSortingModeFlow, List::sortedBy) +} + +private fun List.toAttendanceData(subjectName: String, targetFreq: Int): AttendanceData { + val presences = sumOf { it.allPresences } + val absences = sumOf { it.allAbsences } + return AttendanceData( + subjectName = subjectName, + lessonBalance = calcLessonBalance( + targetFreq.toDouble() / 100, presences, absences + ), + presences = presences, + absences = absences, + ) +} + +private fun calcLessonBalance(targetFreq: Double, presences: Int, absences: Int): Int { + val total = presences + absences + // The `+ 1` is to avoid false positives in close cases. Eg.: + // target frequency 99%, 1 presence. Without the `+ 1` this would be reported shown as + // a positive balance of +1, however that is not actually true as skipping one class + // would make it so that the balance would actually be negative (-98). The `+ 1` + // fixes this and makes sure that in situations like these, it's not reporting incorrect + // balances + return when { + presences / (total + 1f) >= targetFreq -> calcMissingAbsences( + targetFreq, absences, presences + ) + presences / (total + 0f) < targetFreq -> -calcMissingPresences( + targetFreq, absences, presences + ) + else -> 0 + } +} + +private fun calcMissingPresences(targetFreq: Double, absences: Int, presences: Int) = + calcMinRequiredPresencesFor(targetFreq, absences) - presences + +private fun calcMinRequiredPresencesFor(targetFreq: Double, absences: Int) = + ceil((targetFreq / (1 - targetFreq)) * absences).toInt() + +private fun calcMissingAbsences(targetFreq: Double, absences: Int, presences: Int) = + calcMinRequiredAbsencesFor(targetFreq, presences) - absences + +private fun calcMinRequiredAbsencesFor(targetFreq: Double, presences: Int) = + floor((presences * (1 - targetFreq)) / targetFreq).toInt() + +private fun List.sortedBy(mode: AttendanceCalculatorSortingMode) = when (mode) { + ALPHABETIC -> sortedBy(AttendanceData::subjectName) + ATTENDANCE -> sortedByDescending(AttendanceData::presencePercentage) + LESSON_BALANCE -> sortedBy(AttendanceData::lessonBalance) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt index 6e842b4d7..07649e436 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceFragment.kt @@ -14,6 +14,7 @@ import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.databinding.DialogExcuseBinding import io.github.wulkanowy.databinding.FragmentAttendanceBinding import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.attendance.calculator.AttendanceCalculatorFragment import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView @@ -134,6 +135,7 @@ class AttendanceFragment : BaseFragment(R.layout.frag override fun onOptionsItemSelected(item: MenuItem): Boolean { return if (item.itemId == R.id.attendanceMenuSummary) presenter.onSummarySwitchSelected() + else if (item.itemId == R.id.attendanceMenuCalculator) presenter.onCalculatorSwitchSelected() else false } @@ -253,6 +255,10 @@ class AttendanceFragment : BaseFragment(R.layout.frag (activity as? MainActivity)?.pushView(AttendanceSummaryFragment.newInstance()) } + override fun openCalculatorView() { + (activity as? MainActivity)?.pushView(AttendanceCalculatorFragment.newInstance()) + } + override fun startActionMode() { actionMode = (activity as MainActivity?)?.startSupportActionMode(actionModeCallback) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt index 82fe69cb7..586a41ad0 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendancePresenter.kt @@ -1,16 +1,36 @@ package io.github.wulkanowy.ui.modules.attendance import android.annotation.SuppressLint -import io.github.wulkanowy.data.* +import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.db.entities.Attendance import io.github.wulkanowy.data.db.entities.Semester +import io.github.wulkanowy.data.flatResourceFlow +import io.github.wulkanowy.data.logResourceStatus +import io.github.wulkanowy.data.mapResourceData +import io.github.wulkanowy.data.onResourceData +import io.github.wulkanowy.data.onResourceError +import io.github.wulkanowy.data.onResourceIntermediate +import io.github.wulkanowy.data.onResourceLoading +import io.github.wulkanowy.data.onResourceNotLoading +import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.repositories.AttendanceRepository import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler -import io.github.wulkanowy.utils.* +import io.github.wulkanowy.utils.AnalyticsHelper +import io.github.wulkanowy.utils.capitalise +import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday +import io.github.wulkanowy.utils.isExcusableOrNotExcused +import io.github.wulkanowy.utils.isHolidays +import io.github.wulkanowy.utils.monday +import io.github.wulkanowy.utils.nextSchoolDay +import io.github.wulkanowy.utils.previousOrSameSchoolDay +import io.github.wulkanowy.utils.previousSchoolDay +import io.github.wulkanowy.utils.sunday +import io.github.wulkanowy.utils.toFormattedString import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.onEach import timber.log.Timber @@ -195,6 +215,11 @@ class AttendancePresenter @Inject constructor( return true } + fun onCalculatorSwitchSelected(): Boolean { + view?.openCalculatorView() + return true + } + private fun loadData(forceRefresh: Boolean = false) { Timber.i("Loading attendance data started") diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt index 2629c217e..f51ce7c7e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/AttendanceView.kt @@ -56,6 +56,8 @@ interface AttendanceView : BaseView { fun openSummaryView() + fun openCalculatorView() + fun startSendMessageIntent(date: LocalDate, numbers: String, reason: String) fun startActionMode() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorAdapter.kt new file mode 100644 index 000000000..4b908bba8 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorAdapter.kt @@ -0,0 +1,67 @@ +package io.github.wulkanowy.ui.modules.attendance.calculator + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import io.github.wulkanowy.R +import io.github.wulkanowy.data.pojos.AttendanceData +import io.github.wulkanowy.databinding.ItemAttendanceCalculatorHeaderBinding +import javax.inject.Inject +import kotlin.math.abs +import kotlin.math.roundToInt + +class AttendanceCalculatorAdapter @Inject constructor() : + RecyclerView.Adapter() { + + var items = emptyList() + + override fun getItemCount() = items.size + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + ItemAttendanceCalculatorHeaderBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + ) + + override fun onBindViewHolder(parent: ViewHolder, position: Int) { + val context = parent.binding.root.context + val item = items[position] + + with(parent.binding) { + attendanceCalculatorPercentage.text = "${item.presencePercentage.roundToInt()}" + + attendanceCalculatorSummaryBalance.text = when { + item.lessonBalance > 0 -> { + context.getString( + R.string.attendance_calculator_summary_balance_positive, + item.lessonBalance + ) + } + + item.lessonBalance < 0 -> { + context.getString( + R.string.attendance_calculator_summary_balance_negative, + abs(item.lessonBalance) + ) + } + + else -> context.getString(R.string.attendance_calculator_summary_balance_neutral) + } + attendanceCalculatorWarning.isVisible = item.lessonBalance < 0 + attendanceCalculatorTitle.text = item.subjectName + attendanceCalculatorSummaryValues.text = if (item.total == 0) { + context.getString(R.string.attendance_calculator_summary_values_empty) + } else { + context.getString( + R.string.attendance_calculator_summary_values, + item.presences, + item.total + ) + } + } + } + + class ViewHolder(val binding: ItemAttendanceCalculatorHeaderBinding) : + RecyclerView.ViewHolder(binding.root) +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorFragment.kt new file mode 100644 index 000000000..63d1d8be5 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorFragment.kt @@ -0,0 +1,133 @@ +package io.github.wulkanowy.ui.modules.attendance.calculator + +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import androidx.core.view.isVisible +import androidx.recyclerview.widget.LinearLayoutManager +import dagger.hilt.android.AndroidEntryPoint +import io.github.wulkanowy.R +import io.github.wulkanowy.data.pojos.AttendanceData +import io.github.wulkanowy.databinding.FragmentAttendanceCalculatorBinding +import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.main.MainActivity +import io.github.wulkanowy.ui.modules.main.MainView +import io.github.wulkanowy.ui.modules.settings.appearance.AppearanceFragment +import io.github.wulkanowy.ui.widgets.DividerItemDecoration +import io.github.wulkanowy.utils.getThemeAttrColor +import javax.inject.Inject + +@AndroidEntryPoint +class AttendanceCalculatorFragment : + BaseFragment(R.layout.fragment_attendance_calculator), + AttendanceCalculatorView, MainView.TitledView { + + @Inject + lateinit var presenter: AttendanceCalculatorPresenter + + @Inject + lateinit var attendanceCalculatorAdapter: AttendanceCalculatorAdapter + + override val titleStringId get() = R.string.attendance_title + + companion object { + fun newInstance() = AttendanceCalculatorFragment() + } + + override val isViewEmpty get() = attendanceCalculatorAdapter.items.isEmpty() + + @Suppress("DEPRECATION") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding = FragmentAttendanceCalculatorBinding.bind(view) + messageContainer = binding.attendanceCalculatorRecycler + presenter.onAttachView(this) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.action_menu_attendance_calculator, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return if (item.itemId == R.id.attendance_calculator_menu_settings) presenter.onSettingsSelected() + else false + } + + override fun openSettingsView() { + (activity as? MainActivity)?.pushView(AppearanceFragment.withFocusedPreference(getString(R.string.pref_key_attendance_target))) + } + + override fun initView() { + with(binding.attendanceCalculatorRecycler) { + layoutManager = LinearLayoutManager(context) + adapter = attendanceCalculatorAdapter + addItemDecoration(DividerItemDecoration(context)) + } + + with(binding) { + attendanceCalculatorSwipe.setOnRefreshListener(presenter::onSwipeRefresh) + attendanceCalculatorSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary)) + attendanceCalculatorSwipe.setProgressBackgroundColorSchemeColor( + requireContext().getThemeAttrColor( + R.attr.colorSwipeRefresh + ) + ) + attendanceCalculatorErrorRetry.setOnClickListener { presenter.onRetry() } + attendanceCalculatorErrorDetails.setOnClickListener { presenter.onDetailsClick() } + } + } + + override fun updateData(data: List) { + with(attendanceCalculatorAdapter) { + items = data + notifyDataSetChanged() + } + } + + override fun clearView() { + with(attendanceCalculatorAdapter) { + items = emptyList() + notifyDataSetChanged() + } + } + + override fun showEmpty(show: Boolean) { + binding.attendanceCalculatorEmpty.isVisible = show + } + + override fun showErrorView(show: Boolean) { + binding.attendanceCalculatorError.isVisible = show + } + + override fun setErrorDetails(message: String) { + binding.attendanceCalculatorErrorMessage.text = message + } + + override fun showProgress(show: Boolean) { + binding.attendanceCalculatorProgress.isVisible = show + } + + override fun enableSwipe(enable: Boolean) { + binding.attendanceCalculatorSwipe.isEnabled = enable + } + + override fun showContent(show: Boolean) { + binding.attendanceCalculatorRecycler.isVisible = show + } + + override fun showRefresh(show: Boolean) { + binding.attendanceCalculatorSwipe.isRefreshing = show + } + + override fun onDestroyView() { + presenter.onDetachView() + super.onDestroyView() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorPresenter.kt new file mode 100644 index 000000000..29cb2197f --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorPresenter.kt @@ -0,0 +1,94 @@ +package io.github.wulkanowy.ui.modules.attendance.calculator + +import io.github.wulkanowy.data.flatResourceFlow +import io.github.wulkanowy.data.logResourceStatus +import io.github.wulkanowy.data.onResourceData +import io.github.wulkanowy.data.onResourceError +import io.github.wulkanowy.data.onResourceIntermediate +import io.github.wulkanowy.data.onResourceNotLoading +import io.github.wulkanowy.data.repositories.SemesterRepository +import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.domain.attendance.GetAttendanceCalculatorDataUseCase +import io.github.wulkanowy.ui.base.BasePresenter +import io.github.wulkanowy.ui.base.ErrorHandler +import timber.log.Timber +import javax.inject.Inject + +class AttendanceCalculatorPresenter @Inject constructor( + errorHandler: ErrorHandler, + studentRepository: StudentRepository, + private val semesterRepository: SemesterRepository, + private val getAttendanceCalculatorData: GetAttendanceCalculatorDataUseCase, +) : BasePresenter(errorHandler, studentRepository) { + + private lateinit var lastError: Throwable + + override fun onAttachView(view: AttendanceCalculatorView) { + super.onAttachView(view) + view.initView() + Timber.i("Attendance calculator view was initialized") + errorHandler.showErrorMessage = ::showErrorViewOnError + loadData() + } + + fun onSwipeRefresh() { + Timber.i("Force refreshing the attendance calculator") + loadData(forceRefresh = true) + } + + fun onRetry() { + view?.run { + showErrorView(false) + showProgress(true) + } + loadData() + } + + fun onDetailsClick() { + view?.showErrorDetailsDialog(lastError) + } + + private fun loadData(forceRefresh: Boolean = false) { + flatResourceFlow { + val student = studentRepository.getCurrentStudent() + val semester = semesterRepository.getCurrentSemester(student) + getAttendanceCalculatorData(student, semester, forceRefresh) + } + .logResourceStatus("load attendance calculator") + .onResourceData { + view?.run { + showProgress(false) + showErrorView(false) + showContent(it.isNotEmpty()) + showEmpty(it.isEmpty()) + updateData(it) + } + } + .onResourceIntermediate { view?.showRefresh(true) } + .onResourceNotLoading { + view?.run { + enableSwipe(true) + showRefresh(false) + showProgress(false) + } + } + .onResourceError(errorHandler::dispatch) + .launch() + } + + private fun showErrorViewOnError(message: String, error: Throwable) { + view?.run { + if (isViewEmpty) { + lastError = error + setErrorDetails(message) + showErrorView(true) + showEmpty(false) + } else showError(message, error) + } + } + + fun onSettingsSelected(): Boolean { + view?.openSettingsView() + return true + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorView.kt new file mode 100644 index 000000000..21afe532e --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/attendance/calculator/AttendanceCalculatorView.kt @@ -0,0 +1,31 @@ +package io.github.wulkanowy.ui.modules.attendance.calculator + +import io.github.wulkanowy.data.pojos.AttendanceData +import io.github.wulkanowy.ui.base.BaseView + +interface AttendanceCalculatorView : BaseView { + + val isViewEmpty: Boolean + + fun initView() + + fun showRefresh(show: Boolean) + + fun showContent(show: Boolean) + + fun showProgress(show: Boolean) + + fun enableSwipe(enable: Boolean) + + fun showEmpty(show: Boolean) + + fun showErrorView(show: Boolean) + + fun setErrorDetails(message: String) + + fun updateData(data: List) + + fun clearView() + + fun openSettingsView() +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthPresenter.kt index 5c597eeb4..0be086b69 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/auth/AuthPresenter.kt @@ -27,8 +27,12 @@ class AuthPresenter @Inject constructor( private fun loadName() { presenterScope.launch { - runCatching { studentRepository.getCurrentStudent(false) } - .onSuccess { view?.showDescriptionWithName(it.studentName) } + runCatching { + studentRepository.getCurrentStudent(false) + .studentName + .replace(" ", "\u00A0") + } + .onSuccess { view?.showDescriptionWithName(it) } .onFailure { errorHandler.dispatch(it) } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt index 0fe36aa99..4eb602658 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectFragment.kt @@ -6,10 +6,12 @@ import androidx.core.os.bundleOf import androidx.core.view.isVisible import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.databinding.FragmentLoginStudentSelectBinding import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog @@ -111,6 +113,19 @@ class LoginStudentSelectFragment : LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog") } + override fun showAdminMessage(adminMessage: AdminMessage?) { + AdminMessageViewHolder( + binding = binding.loginStudentSelectAdminMessage, + onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed, + onAdminMessageClickListener = presenter::onAdminMessageSelected, + ).bind(adminMessage) + binding.loginStudentSelectAdminMessage.root.isVisible = adminMessage != null + } + + override fun openInternetBrowser(url: String) { + requireContext().openInternetBrowser(url) + } + override fun onDestroyView() { presenter.onDetachView() super.onDestroyView() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt index c81d353d4..39070cf0a 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenter.kt @@ -2,16 +2,23 @@ package io.github.wulkanowy.ui.modules.login.studentselect import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.dataOrNull +import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.db.entities.StudentWithSemesters +import io.github.wulkanowy.data.enums.MessageType +import io.github.wulkanowy.data.flatResourceFlow import io.github.wulkanowy.data.logResourceStatus import io.github.wulkanowy.data.mappers.mapToStudentWithSemesters +import io.github.wulkanowy.data.onResourceData +import io.github.wulkanowy.data.onResourceError import io.github.wulkanowy.data.pojos.RegisterStudent import io.github.wulkanowy.data.pojos.RegisterSymbol import io.github.wulkanowy.data.pojos.RegisterUnit import io.github.wulkanowy.data.pojos.RegisterUser +import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SchoolsRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.resourceFlow +import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase import io.github.wulkanowy.sdk.scrapper.exception.StudentGraduateException import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException import io.github.wulkanowy.services.sync.SyncManager @@ -33,6 +40,8 @@ class LoginStudentSelectPresenter @Inject constructor( private val syncManager: SyncManager, private val analytics: AnalyticsHelper, private val appInfo: AppInfo, + private val preferencesRepository: PreferencesRepository, + private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase ) : BasePresenter(loginErrorHandler, studentRepository) { private var lastError: Throwable? = null @@ -65,6 +74,7 @@ class LoginStudentSelectPresenter @Inject constructor( this.loginData = loginData this.registerUser = registerUser loadData() + loadAdminMessage() } private fun loadData() { @@ -88,7 +98,20 @@ class LoginStudentSelectPresenter @Inject constructor( refreshItems() } } - }.launch() + }.launch("load_data") + } + + private fun loadAdminMessage() { + flatResourceFlow { + getAppropriateAdminMessageUseCase( + scrapperBaseUrl = registerUser.scrapperBaseUrl.orEmpty(), + type = MessageType.LOGIN_STUDENT_SELECT_MESSAGE, + ) + } + .logResourceStatus("load login admin message") + .onResourceData { view?.showAdminMessage(it) } + .onResourceError { view?.showAdminMessage(null) } + .launch("load_admin_message") } private fun getStudentsWithCurrentlyActiveSemesters(): List { @@ -341,4 +364,14 @@ class LoginStudentSelectPresenter @Inject constructor( ) } } + + fun onAdminMessageSelected(url: String?) { + url?.let { view?.openInternetBrowser(it) } + } + + fun onAdminMessageDismissed(adminMessage: AdminMessage) { + preferencesRepository.dismissedAdminMessageIds += adminMessage.id + + view?.showAdminMessage(null) + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectView.kt index b69700f17..4d0ef9e92 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectView.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.ui.modules.login.studentselect +import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo @@ -25,4 +26,8 @@ interface LoginStudentSelectView : BaseView { fun openDiscordInvite() fun openEmail(supportInfo: LoginSupportInfo) + + fun showAdminMessage(adminMessage: AdminMessage?) + + fun openInternetBrowser(url: String) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt index 23ebffe9d..a813e7c00 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolFragment.kt @@ -9,13 +9,16 @@ import android.view.inputmethod.EditorInfo.IME_NULL import android.widget.ArrayAdapter import androidx.core.os.bundleOf import androidx.core.text.parseAsHtml +import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R +import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.databinding.FragmentLoginSymbolBinding import io.github.wulkanowy.ui.base.BaseFragment +import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog @@ -179,4 +182,17 @@ class LoginSymbolFragment : override fun openSupportDialog(supportInfo: LoginSupportInfo) { LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog") } + + override fun showAdminMessage(adminMessage: AdminMessage?) { + AdminMessageViewHolder( + binding = binding.loginSymbolAdminMessage, + onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed, + onAdminMessageClickListener = presenter::onAdminMessageSelected, + ).bind(adminMessage) + binding.loginSymbolAdminMessage.root.isVisible = adminMessage != null + } + + override fun openInternetBrowser(url: String) { + requireContext().openInternetBrowser(url) + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt index 5c31f14d4..8de2994a7 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolPresenter.kt @@ -2,10 +2,18 @@ package io.github.wulkanowy.ui.modules.login.symbol import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.dataOrNull +import io.github.wulkanowy.data.db.entities.AdminMessage +import io.github.wulkanowy.data.enums.MessageType +import io.github.wulkanowy.data.flatResourceFlow +import io.github.wulkanowy.data.logResourceStatus +import io.github.wulkanowy.data.onResourceData +import io.github.wulkanowy.data.onResourceError import io.github.wulkanowy.data.onResourceNotLoading import io.github.wulkanowy.data.pojos.RegisterUser +import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.resourceFlow +import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase import io.github.wulkanowy.sdk.scrapper.getNormalizedSymbol import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException import io.github.wulkanowy.ui.base.BasePresenter @@ -21,7 +29,9 @@ import javax.inject.Inject class LoginSymbolPresenter @Inject constructor( studentRepository: StudentRepository, private val loginErrorHandler: LoginErrorHandler, - private val analytics: AnalyticsHelper + private val analytics: AnalyticsHelper, + private val preferencesRepository: PreferencesRepository, + private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase, ) : BasePresenter(loginErrorHandler, studentRepository) { private var lastError: Throwable? = null @@ -43,6 +53,21 @@ class LoginSymbolPresenter @Inject constructor( clearAndFocusSymbol() showSoftKeyboard() } + + loadAdminMessage() + } + + private fun loadAdminMessage() { + flatResourceFlow { + getAppropriateAdminMessageUseCase( + scrapperBaseUrl = loginData.baseUrl, + type = MessageType.LOGIN_SYMBOL_MESSAGE, + ) + } + .logResourceStatus("load login admin message") + .onResourceData { view?.showAdminMessage(it) } + .onResourceError { view?.showAdminMessage(null) } + .launch("load_admin_message") } fun onSymbolTextChanged() { @@ -166,4 +191,14 @@ class LoginSymbolPresenter @Inject constructor( ) ) } + + fun onAdminMessageSelected(url: String?) { + url?.let { view?.openInternetBrowser(it) } + } + + fun onAdminMessageDismissed(adminMessage: AdminMessage) { + preferencesRepository.dismissedAdminMessageIds += adminMessage.id + + view?.showAdminMessage(null) + } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolView.kt index ace12f780..2fc910242 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/login/symbol/LoginSymbolView.kt @@ -1,5 +1,6 @@ package io.github.wulkanowy.ui.modules.login.symbol +import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.modules.login.LoginData @@ -44,4 +45,8 @@ interface LoginSymbolView : BaseView { fun openFaqPage() fun openSupportDialog(supportInfo: LoginSupportInfo) + + fun showAdminMessage(adminMessage: AdminMessage?) + + fun openInternetBrowser(url: String) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryAdapter.kt index 0c1b89c8e..9c718af45 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumber/history/LuckyNumberHistoryAdapter.kt @@ -33,4 +33,4 @@ class LuckyNumberHistoryAdapter @Inject constructor() : } class ItemViewHolder(val binding: ItemLuckyNumberHistoryBinding) : RecyclerView.ViewHolder(binding.root) -} \ No newline at end of file +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt index 1ab079a3a..e6de17818 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/luckynumberwidget/LuckyNumberWidgetProvider.kt @@ -145,7 +145,11 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() { } if (currentStudent != null) { - luckyNumberRepository.getLuckyNumber(currentStudent, forceRefresh = false) + luckyNumberRepository.getLuckyNumber( + student = currentStudent, + forceRefresh = false, + isFromAppWidget = true + ) .toFirstResult() .dataOrThrow } else null diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt index 3d0c8052b..62544f83e 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/settings/appearance/AppearanceFragment.kt @@ -3,7 +3,9 @@ package io.github.wulkanowy.ui.modules.settings.appearance import android.content.SharedPreferences import android.os.Bundle import android.view.View +import androidx.core.os.bundleOf import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SeekBarPreference import com.yariksoffice.lingver.Lingver import dagger.hilt.android.AndroidEntryPoint import io.github.wulkanowy.R @@ -29,13 +31,31 @@ class AppearanceFragment : PreferenceFragmentCompat(), override val titleStringId get() = R.string.pref_settings_appearance_title + companion object { + fun withFocusedPreference(key: String) = AppearanceFragment().apply { + arguments = bundleOf(FOCUSED_KEY to key) + } + + private const val FOCUSED_KEY = "focusedKey" + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) presenter.onAttachView(this) + arguments?.getString(FOCUSED_KEY)?.let { scrollToPreference(it) } } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.scheme_preferences_appearance, rootKey) + val attendanceTargetPref = + findPreference(requireContext().getString(R.string.pref_key_attendance_target))!! + attendanceTargetPref.setOnPreferenceChangeListener { _, newValueObj -> + val newValue = (((newValueObj as Int).toDouble() + 2.5) / 5).toInt() * 5 + attendanceTargetPref.value = + newValue.coerceIn(attendanceTargetPref.min, attendanceTargetPref.max) + + false + } } override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt index a4221a2a2..5cb6c401f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableAdapter.kt @@ -7,20 +7,21 @@ import android.view.ViewGroup import android.widget.TextView import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import io.github.wulkanowy.R import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.databinding.ItemTimetableBinding import io.github.wulkanowy.databinding.ItemTimetableEmptyBinding +import io.github.wulkanowy.databinding.ItemTimetableMainAdditionalBinding import io.github.wulkanowy.databinding.ItemTimetableSmallBinding +import io.github.wulkanowy.utils.SyncListAdapter import io.github.wulkanowy.utils.getPlural import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.toFormattedString import javax.inject.Inject class TimetableAdapter @Inject constructor() : - ListAdapter(differ) { + SyncListAdapter(Differ) { override fun getItemViewType(position: Int): Int = getItem(position).type.ordinal @@ -39,6 +40,10 @@ class TimetableAdapter @Inject constructor() : TimetableItemType.EMPTY -> EmptyViewHolder( ItemTimetableEmptyBinding.inflate(inflater, parent, false) ) + + TimetableItemType.ADDITIONAL -> AdditionalViewHolder( + ItemTimetableMainAdditionalBinding.inflate(inflater, parent, false) + ) } } @@ -61,16 +66,30 @@ class TimetableAdapter @Inject constructor() : binding = holder.binding, item = getItem(position) as TimetableItem.Small, ) - is NormalViewHolder -> bindNormalView( binding = holder.binding, item = getItem(position) as TimetableItem.Normal, ) - is EmptyViewHolder -> bindEmptyView( binding = holder.binding, item = getItem(position) as TimetableItem.Empty, ) + + is AdditionalViewHolder -> bindAdditionalView( + binding = holder.binding, + item = getItem(position) as TimetableItem.Additional, + ) + } + } + + private fun bindAdditionalView( + binding: ItemTimetableMainAdditionalBinding, + item: TimetableItem.Additional + ) { + with(binding) { + timetableItemSubject.text = item.additional.subject + timetableItemTimeStart.text = item.additional.start.toFormattedString("HH:mm") + timetableItemTimeFinish.text = item.additional.end.toFormattedString("HH:mm") } } @@ -307,31 +326,32 @@ class TimetableAdapter @Inject constructor() : private class EmptyViewHolder(val binding: ItemTimetableEmptyBinding) : RecyclerView.ViewHolder(binding.root) - companion object { - private val differ = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: TimetableItem, newItem: TimetableItem): Boolean = - when { - oldItem is TimetableItem.Small && newItem is TimetableItem.Small -> { - oldItem.lesson.start == newItem.lesson.start - } + private class AdditionalViewHolder(val binding: ItemTimetableMainAdditionalBinding) : + RecyclerView.ViewHolder(binding.root) - oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal -> { - oldItem.lesson.start == newItem.lesson.start - } - - else -> oldItem == newItem + private object Differ : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: TimetableItem, newItem: TimetableItem): Boolean = + when { + oldItem is TimetableItem.Small && newItem is TimetableItem.Small -> { + oldItem.lesson.start == newItem.lesson.start } - override fun areContentsTheSame(oldItem: TimetableItem, newItem: TimetableItem) = - oldItem == newItem + oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal -> { + oldItem.lesson.start == newItem.lesson.start + } - override fun getChangePayload(oldItem: TimetableItem, newItem: TimetableItem): Any? { - return if (oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal) { - if (oldItem.lesson == newItem.lesson && oldItem.showGroupsInPlan == newItem.showGroupsInPlan && oldItem.timeLeft != newItem.timeLeft) { - "time_left" - } else super.getChangePayload(oldItem, newItem) - } else super.getChangePayload(oldItem, newItem) + else -> oldItem == newItem } + + override fun areContentsTheSame(oldItem: TimetableItem, newItem: TimetableItem) = + oldItem == newItem + + override fun getChangePayload(oldItem: TimetableItem, newItem: TimetableItem): Any? { + return if (oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal) { + if (oldItem.lesson == newItem.lesson && oldItem.showGroupsInPlan == newItem.showGroupsInPlan && oldItem.timeLeft != newItem.timeLeft) { + "time_left" + } else super.getChangePayload(oldItem, newItem) + } else super.getChangePayload(oldItem, newItem) } } } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt index 0e6459110..b73e7c26d 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableFragment.kt @@ -21,7 +21,11 @@ import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.timetable.additional.AdditionalLessonsFragment import io.github.wulkanowy.ui.modules.timetable.completed.CompletedLessonsFragment import io.github.wulkanowy.ui.widgets.DividerItemDecoration -import io.github.wulkanowy.utils.* +import io.github.wulkanowy.utils.dpToPx +import io.github.wulkanowy.utils.firstSchoolDayInSchoolYear +import io.github.wulkanowy.utils.getThemeAttrColor +import io.github.wulkanowy.utils.lastSchoolDayInSchoolYear +import io.github.wulkanowy.utils.openMaterialDatePicker import java.time.LocalDate import javax.inject.Inject @@ -104,8 +108,11 @@ class TimetableFragment : BaseFragment(R.layout.fragme } } - override fun updateData(data: List) { - timetableAdapter.submitList(data) + override fun updateData(data: List, isDayChanged: Boolean) { + when { + isDayChanged -> timetableAdapter.recreate(data) + else -> timetableAdapter.submitList(data) + } } override fun clearData() { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableItem.kt index 402b03dd9..93290ba21 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableItem.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableItem.kt @@ -1,6 +1,7 @@ package io.github.wulkanowy.ui.modules.timetable import io.github.wulkanowy.data.db.entities.Timetable +import io.github.wulkanowy.data.db.entities.TimetableAdditional import java.time.Duration sealed class TimetableItem(val type: TimetableItemType) { @@ -23,6 +24,10 @@ sealed class TimetableItem(val type: TimetableItemType) { val numFrom: Int, val numTo: Int ) : TimetableItem(TimetableItemType.EMPTY) + + data class Additional( + val additional: TimetableAdditional, + ) : TimetableItem(TimetableItemType.ADDITIONAL) } data class TimeLeft( @@ -34,5 +39,6 @@ data class TimeLeft( enum class TimetableItemType { SMALL, NORMAL, - EMPTY + EMPTY, + ADDITIONAL, } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt index 8ef0772b8..c00bdc3e4 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetablePresenter.kt @@ -4,6 +4,9 @@ import android.os.Handler import android.os.Looper import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Timetable +import io.github.wulkanowy.data.db.entities.TimetableAdditional +import io.github.wulkanowy.data.enums.ShowAdditionalLessonsMode.BELOW +import io.github.wulkanowy.data.enums.ShowAdditionalLessonsMode.NONE import io.github.wulkanowy.data.enums.TimetableGapsMode.BETWEEN_AND_BEFORE_LESSONS import io.github.wulkanowy.data.enums.TimetableGapsMode.NO_GAPS import io.github.wulkanowy.data.enums.TimetableMode @@ -14,6 +17,7 @@ import io.github.wulkanowy.data.onResourceError import io.github.wulkanowy.data.onResourceIntermediate import io.github.wulkanowy.data.onResourceNotLoading import io.github.wulkanowy.data.onResourceSuccess +import io.github.wulkanowy.data.pojos.TimetableFull import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository @@ -81,7 +85,7 @@ class TimetablePresenter @Inject constructor( } else currentDate?.previousSchoolDay reloadView(date ?: return) - loadData() + loadData(isDayChanged = true) } fun onNextDay() { @@ -90,7 +94,7 @@ class TimetablePresenter @Inject constructor( } else currentDate?.nextSchoolDay reloadView(date ?: return) - loadData() + loadData(isDayChanged = true) } fun onPickDate() { @@ -104,7 +108,7 @@ class TimetablePresenter @Inject constructor( fun onSwipeRefresh() { Timber.i("Force refreshing the timetable") - loadData(true) + loadData(forceRefresh = true) } fun onRetry() { @@ -112,7 +116,7 @@ class TimetablePresenter @Inject constructor( showErrorView(false) showProgress(true) } - loadData(true) + loadData(forceRefresh = true) } fun onDetailsClick() { @@ -145,7 +149,7 @@ class TimetablePresenter @Inject constructor( return true } - private fun loadData(forceRefresh: Boolean = false) { + private fun loadData(forceRefresh: Boolean = false, isDayChanged: Boolean = false) { flatResourceFlow { val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) @@ -169,9 +173,9 @@ class TimetablePresenter @Inject constructor( enableSwipe(true) showProgress(false) showErrorView(false) - showContent(it.lessons.isNotEmpty()) - showEmpty(it.lessons.isEmpty()) - updateData(it.lessons) + updateData(it, isDayChanged) + showContent(it.lessons.isNotEmpty() || it.additional.isNotEmpty()) + showEmpty(it.lessons.isEmpty() && it.additional.isEmpty()) setDayHeaderMessage(it.headers.find { header -> header.date == currentDate }?.content) reloadNavigation() } @@ -216,67 +220,97 @@ class TimetablePresenter @Inject constructor( } } - private fun updateData(lessons: List) { + private fun updateData(lessons: TimetableFull, isDayChanged: Boolean) { tickTimer?.cancel() - if (currentDate != now()) { - view?.updateData(createItems(lessons)) - } else { - tickTimer = timer(period = 2_000) { + view?.updateData(createItems(lessons), isDayChanged) + if (currentDate == now()) { + tickTimer = timer(period = 2_000, initialDelay = 2_000) { Handler(Looper.getMainLooper()).post { - view?.updateData(createItems(lessons)) + view?.updateData(createItems(lessons), isDayChanged) } } } } - private fun createItems(items: List): List { - val filteredItems = items - .filter { - if (prefRepository.showWholeClassPlan == TimetableMode.ONLY_CURRENT_GROUP) { - it.isStudentPlan - } else true - } - .sortedWith(compareBy({ item -> item.start }, { item -> !item.isStudentPlan })) + private sealed class Item( + val isStudentPlan: Boolean, + val start: Instant, + val number: Int?, + ) { + class Lesson(val lesson: Timetable) : + Item(lesson.isStudentPlan, lesson.start, lesson.number) + + class Additional(val additional: TimetableAdditional) : Item(true, additional.start, null) + } + + private fun createItems(fullTimetable: TimetableFull): List { + val showAdditionalLessonsInPlan = prefRepository.showAdditionalLessonsInPlan + val allItems = + fullTimetable.lessons.map(Item::Lesson) + fullTimetable.additional.map(Item::Additional) + .takeIf { showAdditionalLessonsInPlan != NONE }.orEmpty() + + val filteredItems = allItems.filter { + if (prefRepository.showWholeClassPlan == TimetableMode.ONLY_CURRENT_GROUP) { + it.isStudentPlan + } else true + }.sortedWith( + (compareBy { it is Item.Additional } + .takeIf { showAdditionalLessonsInPlan == BELOW } ?: EmptyComparator()) + .thenBy { it.start } + .thenBy { !it.isStudentPlan } + ) var prevNum = when (prefRepository.showTimetableGaps) { BETWEEN_AND_BEFORE_LESSONS -> 0 else -> null } + var prevIsAdditional = false return buildList { filteredItems.forEachIndexed { i, it -> - if (prefRepository.showTimetableGaps != NO_GAPS && prevNum != null && it.number > prevNum!! + 1) { - val emptyLesson = TimetableItem.Empty( - numFrom = prevNum!! + 1, - numTo = it.number - 1 - ) - add(emptyLesson) + if (prefRepository.showTimetableGaps != NO_GAPS) { + if (prevNum != null && it.number != null && it.number > prevNum!! + 1) { + if (!prevIsAdditional) { + // Additional lessons do count as a lesson so don't add empty lessons + // when there is an additional lesson present + val emptyLesson = TimetableItem.Empty( + numFrom = prevNum!! + 1, numTo = it.number - 1 + ) + add(emptyLesson) + } + } + prevNum = it.number + prevIsAdditional = it is Item.Additional } - if (it.isStudentPlan) { - val normalLesson = TimetableItem.Normal( - lesson = it, - showGroupsInPlan = prefRepository.showGroupsInPlan, - timeLeft = filteredItems.getTimeLeftForLesson(it, i), - onClick = ::onTimetableItemSelected, - isLessonNumberVisible = !isEduOne - ) - add(normalLesson) - } else { - val smallLesson = TimetableItem.Small( - lesson = it, - onClick = ::onTimetableItemSelected, - isLessonNumberVisible = !isEduOne - ) - add(smallLesson) + if (it is Item.Lesson) { + if (it.isStudentPlan) { + val normalLesson = TimetableItem.Normal( + lesson = it.lesson, + showGroupsInPlan = prefRepository.showGroupsInPlan, + timeLeft = filteredItems.getTimeLeftForLesson(it.lesson, i), + onClick = ::onTimetableItemSelected, + isLessonNumberVisible = !isEduOne + ) + add(normalLesson) + } else { + val smallLesson = TimetableItem.Small( + lesson = it.lesson, + onClick = ::onTimetableItemSelected, + isLessonNumberVisible = !isEduOne + ) + add(smallLesson) + } + } else if (it is Item.Additional) { + // If the user disabled showing additional lessons, they would've been filtered + // out already, so there's no need to check it again. + add(TimetableItem.Additional(it.additional)) } - - prevNum = it.number } } } - private fun List.getTimeLeftForLesson(lesson: Timetable, index: Int): TimeLeft { + private fun List.getTimeLeftForLesson(lesson: Timetable, index: Int): TimeLeft { val isShowTimeUntil = lesson.isShowTimeUntil(getPreviousLesson(index)) return TimeLeft( until = lesson.until.plusMinutes(1).takeIf { isShowTimeUntil }, @@ -285,11 +319,20 @@ class TimetablePresenter @Inject constructor( ) } - private fun List.getPreviousLesson(position: Int): Instant? { - return filter { it.isStudentPlan } - .getOrNull(position - 1 - filterIndexed { i, item -> i < position && !item.isStudentPlan }.size) + private fun List.getPreviousLesson(position: Int): Instant? { + val lessonAdditionalOffset = filterIndexed { i, item -> + i < position && item is Item.Additional + }.size + val lessonStudentPlanOffset = filterIndexed { i, item -> + i < position && !item.isStudentPlan + }.size + val lessonIndex = position - 1 - lessonAdditionalOffset - lessonStudentPlanOffset + + return filterIsInstance() + .filter { it.isStudentPlan } + .getOrNull(lessonIndex) ?.let { - if (!it.canceled && it.isStudentPlan) it.end + if (!it.lesson.canceled && it.isStudentPlan) it.lesson.end else null } } @@ -342,3 +385,7 @@ class TimetablePresenter @Inject constructor( super.onDetachView() } } + +private class EmptyComparator : Comparator { + override fun compare(o1: T, o2: T) = 0 +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt index 40190d51f..f4d5b7621 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/TimetableView.kt @@ -12,7 +12,7 @@ interface TimetableView : BaseView { fun initView() - fun updateData(data: List) + fun updateData(data: List, isDayChanged: Boolean) fun updateNavigationDay(date: String) diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt index faa833c20..bf6be56f6 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsFragment.kt @@ -13,7 +13,11 @@ import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.timetable.additional.add.AdditionalLessonAddDialog import io.github.wulkanowy.ui.widgets.DividerItemDecoration -import io.github.wulkanowy.utils.* +import io.github.wulkanowy.utils.dpToPx +import io.github.wulkanowy.utils.firstSchoolDayInSchoolYear +import io.github.wulkanowy.utils.getThemeAttrColor +import io.github.wulkanowy.utils.lastSchoolDayInSchoolYear +import io.github.wulkanowy.utils.openMaterialDatePicker import java.time.LocalDate import javax.inject.Inject @@ -132,8 +136,12 @@ class AdditionalLessonsFragment : binding.additionalLessonsNextButton.visibility = if (show) View.VISIBLE else View.INVISIBLE } - override fun showAddAdditionalLessonDialog() { - (activity as? MainActivity)?.showDialogFragment(AdditionalLessonAddDialog.newInstance()) + override fun showAddAdditionalLessonDialog(currentDate: LocalDate) { + (activity as? MainActivity)?.showDialogFragment( + AdditionalLessonAddDialog.newInstance( + currentDate + ) + ) } override fun showDatePickerDialog(selectedDate: LocalDate) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsPresenter.kt index d0a01b38c..16ec9746f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsPresenter.kt @@ -1,14 +1,27 @@ package io.github.wulkanowy.ui.modules.timetable.additional import android.annotation.SuppressLint -import io.github.wulkanowy.data.* import io.github.wulkanowy.data.db.entities.TimetableAdditional +import io.github.wulkanowy.data.flatResourceFlow +import io.github.wulkanowy.data.logResourceStatus +import io.github.wulkanowy.data.onResourceData +import io.github.wulkanowy.data.onResourceError +import io.github.wulkanowy.data.onResourceNotLoading +import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.TimetableRepository +import io.github.wulkanowy.domain.timetable.IsStudentHasLessonsOnWeekendUseCase import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.ErrorHandler -import io.github.wulkanowy.utils.* +import io.github.wulkanowy.utils.AnalyticsHelper +import io.github.wulkanowy.utils.capitalise +import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday +import io.github.wulkanowy.utils.isHolidays +import io.github.wulkanowy.utils.nextOrSameSchoolDay +import io.github.wulkanowy.utils.nextSchoolDay +import io.github.wulkanowy.utils.previousSchoolDay +import io.github.wulkanowy.utils.toFormattedString import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.onEach @@ -22,11 +35,14 @@ class AdditionalLessonsPresenter @Inject constructor( errorHandler: ErrorHandler, private val semesterRepository: SemesterRepository, private val timetableRepository: TimetableRepository, + private val isStudentHasLessonsOnWeekendUseCase: IsStudentHasLessonsOnWeekendUseCase, private val analytics: AnalyticsHelper ) : BasePresenter(errorHandler, studentRepository) { private var baseDate: LocalDate = LocalDate.now().nextOrSameSchoolDay + private var isWeekendHasLessons: Boolean = false + lateinit var currentDate: LocalDate private set @@ -43,12 +59,18 @@ class AdditionalLessonsPresenter @Inject constructor( } fun onPreviousDay() { - loadData(currentDate.previousSchoolDay) + val date = if (isWeekendHasLessons) { + currentDate.minusDays(1) + } else currentDate.previousSchoolDay + loadData(date) reloadView() } fun onNextDay() { - loadData(currentDate.nextSchoolDay) + val date = if (isWeekendHasLessons) { + currentDate.plusDays(1) + } else currentDate.nextSchoolDay + loadData(date) reloadView() } @@ -57,7 +79,7 @@ class AdditionalLessonsPresenter @Inject constructor( } fun onAdditionalLessonAddButtonClicked() { - view?.showAddAdditionalLessonDialog() + view?.showAddAdditionalLessonDialog(currentDate) } fun onDateSet(year: Int, month: Int, day: Int) { @@ -131,6 +153,8 @@ class AdditionalLessonsPresenter @Inject constructor( flatResourceFlow { val student = studentRepository.getCurrentStudent() val semester = semesterRepository.getCurrentSemester(student) + + isWeekendHasLessons = isStudentHasLessonsOnWeekendUseCase(semester, currentDate) timetableRepository.getTimetable( student = student, semester = semester, diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsView.kt index 76d37b754..291c12172 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/AdditionalLessonsView.kt @@ -36,7 +36,7 @@ interface AdditionalLessonsView : BaseView { fun showDatePickerDialog(selectedDate: LocalDate) - fun showAddAdditionalLessonDialog() + fun showAddAdditionalLessonDialog(currentDate: LocalDate) fun showSuccessMessage() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddDialog.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddDialog.kt index 134719979..9470c910f 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddDialog.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddDialog.kt @@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.timetable.additional.add import android.app.Dialog import android.os.Bundle import android.view.View +import androidx.core.os.bundleOf import androidx.core.widget.doOnTextChanged import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.timepicker.MaterialTimePicker @@ -26,10 +27,12 @@ class AdditionalLessonAddDialog : BaseDialogFragment lateinit var presenter: AdditionalLessonAddPresenter companion object { - fun newInstance() = AdditionalLessonAddDialog() + const val ARGUMENT_KEY = "additional_lesson_default_date" + fun newInstance(defaultDate: LocalDate) = AdditionalLessonAddDialog().apply { + arguments = bundleOf(ARGUMENT_KEY to defaultDate.toEpochDay()) + } } - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return MaterialAlertDialogBuilder(requireContext(), theme) .setView( @@ -40,10 +43,13 @@ class AdditionalLessonAddDialog : BaseDialogFragment override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + arguments?.getLong(ARGUMENT_KEY)?.let(LocalDate::ofEpochDay)?.let { + presenter.onDateSelected(it) + } presenter.onAttachView(this) } - override fun initView() { + override fun initView(selectedDate: LocalDate) { with(binding) { additionalLessonDialogStartEdit.doOnTextChanged { _, _, _, _ -> additionalLessonDialogStart.isErrorEnabled = false @@ -53,6 +59,7 @@ class AdditionalLessonAddDialog : BaseDialogFragment additionalLessonDialogEnd.isErrorEnabled = false additionalLessonDialogEnd.error = null } + additionalLessonDialogDateEdit.setText(selectedDate.toFormattedString()) additionalLessonDialogDateEdit.doOnTextChanged { _, _, _, _ -> additionalLessonDialogDate.isErrorEnabled = false additionalLessonDialogDate.error = null @@ -61,7 +68,6 @@ class AdditionalLessonAddDialog : BaseDialogFragment additionalLessonDialogContent.isErrorEnabled = false additionalLessonDialogContent.error = null } - additionalLessonDialogAdd.setOnClickListener { presenter.onAddAdditionalClicked( start = additionalLessonDialogStartEdit.text?.toString(), @@ -155,7 +161,9 @@ class AdditionalLessonAddDialog : BaseDialogFragment .build() timePicker.addOnPositiveButtonClickListener { - onTimeSelected(LocalTime.of(timePicker.hour, timePicker.minute)) + if (isAdded) { + onTimeSelected(LocalTime.of(timePicker.hour, timePicker.minute)) + } } if (!parentFragmentManager.isStateSaved) { diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddPresenter.kt index c207165d3..db59a2ab5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddPresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddPresenter.kt @@ -10,9 +10,12 @@ import io.github.wulkanowy.utils.lastSchoolDayInSchoolYear import io.github.wulkanowy.utils.toLocalDate import kotlinx.coroutines.launch import timber.log.Timber -import java.time.* +import java.time.LocalDate +import java.time.LocalTime +import java.time.ZoneId +import java.time.ZonedDateTime import java.time.temporal.ChronoUnit -import java.util.* +import java.util.UUID import javax.inject.Inject class AdditionalLessonAddPresenter @Inject constructor( @@ -30,7 +33,7 @@ class AdditionalLessonAddPresenter @Inject constructor( override fun onAttachView(view: AdditionalLessonAddView) { super.onAttachView(view) - view.initView() + view.initView(selectedDate) Timber.i("AdditionalLesson details view was initialized") } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddView.kt index 0df53815b..8d9678e7b 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetable/additional/add/AdditionalLessonAddView.kt @@ -6,7 +6,7 @@ import java.time.LocalTime interface AdditionalLessonAddView : BaseView { - fun initView() + fun initView(selectedDate: LocalDate) fun closeDialog() diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt index 218c25834..e60d54880 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt @@ -101,7 +101,14 @@ class TimetableWidgetFactory( private suspend fun getLessons( student: Student, semester: Semester, date: LocalDate ): List { - val timetable = timetableRepository.getTimetable(student, semester, date, date, false) + val timetable = timetableRepository.getTimetable( + student = student, + semester = semester, + start = date, + end = date, + forceRefresh = false, + isFromAppWidget = true + ) val lessons = timetable.toFirstResult().dataOrThrow.lessons return lessons.sortedBy { it.start } } diff --git a/app/src/main/java/io/github/wulkanowy/utils/AppWidgetUpdater.kt b/app/src/main/java/io/github/wulkanowy/utils/AppWidgetUpdater.kt new file mode 100644 index 000000000..1b54f40c1 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/utils/AppWidgetUpdater.kt @@ -0,0 +1,34 @@ +package io.github.wulkanowy.utils + +import android.appwidget.AppWidgetManager +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import dagger.hilt.android.qualifiers.ApplicationContext +import timber.log.Timber +import javax.inject.Inject +import kotlin.reflect.KClass + +class AppWidgetUpdater @Inject constructor( + @ApplicationContext private val context: Context, + private val appWidgetManager: AppWidgetManager +) { + + fun updateAllAppWidgetsByProvider(providerClass: KClass) { + try { + val ids = appWidgetManager.getAppWidgetIds(ComponentName(context, providerClass.java)) + if (ids.isEmpty()) return + + val intent = Intent(context, providerClass.java) + .apply { + action = AppWidgetManager.ACTION_APPWIDGET_UPDATE + putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids) + } + + context.sendBroadcast(intent) + } catch (e: Exception) { + Timber.e(e, "Failed to update all widgets for provider $providerClass") + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt index 397c95953..3cac0b48e 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/AttendanceExtension.kt @@ -10,19 +10,19 @@ import io.github.wulkanowy.sdk.scrapper.attendance.AttendanceCategory * (https://www.vulcan.edu.pl/vulcang_files/user/AABW/AABW-PDF/uonetplus/uonetplus_Frekwencja-liczby-obecnych-nieobecnych.pdf) */ -private inline val AttendanceSummary.allPresences: Double - get() = presence.toDouble() + absenceForSchoolReasons + lateness + latenessExcused +inline val AttendanceSummary.allPresences: Int + get() = presence + absenceForSchoolReasons + lateness + latenessExcused -private inline val AttendanceSummary.allAbsences: Double - get() = absence.toDouble() + absenceExcused +inline val AttendanceSummary.allAbsences: Int + get() = absence + absenceExcused inline val Attendance.isExcusableOrNotExcused: Boolean get() = (excusable || ((absence || lateness) && !excused)) && excuseStatus == null -fun AttendanceSummary.calculatePercentage() = calculatePercentage(allPresences, allAbsences) +fun AttendanceSummary.calculatePercentage() = calculatePercentage(allPresences.toDouble(), allAbsences.toDouble()) fun List.calculatePercentage(): Double { - return calculatePercentage(sumOf { it.allPresences }, sumOf { it.allAbsences }) + return calculatePercentage(sumOf { it.allPresences.toDouble() }, sumOf { it.allAbsences.toDouble() }) } private fun calculatePercentage(presence: Double, absence: Double): Double { diff --git a/app/src/main/java/io/github/wulkanowy/utils/SyncListAdapter.kt b/app/src/main/java/io/github/wulkanowy/utils/SyncListAdapter.kt new file mode 100644 index 000000000..e9135f498 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/utils/SyncListAdapter.kt @@ -0,0 +1,66 @@ +package io.github.wulkanowy.utils + +import android.annotation.SuppressLint +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView + +/** + * Custom alternative to androidx.recyclerview.widget.ListAdapter. ListAdapter is asynchronous which + * caused data race problems in views when a Resource.Error arrived shortly after + * Resource.Intermediate/Success - occasionally in that case the user could see both the Resource's + * data and an error message one on top of the other. This is synchronized by design to avoid that + * problem, however it retains the quality of life improvements of the original. + */ +abstract class SyncListAdapter private constructor( + private val updateStrategy: SyncListAdapter.(List) -> Unit +) : RecyclerView.Adapter() { + + constructor(differ: DiffUtil.ItemCallback) : this({ newItems -> + val diffResult = DiffUtil.calculateDiff(toCallback(differ, items, newItems)) + items = newItems + diffResult.dispatchUpdatesTo(this) + }) + + var items = emptyList() + private set + + final override fun getItemCount() = items.size + + fun getItem(position: Int): T { + return items[position] + } + + /** + * Updates all items, same as submitList, however also disables animations temporarily. + * This prevents a flashing effect on some views. Should be used in favor of submitList when + * all data is changed (e.g. the selected day changes in timetable causing all lessons to change). + */ + @SuppressLint("NotifyDataSetChanged") + fun recreate(data: List) { + items = data + notifyDataSetChanged() + } + + fun submitList(data: List) { + updateStrategy(data.toList()) + } + + private fun toCallback( + itemCallback: DiffUtil.ItemCallback, + old: List, + new: List, + ) = object : DiffUtil.Callback() { + override fun getOldListSize() = old.size + + override fun getNewListSize() = new.size + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = + itemCallback.areItemsTheSame(old[oldItemPosition], new[newItemPosition]) + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = + itemCallback.areContentsTheSame(old[oldItemPosition], new[newItemPosition]) + + override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int) = + itemCallback.getChangePayload(old[oldItemPosition], new[newItemPosition]) + } +} diff --git a/app/src/main/play/listings/cs-CZ/full-description.txt b/app/src/main/play/listings/cs-CZ/full-description.txt index 1420f5d67..3d142ebff 100644 --- a/app/src/main/play/listings/cs-CZ/full-description.txt +++ b/app/src/main/play/listings/cs-CZ/full-description.txt @@ -6,7 +6,7 @@ Zvýrazněné vlastnosti a funkce: - šťastné číslo, - náhled na další a dokončené lekce, - tmavý motiv, -- žádné reklamy, +- volitelné reklamy, - offline režim, - upozornění. diff --git a/app/src/main/play/listings/pl-PL/full-description.txt b/app/src/main/play/listings/pl-PL/full-description.txt index 7da51da2d..b0193b5d1 100644 --- a/app/src/main/play/listings/pl-PL/full-description.txt +++ b/app/src/main/play/listings/pl-PL/full-description.txt @@ -6,7 +6,7 @@ Wyróżnione cechy i funkcje: - szczęśliwy numerek, - podgląd lekcji dodatkowych i zrealizowanych, - ciemny motyw. -- brak reklam, +- opcjonalne reklam, - tryb offline, - powiadomienia. diff --git a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/1-start.jpg b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/1-start.jpg index 0ed20c04d..19b96d9a6 100644 Binary files a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/1-start.jpg and b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/1-start.jpg differ diff --git a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/2.jpg b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/2.jpg index f70e2c43b..fe81aadce 100644 Binary files a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/2.jpg and b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/2.jpg differ diff --git a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/3-timetable-dialog.jpg b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/3-timetable-dialog.jpg index 968fccdbe..0e62c2e10 100644 Binary files a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/3-timetable-dialog.jpg and b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/3-timetable-dialog.jpg differ diff --git a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/4-exams.jpg b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/4-exams.jpg index 3f49e774a..980993af1 100644 Binary files a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/4-exams.jpg and b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/4-exams.jpg differ diff --git a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/5-timetable-widget.jpg b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/5-timetable-widget.jpg index f68daaf1a..6ee7eeb95 100644 Binary files a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/5-timetable-widget.jpg and b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/5-timetable-widget.jpg differ diff --git a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/6-class-grades.jpg b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/6-class-grades.jpg index ca5446a24..ec86cdb0d 100644 Binary files a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/6-class-grades.jpg and b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/6-class-grades.jpg differ diff --git a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/7-account-switcher.jpg b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/7-account-switcher.jpg index ca747aff0..66c0db40d 100644 Binary files a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/7-account-switcher.jpg and b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/7-account-switcher.jpg differ diff --git a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/8-themes.jpg b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/8-themes.jpg index ce3af9bbf..94788cd7f 100644 Binary files a/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/8-themes.jpg and b/app/src/main/play/listings/pl-PL/graphics/phone-screenshots/8-themes.jpg differ diff --git a/app/src/main/play/listings/sk/full-description.txt b/app/src/main/play/listings/sk/full-description.txt index 2a4787d2d..236825984 100644 --- a/app/src/main/play/listings/sk/full-description.txt +++ b/app/src/main/play/listings/sk/full-description.txt @@ -6,7 +6,7 @@ Zvýraznené vlastnosti a funkcie: - šťastné číslo, - náhľad na ďalšie a dokončené lekcie, - tmavý motív, -- žiadne reklamy, +- voliteľné reklamy, - offline režim, - upozornenia. diff --git a/app/src/main/play/release-notes/pl-PL/default.txt b/app/src/main/play/release-notes/pl-PL/default.txt index 2b87f21d5..fb07d4244 100644 --- a/app/src/main/play/release-notes/pl-PL/default.txt +++ b/app/src/main/play/release-notes/pl-PL/default.txt @@ -1,5 +1,8 @@ -Wersja 2.5.8 +Wersja 2.6.0 -— obeszliśmy próby blokowania Wulkanowego przez firmę VULCAN, o czymś pewnie zapomnieliśmy, ale nie miejcie nam tego za złe +— dodaliśmy kalkulator frekwencji +— dodaliśmy wyświetlanie lekcji dodatkowych w planie lekcji +— ulepszyliśmy wyjaśnienie na ekranie z miejscem na wpisanie numeru PESEL +— naprawiliśmy rzadkie sytuacje, gdy plan lekcji nakładał się na informację o jego braku Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases diff --git a/app/src/main/res/drawable/ic_menu_attendance_calculator.xml b/app/src/main/res/drawable/ic_menu_attendance_calculator.xml new file mode 100644 index 000000000..8a7d209a7 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_attendance_calculator.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/dialog_auth.xml b/app/src/main/res/layout/dialog_auth.xml index e2e2aa304..a0b9d6ea7 100644 --- a/app/src/main/res/layout/dialog_auth.xml +++ b/app/src/main/res/layout/dialog_auth.xml @@ -32,12 +32,11 @@ android:layout_height="wrap_content" android:layout_marginHorizontal="24dp" android:layout_marginTop="8dp" - android:textSize="16sp" + android:textSize="14sp" app:layout_constraintTop_toBottomOf="@id/auth_title" - app:lineHeight="24sp" + app:lineHeight="18sp" tools:text="@string/auth_description" /> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_login_student_select.xml b/app/src/main/res/layout/fragment_login_student_select.xml index 04c808857..6b603c9f0 100644 --- a/app/src/main/res/layout/fragment_login_student_select.xml +++ b/app/src/main/res/layout/fragment_login_student_select.xml @@ -11,6 +11,18 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_timetable_main_additional.xml b/app/src/main/res/layout/item_timetable_main_additional.xml new file mode 100644 index 000000000..b3aa55d42 --- /dev/null +++ b/app/src/main/res/layout/item_timetable_main_additional.xml @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/pref_target_attendance.xml b/app/src/main/res/layout/pref_target_attendance.xml new file mode 100644 index 000000000..558b0d36f --- /dev/null +++ b/app/src/main/res/layout/pref_target_attendance.xml @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/action_menu_attendance.xml b/app/src/main/res/menu/action_menu_attendance.xml index bb20c8ec2..5c59d2391 100644 --- a/app/src/main/res/menu/action_menu_attendance.xml +++ b/app/src/main/res/menu/action_menu_attendance.xml @@ -1,6 +1,13 @@ + + + + diff --git a/app/src/main/res/values-cs/preferences_values.xml b/app/src/main/res/values-cs/preferences_values.xml index 1590c47ab..c9b2258fc 100644 --- a/app/src/main/res/values-cs/preferences_values.xml +++ b/app/src/main/res/values-cs/preferences_values.xml @@ -1,5 +1,10 @@ + Abecedně + Podle data + Podle průměru + Podle procenta docházky + Podle rovnováhy docházky předmětu Světlý Tmavý @@ -31,11 +36,6 @@ 0,5 0,75 - - Abecedně - Podle data - Podle průměru - Dzienniczek+ Wulkanowy @@ -56,10 +56,15 @@ Pouze mezi lekcemi Před a mezi lekcemi + + Nezobrazovat + Zobrazit v řadě + Zobrazit pod pravidelnými hodinami + Šťastné číslo Nepřečtené zprávy - Frekvence + Docházka Lekce Známky Domácí úkoly diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 58c1d7d3f..c89414a21 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -4,7 +4,7 @@ Přihlášení Wulkanowy Známky - Frekvence + Docházka Zkoušky Plán lekce Nastavení @@ -31,7 +31,7 @@ Semestr %1$d, %2$d/%3$d - Přihlaste se pomocí studentského nebo rodičovského účtu + Přihlaste se pomocí žákovského nebo rodičovského účtu Zadejte symbol ze stránky deníku: <b>%1$s</b> Uživatelské jméno Email @@ -64,7 +64,7 @@ Symbol najdete na stránce deníku v  Uczeń→ Dostęp Mobilny → Wygeneruj kod dostępu.\n\nUjistěte se, že jste nastavili správnou variantu deníku v poli Variace deníku UONET+ na první přihlašovací obrazovce Vyberte žáky, kteří se mají do aplikace přihlásit Jiné možnosti - V tomto režimu nefungují následující: šťastné číslo, statistiky třídy, shrnutí frekvencí, ospravedlnění nepřítomnosti, dokončené lekce, informace o škole a prohlížení seznamu registrovaných zařízení + V tomto režimu nefungují následující: šťastné číslo, statistiky třídy, shrnutí docházky, ospravedlnění nepřítomnosti, dokončené lekce, informace o škole a prohlížení seznamu registrovaných zařízení Tento režim zobrazuje stejná data, která se zobrazují na webových stránkách deníka Kombinace nejlepších vlastností ostatních dvou režimů. Funguje rychleji než scraper a poskytuje funkce, které nejsou k dispozici v režimu Mobile API. Je to v experimentální fázi Ochrana osobních údajů @@ -113,13 +113,17 @@ Komentář Počet nových známek: %1$d Průměr: %1$.2f + Roční: %1$.2f Body: %s Bez průměru + Pololetní průměr + Roční průměr Součet bodů Konečná známka Předpokládaná známka Popisná známka - Vypočítaný průměr + Vypočítaný pololetní průměr + Vypočítaný roční průměr Jak funguje vypočítaný průměr? Vypočítaný průměr je aritmetický průměr vypočítaný z průměrů předmětů. Umožňuje vám to znát přibližný konečný průměr. Vypočítává se způsobem zvoleným uživatelem v nastavení aplikaci. Doporučuje se vybrat příslušnou možnost. Důvodem je rozdílný výpočet školních průměrů. Pokud vaše škola navíc uvádí průměr předmětů na stránce deníku Vulcan, aplikace si je stáhne a tyto průměry nepočítá. To lze změnit vynucením výpočtu průměru v nastavení aplikaci.\n\nPrůměr známek pouze z vybraného semestru:\n1. Výpočet váženého průměru pro každý předmět v daném semestru\n2. Sčítání vypočítaných průměrů\n3. Výpočet aritmetického průměru součtených průměrů\n\nPrůměr průměrů z obou semestrů:\n1. Výpočet váženého průměru pro každý předmět v semestru 1 a 2\n2. Výpočet aritmetického průměru vypočítaných průměrů za semestry 1 a 2 pro každý předmět.\n3. Sčítání vypočítaných průměrů\n4. Výpočet aritmetického průměru sečtených průměrů\n\nPrůměr známek z celého roku:\n1. Výpočet váženého průměru za rok pro každý předmět. Konečný průměr v 1. semestru je nepodstatný.\n2. Sčítání vypočítaných průměrů\n3. Výpočet aritmetického průměru součtených průměrů Jak funguje konečný průměr? @@ -194,6 +198,7 @@ Lekce + Další lekce Učebna Skupina Hodiny @@ -264,7 +269,13 @@ Čas ukončení Čas ukončení musí být pozdější než čas zahájení - Shrnutí frekvencí + Shrnutí docházky + Kalkulačka docházky + %1$d nad cílem + přesně v cíli + %1$d pod cílem + %1$d/%2$d přítomnosti + Nebyla zaznamenána žádná docházka Nepřítomnost ze školních důvodů Omluvená nepřítomnost Neomluvená nepřítomnost @@ -282,22 +293,22 @@ Musíte vybrat alespoň jednu nepřítomnost! Ospravedlnit - Nové frekvence - Nové frekvence - Nové frekvence - Nové frekvence + Nová docházka + Nové docházky + Nové docházky + Nové docházky - %1$d nové frekvence - %1$d nové frekvence - %1$d nových frekvencí - %1$d nových frekvencí + %1$d nová docházka + %1$d nové docházky + %1$d nových docházek + %1$d nových docházek - %d frekvence - %d frekvence - %d frekvencí - %d frekvencí + %d docházka + %d docházky + %d docházek + %d docházek Společně @@ -731,9 +742,13 @@ Možnosti vypočítaného průměru Vynutit průměrný výpočet podle aplikace Zobrazit přítomnost + Cílová docházka + Zobrazit předměty bez docházek + Třídění kalkulačky docházky Motiv Rozvíjení známek Zobrazit skupiny vedle předmětů + Zobrazit další lekce Zobrazit prázdné dlaždice, kde není žádná lekce Zobrazit seznam grafů v známkách třídy Zobrazit předměty bez známek @@ -797,7 +812,9 @@ Známky Domů Viditelnost dlaždic - Frekvence + Docházka + Kalkulačka docházky + Nastavení Plán lekce Známky Vypočítaný průměr @@ -825,7 +842,7 @@ Nadcházející lekce Ladění Změny plánu lekcí - Nové frekvence + Nové docházky Černá Červená @@ -849,7 +866,7 @@ Autorizovat Autorizace byla úspěšně dokončena Autorizace - Pro provoz aplikace potřebujeme potvrdit vaši identitu. Zadejte PESEL žáka <b>%1$s</b> v níže uvedeném poli + Vážený rodiči,<br/><br/>Chcete-li autorizovat a zajistit bezpečnost dat, prosíme Vás, abyste níže zadali PESEL číslo žáka <b>%1$s</b>. Tyto detaily jsou nutné pro správné přidělování přístupu k osobním údajům a jejich ochranu v souladu s platnými předpisy.<br/><br/>Po zadání údajů budou data ověřena, čímž se zajistí, že přístup do systému VULCAN získají pouze autorizované osoby. Pokud máte jakékoliv pochybnosti nebo problémy, kontaktujte prosím školního správce deníku pro objasnění situace.<br/><br/>Udržujeme nejvyšší standardy ochrany osobních údajů a zajišťujeme, aby byly všechny poskytnuté informace chráněné. Wulkanowy neukládá ani nezpracovává číslo PESEL.<br/><br/>Připomínáme, že poskytování úplných a přesných údajů je nutné a nezbytné k používání systému VULCAN. Zatím přeskočit Webová stránka deníku VULCAN vyžaduje ověření diff --git a/app/src/main/res/values-da-rDK/preferences_values.xml b/app/src/main/res/values-da-rDK/preferences_values.xml deleted file mode 100644 index 5aff12dec..000000000 --- a/app/src/main/res/values-da-rDK/preferences_values.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - Light - Dark - Black (AMOLED) - - - System language - Polski - English - Pусский - Українська - Deutsch - Čeština - Slovenčina - - - 15 minutes - 30 minutes - 1 hour - 2 hours - 6 hours - 12 hours - 24 hours - - - 0,00 - 0,25 - 0,33 - 0,5 - 0,75 - - - Alphabetically - By date - By average - - - Dzienniczek+ - Wulkanowy - Grade colors in register - - - Up to 1 at once - Always expanded - Unlimited expansions - - - Average of grades only from selected semester - Average of averages from both semesters - Average of grades from the whole year - - - Don\'t show - Only between lessons - Before and between lessons - - - Lucky number - Unread messages - Attendance - Lessons - Grades - Homework - School announcements - Exams - Conferences - - diff --git a/app/src/main/res/values-de/preferences_values.xml b/app/src/main/res/values-de/preferences_values.xml index d1001c74b..23828b030 100644 --- a/app/src/main/res/values-de/preferences_values.xml +++ b/app/src/main/res/values-de/preferences_values.xml @@ -1,5 +1,10 @@ + Alphabetisch + Nach Datum + Nach Durchschnitt + Nach Anwesenheitsprozent + Nach Subjekt Anwesenheitssaldo Licht Dunkel @@ -31,11 +36,6 @@ 0,5 0,75 - - Alphabetisch - Nach Datum - Nach Durchschnitt - Dzienniczek+ Wulkanowy @@ -56,6 +56,11 @@ Only between lessons Before and between lessons + + Nicht zeigen + Inline anzeigen + Unterhalb der regulären Lektionen anzeigen + Glückszahl Ungelesene Nachrichten diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index daabc7d8f..cc61a83cc 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -10,10 +10,10 @@ Einstellungen Mehr Über die Applikation - Log viewer + Log Viewer Debuggen Benachrichtigungen debuggen - Clear webview cookies + Webview-Cookies löschen Mitarbeiter Lizenzen Nachrichten @@ -38,14 +38,14 @@ Anmeldung, PESEL oder e-mail Passwort UONET+ Registervariante - Custom domain suffix + Benutzerdefinierte Domeisensuffixe Mobile API Scraper Hybride Token PIN Symbol - E.g. \"lodz\" or \"powiatjaroslawski\" + Zum Beispiel \"lodz\" oder \"powiatjaroslawski\" Anmelden Passwort ist zu kurz Anmeldedaten sind falsch @@ -56,9 +56,9 @@ Ungültige email Den zugewiesenen Login anstelle von email verwenden Benutze den zugewiesenen Login oder E-Mail in @%1$s - Invalid domain suffix - Invalid symbol. If you cannot find it, please contact the school - Don\'t make this up! If you cannot find it, please contact the school + Ungültiges Domain-Suffix + Ungültiges Symbol. Wenn Sie es nicht finden können, wenden Sie sich bitte an die Schule + Denken Sie sich das nicht aus! Wenn Sie es nicht finden können, wenden Sie sich bitte an die Schule Schüler nicht gefunden. Überprüfen Sie das Symbol und die gewählte Variation des UONET+ Registers Ausgewählter Student ist bereits angemeldet. Das Symbol kann auf der Registerseite in Student → Tost Möbeln → Registrieren Sie Ihr Mobilgerätgefunden werden.\n\nStellen Sie sicher, dass Sie die entsprechende Registervariante im Feld UONET+ Registervariante auf dem vorherigen Bildschirm festgelegt haben @@ -73,7 +73,7 @@ Discord email senden Stellen Sie sicher, dass Sie die richtige UONET+ Registervariation wählen! - Reset password + Passwort zurücksetzen Ihr Konto wiederherstellen Wiederherstellen Student ist bereits angemeldet @@ -81,13 +81,13 @@ Andere Suchorte Keine aktiven Schüler gefunden Geben Sie ein anderes Symbol ein - Get help - Full school name with the town (required) - Np. ZSTiO Jarosław lub SP nr 99 w Łodzi - Enter correct name of the school - Additional information in Polish (optional) - Np. \"Ostatnio zmieniłem szkołę i…\" albo \"Jestem rodzicem i nie widzę drugiego dziecka…\" - Submit + Hilfe anfragen + Vollschulname mit der Stadt (erforderlich) + Z. B. ZSTiO Jarosław oder SP nr 99 w Łodzi + Geben Sie den richtigen Namen der Schule ein + Zusätzliche Informationen auf Polnisch (fakultativ) + Z. B. „Ich habe kürzlich die Schule gewechselt und...“ oder „Ich bin ein Elternteil und kann das Konto des anderen Kindes nicht sehen...“ + Einreichen Benachrichtigungen aktivieren Aktivieren Sie Benachrichtigungen, damit Sie keine Nachricht vom Lehrer oder eine neue Klasse verpassen @@ -98,8 +98,8 @@ Anmelden Die Sitzung ist abgelaufen Die Sitzung ist abgelaufen, bitte loggen Sie sich erneut ein - Password has expired or been changed - Your account password has expired or been changed. You will need to log in to Wulkanowy again + Das Passwort ist abgelaufen oder wurde geändert + Ihr Passwort ist abgelaufen oder wurde geändert. Sie müssen sich erneut bei Wulkanowy anmelden Anwendungsunterstützung Gefällt Ihnen diese App? Unterstützen Sie ihre Entwicklung, indem Sie nicht-invasive Werbung aktivieren, die Sie jederzeit deaktivieren können Werbung aktivieren @@ -113,13 +113,17 @@ Kommentar Anzahl der neuen Bewertungen: %1$d Durchschnitt: %1$.2f + Jährlich: %1$.2f Punkte: %s Kein Durchschnitt + Semesterdurchschnitt + Jahresdurchschnitt Gesamtpunkte Finaler Note Vorhergesagte Note - Descriptive grade - Berechnender Durchschnitt + Deskriptive Note + Berechneter Semesterdurchschnitt + Berechneter Jahresdurchschnitt Wie funktioniert der berechnete Durchschnitt? Der berechnete Mittelwert ist das arithmetische Mittel, das aus den Durchschnittswerten der Probanden errechnet wird. Es erlaubt Ihnen, den ungefähre endgültigen Durchschnitt zu kennen. Sie wird auf eine vom Anwender in den Anwendungseinstellungen gewählte Weise berechnet. Es wird empfohlen, die entsprechende Option zu wählen. Das liegt daran, dass die Berechnung der Schuldurchschnitte unterschiedlich ist. Wenn Ihre Schule den Durchschnitt der Fächer auf der Vulcan-Seite angibt, lädt die Anwendung diese Fächer herunter und berechnet nicht den Durchschnitt. Dies kann geändert werden, indem die Berechnung des Durchschnitts in den Anwendungseinstellungen erzwungen wird. \n\nDurchschnitt der Noten nur aus dem ausgewählten Semester :\n1. Berechnung des gewichteten Durchschnitts für jedes Fach in einem bestimmten Semester\n2. Addition der berechneten Durchschnittswerte\n3. Berechnung des arithmetischen Mittels der summierten Durchschnitte\nDurchschnitt der Durchschnitte aus beiden Semestern:\n1. Berechnung des gewichteten Durchschnitts für jedes Fach in Semester 1 und 2\n2. Berechnung des arithmetischen Mittels der berechneten Durchschnitte für Semester 1 und 2 für jedes Fach. \n3. Hinzufügen von berechneten Durchschnittswerten\n4. Berechnung des arithmetischen Mittels der summierten Durchschnitte\nDurchschnitt der Noten aus dem ganzen Jahr:\n1. Berechnung des gewichteten Jahresdurchschnitts für jedes Fach. Der Abschlussdurchschnitt im 1. Semester ist irrelevant. \n2. Addition der berechneten Durchschnittswerte\n3. Berechnung des arithmetischen Mittels der summierten Mittelwerte Wie funktioniert der endgültige Durchschnitt? @@ -155,8 +159,8 @@ Neue Abschlussnoten - New descriptive grade - New descriptive grades + Neuer Deskriptive Grade + Neuer Deskriptive Grades Du hast %1$d Note bekommen @@ -171,11 +175,12 @@ Sie haben %1$d Abschlussnoten bekommen - You received %1$d descriptive grade - You received %1$d descriptive grades + Sie haben %1$d deskriptive Grade erhalten + Sie haben %1$d deskriptive Grades erhalten Lektion + Zusätzliche Lektion Klassenzimmer Gruppe Stunden @@ -194,8 +199,8 @@ Wechsel des Lehrers von %1$s zu %2$s Thema von %1$s zu %2$s wechseln - No lesson - No lessons + Keine Lektion + Keine Lektionen Änderung des Zeitplans @@ -237,6 +242,12 @@ Endzeit muss grösser sein als Startzeit Übersicht über die Schulbesuch + Anwesenheitsrechner + %1$d Über Ziel + direkt am ziel + %1$d Unter Ziel + %1$d/%2$d Präsenzen + Keine Anwesenheit verzeichnet Aus schulischen Gründen abwesend Entschuldigte Abwesenheit Unentschuldigtes Abwesenheit @@ -296,10 +307,10 @@ Weiterleiten Alle auswählen Alle abwählen - Restore from trash + Wiederherstellen aus dem Papierkorb In Papierkorb verschieben Dauerhaft löschen - Message restored successfully + Nachricht erfolgreich wiederhergestellt Nachricht erfolgreich gelöscht schüler Eltern @@ -337,10 +348,10 @@ %1$d ausgewählt Nachrichten gelöscht - Messages restored + Wiederhergestellte Nachrichten Postfach auswählen - Incognito mode is on - Thanks to incognito mode sender is not notified when you read the message + Inkognito-Modus ist aktiviert + Dank des Inkognito-Modus wird der Absender nicht benachrichtigt, wenn Sie die Nachricht lesen Keine Informationen über Eintragen Punkte @@ -637,10 +648,14 @@ Berechnete Durchschnittsoptionen Mittelwertberechnung durch App erzwingen Anwesendheit zeigen + Anwesenheitsziel + Lektion ohne Anwesenheit anzeigen + Anwesenheitsrechner Sortierung Thema Steigende Sorten Gruppen neben Schulfächen anzeigen - Show empty tiles where there\'s no lesson + Zusätzliche Lektionen anzeigen + Leere Kacheln anzeigen, wenn es keinen Lektionen gibt Liste der Diagramme in Klassenbewertungen anzeigen Schulfächer ohne Noten anzeigen Farbschema der Noten @@ -681,12 +696,12 @@ Wert des Minus Antwort mit Nachrichtenhistorie Arithmetisches Mittel anzeigen, wenn keine Gewichte angegeben sind - Incognito mode - Do not inform about reading the message + Inkognito-Modus + Nicht über das Lesen der Nachricht informieren Unterstützung Datenschutz-Bestimmungen Vereinbarungen - Show consent to data processing + Einwilligung zur Datenverarbeitung zeigen Anzeigen in der App anzeigen Einzelanzeige ansehen, um Projekt zu unterstützen Einwilligung in die Datenverarbeitung @@ -704,6 +719,8 @@ Dashboard Sichtbarkeit der Kacheln Schulbesuch + Anwesenheits-Rechner + Einstellungen Stundenplan Noten Berechneter Durchschnitt @@ -749,37 +766,37 @@ Die Anwendung muss neu gestartet werden, damit die Änderungen gespeichert werden Restart - Authorization has been rejected. The data provided does not match the records in the secretary\'s office. - Invalid PESEL + Die Autorisierung wurde abgelehnt. Die vorgelegten Daten stimmen nicht mit denen des Sekretariats überein. + Ungültig PESEL PESEL - Authorize - Authorization completed successfully - Authorization - To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below - Skip for now + Autorisieren Sie + Autorisierung erfolgreich abgeschlossen + Autorisierung + Liebes Elternteil,<br/><br/>Um die Sicherheit der Daten zu gewährleisten, bitten wir Sie, die PESEL-Nummer des Schülers/der Schülerin anzugeben<b>%1$s</b>Diese Angaben sind für die ordnungsgemäße Zuweisung des Zugriffs und den Schutz der personenbezogenen Daten gemäß den geltenden Vorschriften unerlässlich.<br/><br/>Nach der Eingabe der Daten werden diese überprüft, um sicherzustellen, dass nur berechtigte Personen Zugang zum VULCAN-System erhalten. Wenn Sie Zweifel oder Probleme haben, wenden Sie sich bitte an den Administrator des Schülerkalenders, um die Situation zu klären.<br/><br/>Wir halten die höchsten Standards für den Schutz personenbezogener Daten ein und gewährleisten, dass alle bereitgestellten Informationen sicher sind. Die Wulkanowy-App speichert und verarbeitet die PESEL-Nummer nicht.<br/><br/>Wir erinnern Sie daran, dass die Angabe vollständiger und korrekter Daten obligatorisch und notwendig für die Nutzung des VULCAN-Systems ist. + Vorerst überspringen - VULCAN\'s website requires verification - Why am I seeing this?\nThe register website from which Wulkanowy downloads data displays the same screen as above, so Wulkanowy must also show it to be able to download data from this website. There\'s no way around it - Verified successfully + VULCAN\'s Website erfordert Überprüfung + Warum sehe ich das?\nDie Website des Registers, von der Wulkanowy Daten herunterlädt, zeigt denselben Bildschirm wie oben an, so dass Wulkanowy ihn ebenfalls anzeigen muss, um Daten von dieser Website herunterladen zu können. Es gibt keinen Ausweg + Erfolgreich verifiziert Keine Internetverbindung Es ist ein Fehler aufgetreten. Überprüfen Sie Ihre Geräteuhr - This account is inactive. Try logging in again + Dieses Konto ist inaktiv. Versuchen Sie, sich erneut anzumelden Registrierungsverbindung fehlgeschlagen. Server können überlastet sein. Bitte versuchen Sie es später noch einmal Das Laden der Daten ist fehlgeschlagen. Bitte versuchen Sie es später noch einmal - Your password has expired or been changed. Please log in again + Ihr Passwort ist abgelaufen oder wurde geändert. Bitte melden Sie sich erneut an Passwortänderung für Registrierung erforderlich Wartung im Gange UONET + Klassenbuch. Versuchen Sie es später noch einmal Unbekannter UONET + Registerfehler. Versuchen Sie es später erneut Unbekannter Anwendungsfehler. Bitte versuchen Sie es später noch einmal - Captcha verification required + Captcha-Verifizierung erforderlich Ein unerwarteter Fehler ist aufgetreten Funktion, die von Ihrer Schule deaktiviert wurde Feature in diesem Modus nicht verfügbar Dieses Feld ist erforderlich - Mute - Unmute - You have muted this user - You have unmuted this user + Stumm + Stummschaltung aufheben + Sie haben diesen Benutzer stummgeschaltet + Sie haben die Stummschaltung dieses Benutzers aufgehoben diff --git a/app/src/main/res/values-es-rES/preferences_values.xml b/app/src/main/res/values-es-rES/preferences_values.xml deleted file mode 100644 index 5aff12dec..000000000 --- a/app/src/main/res/values-es-rES/preferences_values.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - Light - Dark - Black (AMOLED) - - - System language - Polski - English - Pусский - Українська - Deutsch - Čeština - Slovenčina - - - 15 minutes - 30 minutes - 1 hour - 2 hours - 6 hours - 12 hours - 24 hours - - - 0,00 - 0,25 - 0,33 - 0,5 - 0,75 - - - Alphabetically - By date - By average - - - Dzienniczek+ - Wulkanowy - Grade colors in register - - - Up to 1 at once - Always expanded - Unlimited expansions - - - Average of grades only from selected semester - Average of averages from both semesters - Average of grades from the whole year - - - Don\'t show - Only between lessons - Before and between lessons - - - Lucky number - Unread messages - Attendance - Lessons - Grades - Homework - School announcements - Exams - Conferences - - diff --git a/app/src/main/res/values-it-rIT/preferences_values.xml b/app/src/main/res/values-it-rIT/preferences_values.xml deleted file mode 100644 index 5aff12dec..000000000 --- a/app/src/main/res/values-it-rIT/preferences_values.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - Light - Dark - Black (AMOLED) - - - System language - Polski - English - Pусский - Українська - Deutsch - Čeština - Slovenčina - - - 15 minutes - 30 minutes - 1 hour - 2 hours - 6 hours - 12 hours - 24 hours - - - 0,00 - 0,25 - 0,33 - 0,5 - 0,75 - - - Alphabetically - By date - By average - - - Dzienniczek+ - Wulkanowy - Grade colors in register - - - Up to 1 at once - Always expanded - Unlimited expansions - - - Average of grades only from selected semester - Average of averages from both semesters - Average of grades from the whole year - - - Don\'t show - Only between lessons - Before and between lessons - - - Lucky number - Unread messages - Attendance - Lessons - Grades - Homework - School announcements - Exams - Conferences - - diff --git a/app/src/main/res/values-pl/preferences_values.xml b/app/src/main/res/values-pl/preferences_values.xml index 2f2432e98..8eafa1cb4 100644 --- a/app/src/main/res/values-pl/preferences_values.xml +++ b/app/src/main/res/values-pl/preferences_values.xml @@ -1,5 +1,10 @@ + Alfabetycznie + Według daty + Według średniej + Według procentu obecności + Według balansu frekwencji przedmiotu Jasny Ciemny @@ -31,11 +36,6 @@ 0,5 0,75 - - Alfabetycznie - Według daty - Według średniej - Dzienniczek+ Wulkanowy @@ -56,6 +56,11 @@ Tylko między lekcjami Przed i między lekcjami + + Nie pokazuj + Pokaż razem + Pokaż poniżej zwykłych lekcji + Szczęśliwy numerek Nieprzeczytane wiadomości diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 895f7687a..a56a07791 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -198,6 +198,7 @@ Lekcja + Dodatkowa lekcja Sala Grupa Godziny @@ -269,6 +270,12 @@ Godzina zakończenia musi być późniejsza niż godzina rozpoczęcia Podsumowanie frekwencji + Kalkulator obecności + %1$d powyżej celu + dokładnie u celu + %1$d poniżej celu + %1$d/%2$d obecności + Nie odnotowano żadnej frekwencji Nieobecność z przyczyn szkolnych Nieobecność usprawiedliwiona Nieobecność nieusprawiedliwiona @@ -735,9 +742,13 @@ Opcje obliczonej średniej Wymuś obliczanie średniej przez aplikację Pokazuj obecność + Docelowa obecność + Pokazuj przedmioty bez frekwencji + Sortowanie kalkulatora obecności Motyw Rozwijanie ocen Pokazuj grupę obok przedmiotu + Pokaż dodatkowe lekcje Pokazuj puste kafelki gdzie nie ma lekcji Pokazuj listę wykresów w ocenach klasy Pokazuj przedmioty bez ocen @@ -802,6 +813,8 @@ Start Widoczność kafelków Frekwencja + Kalkulator frekwencji + Ustawienia Plan lekcji Oceny Obliczona średnia @@ -853,7 +866,7 @@ Potwierdź Autoryzacja zakończona pomyślnie Autoryzacja - Rodzicu, musimy mieć pewność, że Twój adres e-mail został powiązany z prawidłowym kontem ucznia. W celu autoryzacji konta podaj numer PESEL ucznia <b>%1$s</b> w polu poniżej + Szanowny Rodzicu,<br/><br/>W celu autoryzacji i zapewnienia bezpieczeństwa danych, uprzejmie prosimy o wprowadzenie poniżej numeru PESEL ucznia <b>%1$s</b>. Te informacje są niezbędne do prawidłowego przypisania dostępu i ochrony danych osobowych zgodnie z obowiązującymi przepisami.<br/><br/>Po wprowadzeniu danych, będą one weryfikowane w celu zapewnienia, że dostęp do systemu VULCAN jest przyznawany wyłącznie upoważnionym osobom. W przypadku jakichkolwiek wątpliwości lub problemów, prosimy o kontakt z administratorem dziennika szkolnego w celu wyjaśnienia sytuacji.<br/><br/>Zachowujemy najwyższe standardy ochrony danych osobowych i zapewniamy, że wszelkie przekazane informacje są chronione. Wulkanowy nie przechowuje ani nie przetwarza numeru PESEL.<br/><br/>Przypominamy, że podanie pełnych i prawdziwych danych jest obowiązkowe i konieczne do korzystania z systemu VULCAN. Na razie pomiń Strona dziennika VULCAN wymaga weryfikacji diff --git a/app/src/main/res/values-ru/preferences_values.xml b/app/src/main/res/values-ru/preferences_values.xml index df3629c02..6c7a74aea 100644 --- a/app/src/main/res/values-ru/preferences_values.xml +++ b/app/src/main/res/values-ru/preferences_values.xml @@ -1,5 +1,10 @@ + Alphabetically + By date + By average + By attendance percentage + By subject attendance balance Светлая Тёмная @@ -31,11 +36,6 @@ 0,5 0,75 - - В алфавитном порядке - По дате - По средней - Dzienniczek+ Wulkanowy @@ -56,6 +56,11 @@ Only between lessons Before and between lessons + + Don\'t show + Show inline + Show below regular lessons + Счастливый номер Непрочитанные письма diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 8a5fcc40d..95400f93d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -113,13 +113,17 @@ Комментарий Количество новых оценок: %1$d Средняя оценка: %1$.2f + Annual: %1$.2f Баллы: %s Нет средней оценки + Semester average + Annual average Сумма баллов Итоговая оценка Ожидаемая оценка Descriptive grade - Рассчитанная средняя оценка + Calculated semester average + Calculated annual average Как работает \"Рассчитанная средняя оценка\"? Рассчитанная средняя оценка - это среднее арифметическое, рассчитанное на основе средних оценок по предметам. Это позволяет узнать приблизительную итоговую среднюю оценку. Она рассчитывается способом, выбранным пользователем в настройках приложения. Рекомендуется выбрать подходящий вариант, так как каждая школа по разному считает среднюю оценку. Кроме того, если ваша школа выставляет средние оценки по предметам на странице Vulcan, приложение просто загрузит их. Это можно изменить, заставив приложение считать среднюю оценку в настройках.\n\nСредняя из оценок выбранного семестра:\n1. Вычисление средневзвешенного значения по каждому предмету за семестр\n2.Суммирование вычисленных значений\n3. Вычисление среднего арифметического суммированных значений\n\nСредняя из средних оценок семестров:\n1.Расчет средневзвешенного значения для каждого предмета в семестрах. \n2. Вычисление среднего арифметического из средневзвешенных значений для каждого предмета в семестрах.\n3. Суммирование средних арифметических\n4. Вычисление среднего арифматического из суммированных значений\n\nСредняя из оценок со всего года:\n1. Расчет средневзвешенного значения по каждому предмету за год. Итоговое среднее значение за 1 семестр не имеет значения.\n2. Суммирование вычисленных средних\n3. Расчет среднего арифметического суммированных чисел Как работает \"Итоговая средняя оценка\"? @@ -194,6 +198,7 @@ Урок + Additional lesson Аудитория Группа Часы @@ -265,6 +270,12 @@ Время окончания должно быть больше, чем время начала Итоговая посещаемость + Attendance calculator + %1$d over target + right on target + %1$d under target + %1$d/%2$d presences + No attendances recorded Отсутствие по школьным причинам Отсутствие по уважительной причине Отсутствие по неуважительной причине @@ -731,9 +742,13 @@ Параметры расчёта средних оценок Принудительно высчитать среднюю оценку через приложение Показывать присутствия + Attendance target + Show subjects without any attendances + Attendance calculator sorting Тема Разворачивание оценок Показать группы рядом с темами + Show additional lessons Show empty tiles where there\'s no lesson Показывать диаграммы в оценках класса Показать предметы без оценок @@ -798,6 +813,8 @@ Главная Видимость плиток Посещаемость + Attendance calculator + Settings Расписание Оценки Рассчитанная средняя оценка @@ -849,7 +866,7 @@ Авторизовать Авторизация прошла успешно Авторизация - Для работы приложения нам необходимо подтвердить вашу личность. Введите PESEL учащегося <b>%1$s</b> в поле ниже + Dear Parent,<br /><br />To authorize and ensure the security of data, we kindly ask you to enter below PESEL number of student <b>%1$s</b>. These details are essential for the proper assignment of access and protection of personal data in accordance with applicable regulations.<br /><br />After entering the data, it will be verified to ensure that access to the VULCAN system is granted exclusively to authorized individuals. Should you have any doubts or problems, please contact the school diary administrator to clarify the situation.<br /><br />We maintain the highest standards of personal data protection and ensure that all information provided is secure. Wulkanowy app does not store or process the PESEL number.<br /><br />We remind you that providing full and accurate data is mandatory and necessary for the use of the VULCAN system. Пропустить сейчас VULCAN\'s website requires verification diff --git a/app/src/main/res/values-sk/preferences_values.xml b/app/src/main/res/values-sk/preferences_values.xml index 6cd221540..b06f2b3fa 100644 --- a/app/src/main/res/values-sk/preferences_values.xml +++ b/app/src/main/res/values-sk/preferences_values.xml @@ -1,5 +1,10 @@ + Abecedne + Podľa dátumu + Podľa priemeru + Podľa percenta dochádzky + Podľa rovnováhy dochádzky predmetu Svetlý Tmavý @@ -31,11 +36,6 @@ 0,5 0,75 - - Abecedne - Podľa dátumu - Podľa priemeru - Dzienniczek+ Wulkanowy @@ -56,10 +56,15 @@ Iba medzi lekciami Pred a medzi lekciami + + Nezobrazovať + Zobraziť v rade + Zobraziť pod pravidelnými hodinami + Šťastné číslo Neprečítané správy - Frekvencia + Dochádzka Lekcie Známky Domáce úlohy diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 0f5215665..981213842 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -4,7 +4,7 @@ Prihlásenie Wulkanowy Známky - Frekvencia + Dochádzka Skúšky Plán lekcie Nastavenia @@ -31,7 +31,7 @@ Semester %1$d, %2$d/%3$d - Prihláste sa pomocou študentského alebo rodičovského konta + Prihláste sa pomocou žiackeho alebo rodičovského účtu Zadajte symbol zo stránky denníka: <b>%1$s</b> Užívateľské meno Email @@ -64,7 +64,7 @@ Symbol nájdete na stránke denníka v  Uczeń→ Dostęp Mobilny → Wygeneruj kod dostępu.\n\nUistite sa, že ste nastavili správny variant denníka v poli Variácia denníka UONET+ na prvej prihlasovacej obrazovke Vyberte žiakov, ktorí sa majú do aplikácie prihlásiť Iné možnosti - V tomto režime nefungujú nasledovné: šťastné číslo, štatistiky triedy, zhrnutie frekvencií, ospravedlnenie neprítomnosti, dokončené lekcie, informácie o škole a prezeranie zoznamu registrovaných zariadení + V tomto režime nefungujú nasledovné: šťastné číslo, štatistiky triedy, zhrnutie dochádzky, ospravedlnenie neprítomnosti, dokončené lekcie, informácie o škole a prezeranie zoznamu registrovaných zariadení Tento režim zobrazuje rovnaké dáta, ktoré sa zobrazujú na webových stránkach denníka Kombinácia najlepších vlastností ostatných dvoch režimov. Funguje rýchlejšie ako scraper a poskytuje funkcie, ktoré nie sú k dispozícii v režime Mobilne API. Je to v experimentálnej fáze Ochrana osobných údajov @@ -113,13 +113,17 @@ Komentár Počet nových známok %1$d Priemer: %1$.2f + Ročný: %1$.2f Body: %s Bez priemeru + Polročný priemer + Ročný priemer Súčet bodov Konečná známka Predpokladaná známka Popisná známka - Vypočítaný priemer + Vypočítaný polročný priemer + Vypočítaný ročný priemer Ako funguje vypočítaný priemer? Vypočítaný priemer je aritmetický priemer vypočítaný z priemerov predmetov. Umožňuje vám to poznať približný konečný priemer. Vypočítava sa spôsobom zvoleným užívateľom v nastaveniach aplikácii. Odporúča sa vybrať príslušnú možnosť. Dôvodom je rozdielny výpočet školských priemerov. Ak vaša škola navyše uvádza priemer predmetov na stránke denníka Vulcan, aplikácia si ich stiahne a tieto priemery nepočíta. To možno zmeniť vynútením výpočtu priemeru v nastavení aplikácii.\n\nPriemer známok iba z vybraného semestra:\n1. Výpočet váženého priemeru pre každý predmet v danom semestri\n2. Sčítanie vypočítaných priemerov\n3. Výpočet aritmetického priemeru součtených priemerov\n\nPriemer priemerov z oboch semestrov:\n1. Výpočet váženého priemeru pre každý predmet v semestri 1 a 2\n2. Výpočet aritmetického priemeru vypočítaných priemerov za semestre 1 a 2 pre každý predmet.\n3. Sčítanie vypočítaných priemerov\n4. Výpočet aritmetického priemeru součtených priemerov\n\nPriemer známok z celého roka:\n1. Výpočet váženého priemeru za rok pre každý predmet. Konečný priemer v 1. semestri je nepodstatný.\n2. Sčítanie vypočítaných priemerov\n3. Výpočet aritmetického priemeru součtených priemerov Ako funguje konečný priemer? @@ -194,6 +198,7 @@ Lekcia + Ďalšia lekcia Učebňa Skupina Hodiny @@ -264,7 +269,13 @@ Čas ukončenia Čas ukončenia musí byť neskorší ako čas začatia - Zhrnutie frekvencií + Zhrnutie dochádzky + Kalkulačka dochádzky + %1$d nad cieľom + presne v cieli + %1$d pod cieľom + %1$d/%2$d prítomnosti + Nebola zaznamenaná žiadna dochádzka Neprítomnosť zo školských dôvodov Ospravedlnená neprítomnosť Neospravedlnená neprítomnosť @@ -282,22 +293,22 @@ Musíte vybrať aspoň jednu neprítomnosť! Ospravedlniť - Nová frekvencia - Nové frekvencie - Nové frekvencie - Nové frekvencie + Nová dochádzka + Nové dochádzky + Nové dochádzky + Nové dochádzky - %1$d nová frekvencia - %1$d nové frekvencie - %1$d nových frekvencií - %1$d nových frekvencií + %1$d nová dochádzka + %1$d nové dochádzky + %1$d nových dochádzok + %1$d nových dochádzok - %d frekvencia - %d frekvencie - %d frekvencií - %d frekvencií + %d dochádzka + %d dochádzky + %d dochádzok + %d dochádzok Spoločne @@ -731,9 +742,13 @@ Možnosti vypočítaného priemeru Vynútiť priemerný výpočet podľa aplikácie Zobraziť prítomnosť + Cieľová dochádzka + Zobraziť predmety bez dochádzok + Triedenie kalkulačky dochádzky Motív Rozvijanie známok Zobraziť skupiny vedľa predmetov + Zobraziť ďalšie lekcie Zobraziť prázdne dlaždice, kde nie je žiadne lekcie Zobraziť zoznam grafov v známkach triedy Zobraziť predmety bez známok @@ -797,7 +812,9 @@ Známky Domov Viditeľnosť dlaždíc - Frekvencia + Dochádzka + Kalkulačka dochádzky + Nastavenia Plán lekcie Známky Vypočítaný priemer @@ -825,7 +842,7 @@ Nadchádzajúce lekcie Ladenie Zmeny plánu lekcií - Nové frekvencie + Nové dochádzky Čierna Červená @@ -849,7 +866,7 @@ Autorizovať Autorizácia bola úspešne dokončená Autorizácia - Na prevádzku aplikácie potrebujeme potvrdiť vašu identitu. Zadajte PESEL žiaka <b>%1$s</b> v nižšie uvedenom poli + Vážený rodiči,<br/><br/>Ak chcete autorizovať a zaistiť bezpečnosť dát, prosíme Vás, aby ste nižšie zadali PESEL číslo žiaka <b>%1$s</b>. Tieto detaily sú nutné pre správne prideľovanie prístupu k osobným údajom a ich ochranu v súlade s platnými predpismi.<br/><br/>Po zadaní údajov budú dáta overené, čím sa zaistí, že prístup do systému VULCAN získajú iba autorizované osoby. Pokiaľ máte akékoľvek pochybnosti alebo problémy, kontaktujte prosím školského správcu denníka pre objasnenie situácie.<br/><br/>Udržujeme najvyššie štandardy ochrany osobných údajov a zaisťujeme, aby boli všetky poskytnuté informácie chránené. Wulkanowy neukladá ani nespracováva číslo PESEL.<br/><br/>Pripomíname, že poskytovanie úplných a presných údajov je nutné a nevyhnutné na používanie systému VULCAN. Zatiaľ preskočiť Webová stránka denníka VULCAN vyžaduje overenie diff --git a/app/src/main/res/values-uk/preferences_values.xml b/app/src/main/res/values-uk/preferences_values.xml index c02efb54a..72abe1f70 100644 --- a/app/src/main/res/values-uk/preferences_values.xml +++ b/app/src/main/res/values-uk/preferences_values.xml @@ -1,5 +1,10 @@ + За алфавітом + За датою + За середньою + За відсотком відвідуваності + За балансом відвідування теми Світла Темна @@ -31,11 +36,6 @@ 0,5 0,75 - - За алфавітом - За датою - За середньою - Dzienniczek+ Wulkanowy @@ -56,6 +56,11 @@ Тільки між уроками Перед і між уроками + + Не показувати + Показати у рядку + Показати нижче стандартних уроків + Щасливий номер Непрочитані листи diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index a0d4b6c0b..2ce0a2d9f 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -113,13 +113,17 @@ Коментар Кількість нових оцінок: %1$d Середня оцінка: %1$.2f + Підсумкова: %1$.2f Бали: %s Середня оцінка відсутня + Середня за семестр + Підсумкова середня оцінка Всього балів Підсумкова оцінка Передбачувана оцінка Описова оцінка - Розрахована середня оцінка + Розрахована середня за семестр + Розрахована підсумкова середня оцінка Як працює \"Розрахована середня оцінка\"? Розрахована середня оцінка - це середнє арифметичне, обчислене з середніх оцінок з предметів. Це дозволяє дізнатися приблизну кінцеву середню оцінку. Вона розраховується спосібом, обраним користувачем у налаштуваннях програми. Рекомендується вибрати відповідний варіант, тому що кожна школа по різному розраховує середню оцінку. Крім того, якщо у вашій школі повідомляється середня оцінка з предметів на сторінці Vulcan, програма тільки завантажує ці оцінки і не розраховує їх самостійно. Це можна змінити шляхом примусового розрахунку середньоЇ оцінки в налаштуваннях програми.\n\nСередні оцінки тільки за обраний семестр:\n1. Розрахунок середньозваженого числа для кожного предмета в даному семестрі\n2. Сумування розрахованих числ\n3. Розрахунок середнього арифметичного з сумованих чисел\n\nСереднє значення з обох семестрів:\n1. Обчислення середньозваженого числа для кожного предмета у 1 та 2 семестрі\n2. Обчислення середнього арифметичного з розрахованих середньозважених числ за 1 та 2 семестри для кожного предмета.\n3. Додавання розрахованих середніх\n4. Розрахунок середнього арифметичного підсумованих середніх значень\n\nСереднє значення оцінок за весь рік: \n1. Розрахунок середньозваженого числа за рік для кожного предмета. Підсумковий середній показник у 1-му семестрі не має значення.\n2. Сумування розрахованих середніх\n3. Обчислення середнього арифметичного з суммованих середніх Як працює \"Підсумкова середня оцінка\"? @@ -194,6 +198,7 @@ Урок + Додатковий урок Аудиторія Група Години @@ -265,6 +270,12 @@ Час завершення має бути пізніше часу початку Підсумок відвідуваності + Калькулятор відвідуваності + %1$d понад ціль + точно у цілі + %1$d під ціллю + %1$d/%2$d відвідуваності + Немає жодних записаних відвідувань Відсутність зі шкільних причин Відсутність з поважних причин Відсутність без поважних причин @@ -731,9 +742,13 @@ Параметри розраховування середніх оцінок Примусово розраховувати середню оцінку через додаток Показувати присутність + Цільова відвідуваність + Показувати уроки без відвідувань + Сортування калькулятора відвідування Тема Розгортання оцінок Показувати групи поруч з темами + Показати додаткові уроки Показувати порожні плитки там, де немає уроків Показувати діаграми в оцінках класу Показати предмети без оцінок @@ -798,6 +813,8 @@ Головна Видимість плиток Відвідуваність + Калькулятор відвідуваності + Налаштування Розклад Оцінки Розрахована середня оцінка @@ -849,7 +866,7 @@ Авторизовать Авторизація пройшла успішно Авторизувати - Для роботи програми нам потрібно підтвердити вашу особу. Будь ласка, введіть число PESEL <b>%1$s</b> студента в поле нижче + Шановні батьки,<br/><br/>Для авторизації та забезпечення безпеки даних просимо Вас ввести нижче PESEL номер учня <b>%1$s</b>. Ці дані необхідні для правильного призначення доступу та захисту персональних даних відповідно до чинного законодавства.<br/><br/>Після введення даних буде проведена перевірка, щоб переконатися, що доступ до системи VULCAN надається виключно уповноваженим особам. У разі виникнення будь-яких сумнівів або проблем, будь ласка, зв\'яжіться з адміністратором шкільного щоденника для з\'ясування ситуації.<br/><br/>Ми підтримуємо найвищі стандарти захисту персональних даних і гарантуємо, що вся надана інформація є безпечною. Додаток Wulkanowy не зберігає і не обробляє номер PESEL.<br/><br/>Нагадуємо, що надання повних і точних даних є обов\'язковим і необхідним для використання системи VULCAN. Поки що пропустити Веб-сайт VULCAN потребує підтвердження diff --git a/app/src/main/res/values/preferences_defaults.xml b/app/src/main/res/values/preferences_defaults.xml index 8e6fc7d66..2177b25f0 100644 --- a/app/src/main/res/values/preferences_defaults.xml +++ b/app/src/main/res/values/preferences_defaults.xml @@ -2,6 +2,9 @@ 0 true + 50 + alphabetic + false only_one_semester false one @@ -19,7 +22,8 @@ 0.33 0.33 true - false + true + below no alphabetic between diff --git a/app/src/main/res/values/preferences_keys.xml b/app/src/main/res/values/preferences_keys.xml index 74af9262c..ce8773081 100644 --- a/app/src/main/res/values/preferences_keys.xml +++ b/app/src/main/res/values/preferences_keys.xml @@ -1,7 +1,11 @@ default_menu_index + attendance_calculator attendance_present + attendance_target + attendance_calculator_sorting_mode + attendance_calculator_show_empty_subjects app_theme dashboard_tiles grade_color_scheme @@ -27,6 +31,7 @@ grade_sorting_mode show_whole_class_plan show_groups_in_plan + show_additional_lessons timetable_show_gaps subjects_without_grades optional_arithmetic_average diff --git a/app/src/main/res/values/preferences_values.xml b/app/src/main/res/values/preferences_values.xml index f56707c89..e5020b533 100644 --- a/app/src/main/res/values/preferences_values.xml +++ b/app/src/main/res/values/preferences_values.xml @@ -1,5 +1,12 @@ + + Alphabetically + By date + By average + By attendance percentage + By subject attendance balance + @string/dashboard_title @string/grade_title @@ -79,10 +86,21 @@ 0.75 - - Alphabetically - By date - By average + + @string/sort_alphabetically + @string/sort_by_attendance_percentage + @string/sort_by_subject_attendance_balance + + + alphabetic + attendance_percentage + lesson_balance + + + + @string/sort_alphabetically + @string/sort_by_date + @string/sort_by_average alphabetic @@ -134,6 +152,17 @@ before_and_between + + Don\'t show + Show inline + Show below regular lessons + + + none + inline + below + + Lucky number Unread messages diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 957ad6de8..ed4f438c6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -195,6 +195,7 @@ Lesson + Additional lesson Room Group Hours @@ -262,6 +263,12 @@ Attendance summary + Attendance calculator + %1$d over target + right on target + %1$d under target + %1$d/%2$d presences + No attendances recorded Absent for school reasons Excused absence Unexcused absence @@ -719,9 +726,13 @@ Calculated average options Force average calculation by app Show presence + Attendance target + Show subjects without any attendances + Attendance calculator sorting Theme Grades expanding Show groups next to subjects + Show additional lessons Show empty tiles where there\'s no lesson Show chart list in class grades Show subjects without grades @@ -787,6 +798,8 @@ Dashboard Tiles visibility Attendance + Attendance calculator + Settings Timetable Grades Calculated average @@ -847,7 +860,7 @@ Authorize Authorization completed successfully Authorization - To operate the application, we need to confirm your identity. Please enter the student\'s PESEL <b>%1$s</b> in the field below + Dear Parent,<br /><br />To authorize and ensure the security of data, we kindly ask you to enter below PESEL number of student <b>%1$s</b>. These details are essential for the proper assignment of access and protection of personal data in accordance with applicable regulations.<br /><br />After entering the data, it will be verified to ensure that access to the VULCAN system is granted exclusively to authorized individuals. Should you have any doubts or problems, please contact the school diary administrator to clarify the situation.<br /><br />We maintain the highest standards of personal data protection and ensure that all information provided is secure. Wulkanowy app does not store or process the PESEL number.<br /><br />We remind you that providing full and accurate data is mandatory and necessary for the use of the VULCAN system. Skip for now diff --git a/app/src/main/res/xml/scheme_preferences_appearance.xml b/app/src/main/res/xml/scheme_preferences_appearance.xml index 9c02a4910..55f425d99 100644 --- a/app/src/main/res/xml/scheme_preferences_appearance.xml +++ b/app/src/main/res/xml/scheme_preferences_appearance.xml @@ -86,6 +86,34 @@ app:key="@string/pref_key_attendance_present" app:title="@string/pref_view_present" /> + + + + + + diff --git a/app/src/play/AndroidManifest.xml b/app/src/play/AndroidManifest.xml new file mode 100644 index 000000000..38f6306fe --- /dev/null +++ b/app/src/play/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/app/src/release/google-services.json b/app/src/release/google-services.json deleted file mode 100644 index ebd157e1d..000000000 --- a/app/src/release/google-services.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "project_info": { - "project_number": "", - "firebase_url": "", - "project_id": "", - "storage_bucket": "" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:1091101852179:android:b558a25f65d088b1", - "android_client_info": { - "package_name": "io.github.wulkanowy" - } - }, - "oauth_client": [ - { - "client_id": "", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "" - } - ], - "services": { - "analytics_service": { - "status": 1 - }, - "appinvite_service": { - "status": 1, - "other_platform_oauth_client": [] - }, - "ads_service": { - "status": 2 - } - } - } - ], - "configuration_version": "1" -} diff --git a/app/src/test/java/io/github/wulkanowy/data/ResourceTest.kt b/app/src/test/java/io/github/wulkanowy/data/ResourceTest.kt index ea846a57b..aa79a637b 100644 --- a/app/src/test/java/io/github/wulkanowy/data/ResourceTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/ResourceTest.kt @@ -1,6 +1,10 @@ package io.github.wulkanowy.data -import io.mockk.* +import io.mockk.Runs +import io.mockk.coEvery +import io.mockk.coVerifyOrder +import io.mockk.just +import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flowOf @@ -42,7 +46,6 @@ class ResourceTest { // first networkBoundResource( isResultEmpty = { false }, - showSavedOnLoading = false, query = { repo.query() }, fetch = { val data = repo.fetch() @@ -57,7 +60,6 @@ class ResourceTest { // second networkBoundResource( isResultEmpty = { false }, - showSavedOnLoading = false, query = { repo.query() }, fetch = { val data = repo.fetch() @@ -124,7 +126,6 @@ class ResourceTest { networkBoundResource( isResultEmpty = { false }, mutex = saveResultMutex, - showSavedOnLoading = false, query = { repo.query() }, fetch = { val data = repo.fetch() @@ -143,7 +144,6 @@ class ResourceTest { networkBoundResource( isResultEmpty = { false }, mutex = saveResultMutex, - showSavedOnLoading = false, query = { repo.query() }, fetch = { val data = repo.fetch() diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/LuckyNumberRemoteTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/LuckyNumberRemoteTest.kt index 854d5d548..cec16ca08 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/LuckyNumberRemoteTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/LuckyNumberRemoteTest.kt @@ -8,6 +8,7 @@ import io.github.wulkanowy.data.mappers.mapToEntity import io.github.wulkanowy.data.toFirstResult import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk +import io.github.wulkanowy.utils.AppWidgetUpdater import io.mockk.MockKAnnotations import io.mockk.Runs import io.mockk.coEvery @@ -32,6 +33,9 @@ class LuckyNumberRemoteTest { @MockK private lateinit var luckyNumberDb: LuckyNumberDao + @MockK(relaxed = true) + private lateinit var appWidgetUpdater: AppWidgetUpdater + private val student = getStudentEntity() private lateinit var luckyNumberRepository: LuckyNumberRepository @@ -44,7 +48,11 @@ class LuckyNumberRemoteTest { fun setUp() { MockKAnnotations.init(this) - luckyNumberRepository = LuckyNumberRepository(luckyNumberDb, wulkanowySdkFactory) + luckyNumberRepository = LuckyNumberRepository( + luckyNumberDb = luckyNumberDb, + wulkanowySdkFactory = wulkanowySdkFactory, + appWidgetUpdater = appWidgetUpdater, + ) } @Test diff --git a/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt b/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt index a14605435..2a45f1755 100644 --- a/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt +++ b/app/src/test/java/io/github/wulkanowy/data/repositories/TimetableRepositoryTest.kt @@ -12,6 +12,7 @@ import io.github.wulkanowy.getSemesterEntity import io.github.wulkanowy.getStudentEntity import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper +import io.github.wulkanowy.utils.AppWidgetUpdater import io.github.wulkanowy.utils.AutoRefreshHelper import io.mockk.MockKAnnotations import io.mockk.Runs @@ -53,6 +54,9 @@ class TimetableRepositoryTest { @MockK(relaxUnitFun = true) private lateinit var refreshHelper: AutoRefreshHelper + @MockK(relaxed = true) + private lateinit var appWidgetUpdater: AppWidgetUpdater + private val student = getStudentEntity() private val semester = getSemesterEntity() @@ -74,7 +78,8 @@ class TimetableRepositoryTest { timetableHeaderDao, wulkanowySdkFactory, timetableNotificationSchedulerHelper, - refreshHelper + refreshHelper, + appWidgetUpdater ) } diff --git a/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt b/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt index d03337dc3..5ef7a295b 100644 --- a/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt +++ b/app/src/test/java/io/github/wulkanowy/ui/modules/login/studentselect/LoginStudentSelectPresenterTest.kt @@ -5,8 +5,10 @@ import io.github.wulkanowy.data.pojos.RegisterStudent import io.github.wulkanowy.data.pojos.RegisterSymbol import io.github.wulkanowy.data.pojos.RegisterUnit import io.github.wulkanowy.data.pojos.RegisterUser +import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.SchoolsRepository import io.github.wulkanowy.data.repositories.StudentRepository +import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.scrapper.Scrapper import io.github.wulkanowy.services.sync.SyncManager @@ -44,6 +46,12 @@ class LoginStudentSelectPresenterTest { @MockK lateinit var schoolsRepository: SchoolsRepository + @MockK + lateinit var preferencesRepository: PreferencesRepository + + @MockK + lateinit var getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase + @MockK(relaxed = true) lateinit var analytics: AnalyticsHelper @@ -132,6 +140,8 @@ class LoginStudentSelectPresenterTest { syncManager = syncManager, analytics = analytics, appInfo = appInfo, + preferencesRepository = preferencesRepository, + getAppropriateAdminMessageUseCase = getAppropriateAdminMessageUseCase ) } diff --git a/build.gradle b/build.gradle index ec19ee49a..100c0eb9e 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,8 @@ buildscript { ext { - kotlin_version = '1.9.22' - about_libraries = '11.1.0' - hilt_version = '2.51' + kotlin_version = '1.9.23' + about_libraries = '11.1.3' + hilt_version = '2.51.1' } repositories { mavenCentral() @@ -13,15 +13,15 @@ buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" - classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-1.0.16" - classpath 'com.android.tools.build:gradle:8.2.2' + classpath "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin:$kotlin_version-1.0.19" + classpath 'com.android.tools.build:gradle:8.3.2' classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" classpath 'com.google.gms:google-services:4.4.1' classpath 'com.huawei.agconnect:agcp:1.9.1.303' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9' classpath "com.github.triplet.gradle:play-publisher:3.8.4" classpath "ru.cian:huawei-publish-gradle-plugin:1.4.2" - classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:4.4.1.3373" + classpath "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:5.0.0.4638" classpath "gradle.plugin.com.star-zero.gradle:githook:1.2.0" classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libraries" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135c4..d64cd4917 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e6aba2515..2ea3535dc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME