1
0

Compare commits

..

48 Commits

Author SHA1 Message Date
2e71c50894 Merge branch 'bugfix/2.5.3' 2024-03-24 23:43:51 +01:00
b3faac01a5 Version 2.5.3 2024-03-24 23:43:45 +01:00
3881678208 Fix issues related to not authenticated eduOne students (#2501) 2024-03-24 23:22:07 +01:00
76d038eefa Hide grade statistics when is eduOne, add isEduOne flag migration (#2496)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2024-03-24 19:42:36 +01:00
3a55c3c760 Add ignoring FeatureUnavailableException in SyncWorker (#2500) 2024-03-24 18:55:23 +01:00
a0818de7d1 Add isAdded condition to HomeworkAddDialog (#2499) 2024-03-24 14:55:05 +01:00
b280316b07 Bump sdk to 2.5.3-SNAPSHOT 2024-03-21 23:06:18 +01:00
0554aa91fd Add WulkanowySdkFactory (#2479) 2024-03-21 22:11:03 +01:00
5a77d1e940 Hide lesson number when is eduOne (#2498) 2024-03-21 21:19:49 +01:00
c9a42a6cf6 Add try catch to initialize MobileAds SDK (#2497) 2024-03-21 11:03:08 +01:00
e17129efea Merge branch 'bugfix/2.5.2' 2024-03-20 02:18:07 +01:00
6047af9ff0 Version 2.5.2 2024-03-20 02:18:00 +01:00
d789aa718e Change AuthDialog condition to isAuth flag (#2495)
Co-authored-by: Mikołaj Pich <m.pich@outlook.com>
2024-03-20 01:49:55 +01:00
8623b53357 Fix lateness color in attendance (#2481) 2024-03-19 22:13:32 +01:00
78e28ad791 Remove savedInstance in MessagePreviewFragment (#2477) 2024-03-19 22:13:09 +01:00
377c288e9e Add missing onDetachView in AutDialog (#2476) 2024-03-19 22:12:54 +01:00
b31c7e1720 Fix task description color crash (#2475) 2024-03-19 22:12:39 +01:00
d01fe9c370 Bump sdk to 2.5.2-SNAPSHOT 2024-03-19 22:11:02 +01:00
5ed19cb21a Merge branch 'release/2.5.1' 2024-03-03 11:19:42 +01:00
7b2c839775 Merge branch 'release/2.5.0' 2024-03-02 21:18:10 +01:00
3eae3a7667 Merge branch 'release/2.4.2' 2024-02-22 16:15:30 +01:00
90a5b9e20f Merge branch 'release/2.4.1' 2024-02-17 13:10:34 +01:00
b99ba48d2c Merge branch 'release/2.4.0' 2024-02-09 19:45:01 +01:00
496695162d Merge branch 'release/2.3.5' 2024-01-21 21:04:29 +01:00
1fe464a289 Merge branch 'release/2.3.4' 2024-01-14 17:33:14 +01:00
17c139b559 Merge branch 'release/2.3.3' 2024-01-09 21:46:04 +01:00
f893170dec Merge branch 'release/2.3.2' 2024-01-09 19:31:22 +01:00
770749e158 Merge branch 'release/2.3.1' 2024-01-03 16:02:32 +01:00
eb31f9578f Merge branch 'release/2.3.0' 2024-01-02 01:51:52 +01:00
a2a7d2ebb2 Merge branch 'release/2.2.7' 2023-12-27 22:21:13 +01:00
c64be2fab0 Merge branch 'release/2.2.6' 2023-11-06 11:22:19 +01:00
d9bab2af78 Merge branch 'release/2.2.5' 2023-11-03 23:05:42 +01:00
aba08e6aa9 Merge branch 'release/2.2.4' 2023-10-27 14:49:46 +02:00
387ff1cba7 Merge branch 'release/2.2.3' 2023-10-26 18:31:56 +02:00
6071b7571b Merge branch 'release/2.2.2' 2023-10-23 20:02:25 +02:00
bcd305bef3 Merge branch 'release/2.2.1' 2023-10-03 01:14:17 +02:00
1d8378e136 Merge branch 'release/2.2.0' 2023-09-26 23:13:40 +02:00
af346842a3 Merge branch 'release/2.1.0' 2023-08-25 00:01:42 +02:00
391f38485d Merge branch 'release/2.0.8' 2023-06-01 23:43:06 +02:00
fd482777e8 Merge branch 'release/2.0.7' 2023-06-01 10:59:55 +02:00
cc46b3b124 Merge branch 'release/2.0.6' 2023-05-23 16:26:36 +02:00
c40cdf88ad Merge branch 'release/2.0.5' 2023-05-23 02:37:43 +02:00
5c440010e2 Merge branch 'release/2.0.4' 2023-05-22 17:10:02 +02:00
d08f195968 Merge branch 'release/2.0.3' 2023-05-12 22:59:45 +02:00
88c38c4a8d Merge branch 'release/2.0.2' 2023-05-12 00:45:55 +02:00
9697a39464 Merge branch 'release/2.0.1' 2023-05-12 00:21:28 +02:00
18dbbba328 Merge branch 'release/2.0.0' 2023-05-07 23:40:25 +02:00
c4672b8de9 Merge branch 'hotfix/1.9.2' 2023-03-08 21:28:57 +01:00
94 changed files with 6167 additions and 1330 deletions

View File

@ -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 -PdisablePreDex
command: ./gradlew publishPlayRelease --no-daemon --stacktrace --console=plain -PenableCrashlytics -PdisablePreDex
workflows:
version: 2

View File

@ -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 --stacktrace;
run: ./gradlew publishPlayReleaseApps -PenableFirebase --stacktrace;
deploy-app-gallery:
name: AppGallery

View File

@ -36,7 +36,8 @@ 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/google-services.json
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 '/versionNameSuffix/d' app/build.gradle
- name: Add signing config
run: |
@ -130,7 +131,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 --stacktrace
run: ./gradlew assemblePlayDebug -PenableFirebase --stacktrace
- name: Upload apk to github artifacts
uses: actions/upload-artifact@v3
with:

11
.gitignore vendored
View File

@ -117,13 +117,12 @@ 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

View File

@ -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 --stacktrace;
./gradlew publishPlayRelease -PenableFirebase --stacktrace;
fi
after_success:

View File

@ -27,12 +27,15 @@ android {
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21
targetSdkVersion 34
versionCode 150
versionName "2.5.1"
versionCode 152
versionName "2.5.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy"
manifestPlaceholders = [admob_project_id: ""]
manifestPlaceholders = [
firebase_enabled: project.hasProperty("enableFirebase"),
admob_project_id: ""
]
buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null"
buildConfigField "String", "DASHBOARD_TILE_AD_ID", "null"
@ -73,6 +76,7 @@ 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"'
}
@ -160,8 +164,8 @@ play {
defaultToAppBundles = false
track = 'production'
releaseStatus = ReleaseStatus.IN_PROGRESS
userFraction = 0.50d
updatePriority = 1
userFraction = 0.20d
updatePriority = 3
enabled.set(false)
}
@ -191,7 +195,7 @@ ext {
}
dependencies {
implementation 'io.github.wulkanowy:sdk:2.5.1'
implementation 'io.github.wulkanowy:sdk:2.5.3'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
@ -248,13 +252,13 @@ dependencies {
implementation 'com.fredporciuncula:flow-preferences:1.9.1'
implementation 'org.apache.commons:commons-text:1.11.0'
playImplementation platform('com.google.firebase:firebase-bom:32.7.4')
playImplementation platform('com.google.firebase:firebase-bom:32.7.3')
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:23.0.0'
playImplementation 'com.google.android.gms:play-services-ads:22.6.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'

View File

@ -1,8 +1,7 @@
#!/bin/bash -
content=$(cat < "app/src/main/play/release-notes/pl-PL/default.txt") || exit
content2=echo "$content" | dos2unix
if [[ "${#content2}" -gt 500 ]]; then
if [[ "${#content}" -gt 500 ]]; then
echo >&2 "Release notes content has reached the limit of 500 characters"
exit 1
fi

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,92 @@
{
"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"
}
}
]
}

View File

@ -155,9 +155,33 @@
android:resource="@xml/provider_paths" />
</provider>
<!-- workaround for https://github.com/firebase/firebase-android-sdk/issues/473 enabled:false -->
<!-- https://firebase.googleblog.com/2017/03/take-control-of-your-firebase-init-on.html -->
<provider
android:name="com.google.firebase.provider.FirebaseInitProvider"
android:authorities="${applicationId}.firebaseinitprovider"
android:enabled="${firebase_enabled}"
android:exported="false"
tools:ignore="MissingClass" />
<meta-data
android:name="install_channel"
android:value="${install_channel}" />
<meta-data
android:name="firebase_analytics_collection_enabled"
android:value="${firebase_enabled}" />
<meta-data
android:name="google_analytics_adid_collection_enabled"
android:value="${firebase_enabled}" />
<meta-data
android:name="firebase_crashlytics_collection_enabled"
android:value="${firebase_enabled}" />
<meta-data
android:name="firebase_messaging_auto_init_enabled"
android:value="${firebase_enabled}" />
<meta-data
android:name="firebase_inapp_messaging_auto_data_collection_enabled"
android:value="${firebase_enabled}" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_stat_all" />

View File

@ -1,17 +1,11 @@
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
@ -20,39 +14,16 @@ 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 interface Resource<out T> {
/**
* 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<T> : Resource<T>
sealed class Resource<T> {
open class Loading<T> : Resource<T>()
/**
* 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<T>(val data: T) : Loading<T>()
/**
* 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<T>(val data: T) : Resource<T>
data class Success<T>(val data: T) : Resource<T>()
/**
* 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<T>(val error: Throwable) : Resource<T>
data class Error<T>(val error: Throwable) : Resource<T>()
}
val <T> Resource<T>.dataOrNull: T?
@ -93,22 +64,6 @@ fun <T, U> Resource<T>.mapData(block: (T) -> U) = when (this) {
is Resource.Error -> Resource.Error(this.error)
}
/**
* Injects another flow into this flow's resource data.
*/
inline fun <T1, T2, R> Flow<Resource<T1>>.combineWithResourceData(
flow: Flow<T2>,
crossinline block: suspend (T1, T2) -> R
): Flow<Resource<R>> =
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 <T> Flow<Resource<T>>.logResourceStatus(name: String, showData: Boolean = false) = onEach {
val description = when (it) {
is Resource.Intermediate -> "intermediate data received" + if (showData) " (data: `${it.data}`)" else ""
@ -119,29 +74,8 @@ fun <T> Flow<Resource<T>>.logResourceStatus(name: String, showData: Boolean = fa
Timber.i("$name: $description")
}
inline fun <T, U> Flow<Resource<T>>.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 <T, U> Flow<Resource<T>>.flatMapResourceData(
inheritIntermediate: Boolean = true, block: suspend (T) -> Flow<Resource<U>>
) = 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 <T, U> Flow<Resource<T>>.mapResourceData(block: (T) -> U) = map {
it.mapData(block)
}
fun <T> Flow<Resource<T>>.onResourceData(block: suspend (T) -> Unit) = onEach {
@ -171,13 +105,13 @@ fun <T> Flow<Resource<T>>.onResourceSuccess(block: suspend (T) -> Unit) = onEach
}
}
fun <T> Flow<Resource<T>>.onResourceError(block: suspend (Throwable) -> Unit) = onEach {
fun <T> Flow<Resource<T>>.onResourceError(block: (Throwable) -> Unit) = onEach {
if (it is Resource.Error) {
block(it.error)
}
}
fun <T> Flow<Resource<T>>.onResourceNotLoading(block: suspend () -> Unit) = onEach {
fun <T> Flow<Resource<T>>.onResourceNotLoading(block: () -> Unit) = onEach {
if (it !is Resource.Loading) {
block()
}
@ -187,99 +121,70 @@ suspend fun <T> Flow<Resource<T>>.toFirstResult() = filter { it !is Resource.Loa
suspend fun <T> Flow<Resource<T>>.waitForResult() = takeWhile { it is Resource.Loading }.collect()
// Can cause excessive amounts of `Resource.Intermediate` to be emitted. Unless that is desired,
// use `debounceIntermediates` to alleviate this behavior.
inline fun <reified T> combineResourceFlows(flows: Iterable<Flow<Resource<T>>>): Flow<Resource<List<T>>> =
combine(flows) { items ->
var isIntermediate = false
val data = mutableListOf<T>()
for (item in items) {
when (item) {
is Resource.Success -> data.add(item.data)
is Resource.Intermediate -> {
isIntermediate = true
data.add(item.data)
}
is Resource.Loading -> return@combine Resource.Loading()
is Resource.Error -> continue
}
}
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 <T> Flow<Resource<T>>.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
}
})
}
inline fun <OutputType, ApiType> networkBoundResource(
inline fun <ResultType, RequestType> networkBoundResource(
mutex: Mutex = Mutex(),
crossinline isResultEmpty: (OutputType) -> Boolean,
crossinline query: () -> Flow<OutputType>,
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 <DatabaseType, ApiType, OutputType> networkBoundResource(
mutex: Mutex = Mutex(),
crossinline isResultEmpty: (OutputType) -> Boolean,
crossinline query: () -> Flow<DatabaseType>,
crossinline fetch: suspend () -> ApiType,
crossinline saveFetchResult: suspend (old: DatabaseType, new: ApiType) -> Unit,
crossinline shouldFetch: (DatabaseType) -> Boolean = { true },
crossinline mapResult: (DatabaseType) -> OutputType,
showSavedOnLoading: Boolean = true,
crossinline isResultEmpty: (ResultType) -> Boolean,
crossinline query: () -> Flow<ResultType>,
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())
val data = query().first()
if (shouldFetch(data)) {
emit(Resource.Intermediate(data))
emitAll(if (shouldFetch(data)) {
val filteredResult = filterResult(data)
if (showSavedOnLoading && !isResultEmpty(filteredResult)) {
emit(Resource.Intermediate(filteredResult))
}
try {
val newData = fetch()
val newData = fetch(data)
mutex.withLock { saveFetchResult(query().first(), newData) }
query().map { Resource.Success(filterResult(it)) }
} catch (throwable: Throwable) {
emit(Resource.Error(throwable))
return@flow
onFetchFailed(throwable)
flowOf(Resource.Error(throwable))
}
}
emitAll(query().map { Resource.Success(it) })
} else {
query().map { Resource.Success(filterResult(it)) }
})
}
@JvmName("networkBoundResourceWithMap")
inline fun <ResultType, RequestType, T> networkBoundResource(
mutex: Mutex = Mutex(),
showSavedOnLoading: Boolean = true,
crossinline isResultEmpty: (T) -> Boolean,
crossinline query: () -> Flow<ResultType>,
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,
) = flow {
emit(Resource.Loading())
val data = query().first()
emitAll(if (shouldFetch(data)) {
val mappedResult = mapResult(data)
if (showSavedOnLoading && !isResultEmpty(mappedResult)) {
emit(Resource.Intermediate(mappedResult))
}
try {
val newData = fetch(data)
mutex.withLock { saveFetchResult(query().first(), newData) }
query().map { Resource.Success(mapResult(it)) }
} catch (throwable: Throwable) {
onFetchFailed(throwable)
flowOf(Resource.Error(throwable))
}
} else {
query().map { Resource.Success(mapResult(it)) }
})
}
.mapResourceData { mapResult(it) }
.filterNot { it is Resource.Intermediate && isResultEmpty(it.data) }

View File

@ -1,11 +1,15 @@
package io.github.wulkanowy.data
import com.chuckerteam.chucker.api.ChuckerInterceptor
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentIsEduOne
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.RemoteConfigHelper
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -14,9 +18,12 @@ import javax.inject.Singleton
class WulkanowySdkFactory @Inject constructor(
private val chuckerInterceptor: ChuckerInterceptor,
private val remoteConfig: RemoteConfigHelper,
private val webkitCookieManagerProxy: WebkitCookieManagerProxy
private val webkitCookieManagerProxy: WebkitCookieManagerProxy,
private val studentDb: StudentDao,
) {
private val eduOneMutex = Mutex()
private val sdk = Sdk().apply {
androidVersion = android.os.Build.VERSION.RELEASE
buildTag = android.os.Build.MODEL
@ -30,7 +37,12 @@ class WulkanowySdkFactory @Inject constructor(
fun create() = sdk
fun create(student: Student, semester: Semester? = null): Sdk {
suspend fun create(student: Student, semester: Semester? = null): Sdk {
val overrideIsEduOne = checkEduOneAndMigrateIfNecessary(student)
return buildSdk(student, semester, overrideIsEduOne)
}
private fun buildSdk(student: Student, semester: Semester?, isStudentEduOne: Boolean): Sdk {
return create().apply {
email = student.email
password = student.password
@ -39,6 +51,7 @@ class WulkanowySdkFactory @Inject constructor(
studentId = student.studentId
classId = student.classId
emptyCookieJarInterceptor = true
isEduOne = isStudentEduOne
if (Sdk.Mode.valueOf(student.loginMode) == Sdk.Mode.HEBE) {
mobileBaseUrl = student.mobileBaseUrl
@ -61,4 +74,40 @@ class WulkanowySdkFactory @Inject constructor(
}
}
}
private suspend fun checkEduOneAndMigrateIfNecessary(student: Student): Boolean {
if (student.isEduOne != null) return student.isEduOne
eduOneMutex.withLock {
val studentFromDatabase = studentDb.loadById(student.id)
if (studentFromDatabase?.isEduOne != null) {
Timber.d("Migration eduOne: already done")
return studentFromDatabase.isEduOne
}
Timber.d("Migration eduOne: flag missing. Running migration...")
val initializedSdk = buildSdk(
student = student,
semester = null,
isStudentEduOne = false, // doesn't matter
)
val newCurrentStudent = runCatching { initializedSdk.getCurrentStudent() }
.onFailure { Timber.e(it, "Migration eduOne: can't get current student") }
.getOrNull()
if (newCurrentStudent == null) {
Timber.d("Migration eduOne: failed, so skipping")
return false
}
Timber.d("Migration eduOne: success. New isEduOne flag: ${newCurrentStudent.isEduOne}")
val studentIsEduOne = StudentIsEduOne(
id = student.id,
isEduOne = newCurrentStudent.isEduOne
)
studentDb.update(studentIsEduOne)
return newCurrentStudent.isEduOne
}
}
}

View File

@ -120,6 +120,7 @@ import io.github.wulkanowy.data.db.migrations.Migration55
import io.github.wulkanowy.data.db.migrations.Migration57
import io.github.wulkanowy.data.db.migrations.Migration58
import io.github.wulkanowy.data.db.migrations.Migration6
import io.github.wulkanowy.data.db.migrations.Migration63
import io.github.wulkanowy.data.db.migrations.Migration7
import io.github.wulkanowy.data.db.migrations.Migration8
import io.github.wulkanowy.data.db.migrations.Migration9
@ -174,6 +175,8 @@ import javax.inject.Singleton
AutoMigration(from = 58, to = 59),
AutoMigration(from = 59, to = 60),
AutoMigration(from = 60, to = 61),
AutoMigration(from = 61, to = 62),
AutoMigration(from = 62, to = 63, spec = Migration63::class),
],
version = AppDatabase.VERSION_SCHEMA,
exportSchema = true
@ -182,7 +185,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 61
const val VERSION_SCHEMA = 63
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(),
@ -309,6 +312,6 @@ abstract class AppDatabase : RoomDatabase() {
abstract val adminMessagesDao: AdminMessageDao
abstract val mutedMessageSendersDao: MutedMessageSendersDao
abstract val gradeDescriptiveDao: GradeDescriptiveDao
}

View File

@ -9,6 +9,8 @@ import androidx.room.Transaction
import androidx.room.Update
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentIsAuthorized
import io.github.wulkanowy.data.db.entities.StudentIsEduOne
import io.github.wulkanowy.data.db.entities.StudentName
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import javax.inject.Singleton
@ -23,6 +25,12 @@ abstract class StudentDao {
@Delete
abstract suspend fun delete(student: Student)
@Update(entity = Student::class)
abstract suspend fun update(studentIsAuthorized: StudentIsAuthorized)
@Update(entity = Student::class)
abstract suspend fun update(studentIsEduOne: StudentIsEduOne)
@Update(entity = Student::class)
abstract suspend fun update(studentNickAndAvatar: StudentNickAndAvatar)

View File

@ -78,6 +78,13 @@ data class Student(
@ColumnInfo(name = "registration_date")
val registrationDate: Instant,
@ColumnInfo(name = "is_authorized", defaultValue = "0")
val isAuthorized: Boolean,
@ColumnInfo(name = "is_edu_one", defaultValue = "NULL")
val isEduOne: Boolean?,
) : Serializable {
@PrimaryKey(autoGenerate = true)
@ -88,3 +95,22 @@ data class Student(
@ColumnInfo(name = "avatar_color")
var avatarColor = 0L
}
@Entity
data class StudentIsAuthorized(
@PrimaryKey
var id: Long,
@ColumnInfo(name = "is_authorized", defaultValue = "NULL")
val isAuthorized: Boolean?,
) : Serializable
@Entity
data class StudentIsEduOne(
@PrimaryKey
var id: Long,
@ColumnInfo(name = "is_edu_one", defaultValue = "NULL")
val isEduOne: Boolean?,
) : Serializable

View File

@ -0,0 +1,11 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.AutoMigrationSpec
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration63 : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
db.execSQL("UPDATE Students SET is_edu_one = NULL WHERE is_edu_one = 0")
}
}

View File

@ -1,13 +0,0 @@
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
}
}

View File

@ -34,17 +34,19 @@ fun SdkRegisterUser.mapToPojo(password: String?) = RegisterUser(
error = it.error,
students = it.subjects
.filterIsInstance<SdkRegisterStudent>()
.map { registerSubject ->
.map { registerStudent ->
RegisterStudent(
studentId = registerSubject.studentId,
studentName = registerSubject.studentName,
studentSecondName = registerSubject.studentSecondName,
studentSurname = registerSubject.studentSurname,
className = registerSubject.className,
classId = registerSubject.classId,
isParent = registerSubject.isParent,
semesters = registerSubject.semesters
.mapToEntities(registerSubject.studentId),
studentId = registerStudent.studentId,
studentName = registerStudent.studentName,
studentSecondName = registerStudent.studentSecondName,
studentSurname = registerStudent.studentSurname,
className = registerStudent.className,
classId = registerStudent.classId,
isParent = registerStudent.isParent,
isAuthorized = registerStudent.isAuthorized,
isEduOne = registerStudent.isEduOne,
semesters = registerStudent.semesters
.mapToEntities(registerStudent.studentId),
)
},
)
@ -84,6 +86,8 @@ fun RegisterStudent.mapToStudentWithSemesters(
password = user.password.orEmpty(),
isCurrent = false,
registrationDate = Instant.now(),
isAuthorized = this.isAuthorized,
isEduOne = this.isEduOne,
).apply {
avatarColor = colors.random()
},

View File

@ -1,14 +0,0 @@
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
}

View File

@ -45,4 +45,6 @@ data class RegisterStudent(
val classId: Int,
val isParent: Boolean,
val semesters: List<Semester>,
val isAuthorized: Boolean,
val isEduOne: Boolean
) : java.io.Serializable

View File

@ -6,7 +6,6 @@ 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
@ -29,6 +28,6 @@ class AdminMessageRepository @Inject constructor(
saveFetchResult = { oldItems, newItems ->
adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
},
showSavedOnLoading = false,
)
.filterNot { it is Resource.Intermediate }
}

View File

@ -122,7 +122,7 @@ class MessageRepository @Inject constructor(
fetch = {
wulkanowySdkFactory.create(student)
.getMessageDetails(
messageKey = message.messageGlobalKey,
messageKey = it!!.message.messageGlobalKey,
markAsRead = message.unread && markAsRead,
)
},

View File

@ -10,7 +10,6 @@ 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
@ -42,27 +41,6 @@ class PreferencesRepository @Inject constructor(
R.bool.pref_default_attendance_present
)
val targetAttendanceFlow: Flow<Int>
get() = flowSharedPref.getInt(
context.getString(R.string.pref_key_attendance_target),
context.resources.getInteger(R.integer.pref_default_attendance_target)
).asFlow()
val attendanceCalculatorSortingModeFlow: Flow<AttendanceCalculatorSortingMode>
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<Boolean>
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<GradeAverageMode>
get() = getObjectFlow(
R.string.pref_key_grade_average_mode,

View File

@ -7,16 +7,19 @@ import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentIsAuthorized
import io.github.wulkanowy.data.db.entities.StudentName
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.mappers.mapToPojo
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.DispatchersProvider
import io.github.wulkanowy.utils.security.Scrambler
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -99,6 +102,46 @@ class StudentRepository @Inject constructor(
return student
}
suspend fun updateCurrentStudentAuthStatus() {
Timber.i("Check isAuthorized: started")
val student = getCurrentStudent()
if (student.isAuthorized) {
Timber.i("Check isAuthorized: already authorized")
return
}
val initializedSdk = wulkanowySdkFactory.create(student)
val newCurrentStudent = runCatching { initializedSdk.getCurrentStudent() }
.onFailure { Timber.e(it, "Check isAuthorized: error occurred") }
.getOrNull()
if (newCurrentStudent == null) {
Timber.d("Check isAuthorized: current user is null")
return
}
val currentStudentSemesters = semesterDb.loadAll(student.studentId, student.classId)
if (currentStudentSemesters.isEmpty()) {
Timber.d("Check isAuthorized: apply empty semesters workaround")
semesterDb.insertSemesters(
items = newCurrentStudent.semesters.mapToEntities(student.studentId),
)
}
if (!newCurrentStudent.isAuthorized) {
Timber.i("Check isAuthorized: authorization required")
throw NoAuthorizationException()
}
val studentIsAuthorized = StudentIsAuthorized(
id = student.id,
isAuthorized = true
)
Timber.i("Check isAuthorized: already authorized, update local status")
studentDb.update(studentIsAuthorized)
}
suspend fun getCurrentStudent(decryptPass: Boolean = true): Student {
val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException()
@ -151,15 +194,21 @@ class StudentRepository @Inject constructor(
wulkanowySdkFactory.create(student, semester)
.authorizePermission(pesel)
suspend fun refreshStudentName(student: Student, semester: Semester) {
val newCurrentApiStudent = wulkanowySdkFactory.create(student, semester)
.getCurrentStudent() ?: return
suspend fun refreshStudentAfterAuthorize(student: Student, semester: Semester) {
val newCurrentApiStudent = wulkanowySdkFactory
.create(student, semester)
.getCurrentStudent()
?: return Timber.d("Can't find student with id ${student.studentId}")
val studentName = StudentName(
studentName = "${newCurrentApiStudent.studentName} ${newCurrentApiStudent.studentSurname}"
).apply { id = student.id }
studentDb.update(studentName)
semesterDb.removeOldAndSaveNew(
oldItems = semesterDb.loadAll(student.studentId, semester.classId),
newItems = newCurrentApiStudent.semesters.mapToEntities(newCurrentApiStudent.studentId)
)
}
suspend fun deleteStudentsAssociatedWithAccount(student: Student) {
@ -173,3 +222,5 @@ class StudentRepository @Inject constructor(
}
}
}
class NoAuthorizationException : Exception()

View File

@ -1,106 +0,0 @@
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<Resource<List<AttendanceData>>> =
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<AttendanceData>::sortedBy)
}
private fun List<AttendanceSummary>.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<AttendanceData>.sortedBy(mode: AttendanceCalculatorSortingMode) = when (mode) {
ALPHABETIC -> sortedBy(AttendanceData::subjectName)
ATTENDANCE -> sortedByDescending(AttendanceData::presencePercentage)
LESSON_BALANCE -> sortedBy(AttendanceData::lessonBalance)
}

View File

@ -17,6 +17,7 @@ import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
import io.github.wulkanowy.sdk.scrapper.exception.FeatureUnavailableException
import io.github.wulkanowy.services.sync.channels.DebugChannel
import io.github.wulkanowy.services.sync.works.Work
import io.github.wulkanowy.utils.DispatchersProvider
@ -48,6 +49,7 @@ class SyncWorker @AssistedInject constructor(
val semester = semesterRepository.getCurrentSemester(student, true)
student to semester
} catch (e: Throwable) {
Timber.e(e)
return@withContext getResultFromErrors(listOf(e))
}
@ -59,7 +61,7 @@ class SyncWorker @AssistedInject constructor(
null
} catch (e: Throwable) {
Timber.w("${work::class.java.simpleName} result: An exception ${e.message} occurred")
if (e is FeatureDisabledException || e is FeatureNotAvailableException) {
if (e is FeatureDisabledException || e is FeatureNotAvailableException || e is FeatureUnavailableException) {
null
} else {
Timber.e(e)

View File

@ -3,7 +3,7 @@ package io.github.wulkanowy.ui.base
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.sdk.scrapper.exception.AuthorizationRequiredException
import io.github.wulkanowy.data.repositories.NoAuthorizationException
import io.github.wulkanowy.sdk.scrapper.exception.CloudflareVerificationException
import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException
import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException
@ -40,7 +40,7 @@ open class ErrorHandler @Inject constructor(@ApplicationContext protected val co
is ScramblerException -> onDecryptionFailed()
is BadCredentialsException -> onExpiredCredentials()
is NoCurrentStudentException -> onNoCurrentStudent()
is AuthorizationRequiredException -> onAuthorizationRequired()
is NoAuthorizationException -> onAuthorizationRequired()
is CloudflareVerificationException -> onCaptchaVerificationRequired(error.originalUrl)
}
}

View File

@ -14,7 +14,6 @@ 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
@ -135,7 +134,6 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(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
}
@ -255,10 +253,6 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(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)
}

View File

@ -1,36 +1,16 @@
package io.github.wulkanowy.ui.modules.attendance
import android.annotation.SuppressLint
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.*
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.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 io.github.wulkanowy.utils.*
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
@ -215,11 +195,6 @@ 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")

View File

@ -56,8 +56,6 @@ interface AttendanceView : BaseView {
fun openSummaryView()
fun openCalculatorView()
fun startSendMessageIntent(date: LocalDate, numbers: String, reason: String)
fun startActionMode()

View File

@ -1,67 +0,0 @@
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<AttendanceCalculatorAdapter.ViewHolder>() {
var items = emptyList<AttendanceData>()
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)
}

View File

@ -1,105 +0,0 @@
package io.github.wulkanowy.ui.modules.attendance.calculator
import android.os.Bundle
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.MainView
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject
@AndroidEntryPoint
class AttendanceCalculatorFragment :
BaseFragment<FragmentAttendanceCalculatorBinding>(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()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentAttendanceCalculatorBinding.bind(view)
messageContainer = binding.attendanceCalculatorRecycler
presenter.onAttachView(this)
}
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<AttendanceData>) {
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()
}
}

View File

@ -1,84 +0,0 @@
package io.github.wulkanowy.ui.modules.attendance.calculator
import io.github.wulkanowy.data.*
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<AttendanceCalculatorView>(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)
}
}
}

View File

@ -1,29 +0,0 @@
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<AttendanceData>)
fun clearView()
}

View File

@ -5,6 +5,7 @@ import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
class AuthPresenter @Inject constructor(
@ -57,8 +58,9 @@ class AuthPresenter @Inject constructor(
val semester = semesterRepository.getCurrentSemester(student)
val isSuccess = studentRepository.authorizePermission(student, semester, pesel)
Timber.d("Auth succeed: $isSuccess")
if (isSuccess) {
studentRepository.refreshStudentName(student, semester)
studentRepository.refreshStudentAfterAuthorize(student, semester)
}
isSuccess
}
@ -68,6 +70,7 @@ class AuthPresenter @Inject constructor(
view?.showContent(true)
}
.onSuccess {
Timber.d("Auth fully succeed: $it")
if (it) {
view?.showSuccess(true)
view?.showContent(false)

View File

@ -7,7 +7,6 @@ import android.view.MenuItem
import android.view.View
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
@ -31,14 +30,6 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
@Inject
lateinit var presenter: GradePresenter
private val pagerAdapter by lazy {
BaseFragmentPagerAdapter(
fragmentManager = childFragmentManager,
pagesCount = 3,
lifecycle = lifecycle,
)
}
private var semesterSwitchMenu: MenuItem? = null
companion object {
@ -52,6 +43,8 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
override val currentPageIndex get() = binding.gradeViewPager.currentItem
private var pagerAdapter: BaseFragmentPagerAdapter? = null
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -71,13 +64,26 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
}
override fun initView() {
with(binding) {
gradeErrorRetry.setOnClickListener { presenter.onRetry() }
gradeErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
}
override fun initTabs(pageCount: Int) {
pagerAdapter = BaseFragmentPagerAdapter(
lifecycle = lifecycle,
pagesCount = pageCount,
fragmentManager = childFragmentManager
)
with(binding.gradeViewPager) {
adapter = pagerAdapter
offscreenPageLimit = 3
setOnSelectPageListener(presenter::onPageSelected)
}
with(pagerAdapter) {
with(pagerAdapter!!) {
containerId = binding.gradeViewPager.id
titleFactory = {
when (it) {
@ -99,11 +105,6 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
}
binding.gradeTabLayout.elevation = requireContext().dpToPx(4f)
with(binding) {
gradeErrorRetry.setOnClickListener { presenter.onRetry() }
gradeErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -169,19 +170,20 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
}
override fun notifyChildLoadData(index: Int, semesterId: Int, forceRefresh: Boolean) {
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)
(pagerAdapter?.getFragmentInstance(index) as? GradeView.GradeChildView)
?.onParentLoadData(semesterId, forceRefresh)
}
override fun notifyChildParentReselected(index: Int) {
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentReselected()
(pagerAdapter?.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentReselected()
}
override fun notifyChildSemesterChange(index: Int) {
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentChangeSemester()
(pagerAdapter?.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentChangeSemester()
}
override fun onDestroyView() {
pagerAdapter = null
presenter.onDetachView()
super.onDestroyView()
}

View File

@ -22,11 +22,8 @@ class GradePresenter @Inject constructor(
) : BasePresenter<GradeView>(errorHandler, studentRepository) {
private var selectedIndex = 0
private var schoolYear = 0
private var semesters = emptyList<Semester>()
private var availableSemesters = emptyList<Semester>()
private val loadedSemesterId = mutableMapOf<Int, Int>()
private lateinit var lastError: Throwable
@ -40,7 +37,7 @@ class GradePresenter @Inject constructor(
}
fun onCreateMenu() {
if (semesters.isEmpty()) view?.showSemesterSwitch(false)
if (availableSemesters.isEmpty()) view?.showSemesterSwitch(false)
}
fun onViewReselected() {
@ -49,8 +46,8 @@ class GradePresenter @Inject constructor(
}
fun onSemesterSwitch(): Boolean {
if (semesters.isNotEmpty()) {
view?.showSemesterDialog(selectedIndex - 1, semesters.take(2))
if (availableSemesters.isNotEmpty()) {
view?.showSemesterDialog(selectedIndex - 1, availableSemesters.take(2))
}
return true
}
@ -83,7 +80,7 @@ class GradePresenter @Inject constructor(
}
fun onPageSelected(index: Int) {
if (semesters.isNotEmpty()) loadChild(index)
if (availableSemesters.isNotEmpty()) loadChild(index)
}
fun onRetry() {
@ -101,16 +98,24 @@ class GradePresenter @Inject constructor(
private fun loadData() {
resourceFlow {
val student = studentRepository.getCurrentStudent()
semesterRepository.getSemesters(student, refreshOnNoCurrent = true)
val semesters = semesterRepository.getSemesters(student, refreshOnNoCurrent = true)
student to semesters
}
.logResourceStatus("load grade data")
.onResourceData {
val current = it.getCurrentOrLast()
selectedIndex = if (selectedIndex == 0) current.semesterName else selectedIndex
schoolYear = current.schoolYear
semesters = it.filter { semester -> semester.diaryId == current.diaryId }
view?.setCurrentSemesterName(current.semesterName, schoolYear)
.onResourceData { (student, semesters) ->
val currentSemester = semesters.getCurrentOrLast()
selectedIndex =
if (selectedIndex == 0) currentSemester.semesterName else selectedIndex
schoolYear = currentSemester.schoolYear
availableSemesters = semesters.filter { semester ->
semester.diaryId == currentSemester.diaryId
}
view?.run {
initTabs(if (student.isEduOne == true) 2 else 3)
setCurrentSemesterName(currentSemester.semesterName, schoolYear)
Timber.i("Loading grade data: Attempt load index $currentPageIndex")
loadChild(currentPageIndex)
showErrorView(false)
@ -131,10 +136,10 @@ class GradePresenter @Inject constructor(
}
private fun loadChild(index: Int, forceRefresh: Boolean = false) {
Timber.d("Load grade tab child. Selected semester: $selectedIndex, semesters: ${semesters.joinToString { it.semesterName.toString() }}")
Timber.d("Load grade tab child. Selected semester: $selectedIndex, semesters: ${availableSemesters.joinToString { it.semesterName.toString() }}")
val newSelectedSemesterId = try {
semesters.first { it.semesterName == selectedIndex }.semesterId
availableSemesters.first { it.semesterName == selectedIndex }.semesterId
} catch (e: NoSuchElementException) {
Timber.e(e, "Selected semester no exists")
return

View File

@ -9,6 +9,8 @@ interface GradeView : BaseView {
fun initView()
fun initTabs(pageCount: Int)
fun showContent(show: Boolean)
fun showProgress(show: Boolean)

View File

@ -98,7 +98,9 @@ class HomeworkAddDialog : BaseDialogFragment<DialogHomeworkAddBinding>(), Homewo
rangeEnd = LocalDate.now().lastSchoolDayInSchoolYear,
onDateSelected = {
date = it
binding.homeworkDialogDate.editText?.setText(date!!.toFormattedString())
if (isAdded) {
binding.homeworkDialogDate.editText?.setText(date!!.toFormattedString())
}
}
)
}

View File

@ -73,6 +73,7 @@ class MainPresenter @Inject constructor(
syncManager.startPeriodicSyncWorker()
checkAppSupport()
updateCurrentStudentAuthStatus()
analytics.logEvent("app_open", "destination" to initDestination.toString())
Timber.i("Main view was initialized with $initDestination")
@ -191,4 +192,11 @@ class MainPresenter @Inject constructor(
view?.showStudentAvatar(currentStudent)
}
private fun updateCurrentStudentAuthStatus() {
presenterScope.launch {
runCatching { studentRepository.updateCurrentStudentAuthStatus() }
.onFailure { errorHandler.dispatch(it) }
}
}
}

View File

@ -4,7 +4,6 @@ import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SeekBarPreference
import com.yariksoffice.lingver.Lingver
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
@ -37,15 +36,6 @@ class AppearanceFragment : PreferenceFragmentCompat(),
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.scheme_preferences_appearance, rootKey)
val attendanceTargetPref =
findPreference<SeekBarPreference>(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?) {

View File

@ -27,7 +27,7 @@ class TimetableAdapter @Inject constructor() :
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (TimetableItemType.values()[viewType]) {
return when (TimetableItemType.entries[viewType]) {
TimetableItemType.SMALL -> SmallViewHolder(
ItemTimetableSmallBinding.inflate(inflater, parent, false)
)
@ -79,6 +79,7 @@ class TimetableAdapter @Inject constructor() :
with(binding) {
timetableSmallItemNumber.text = lesson.number.toString()
timetableSmallItemNumber.isVisible = item.isLessonNumberVisible
timetableSmallItemSubject.text = lesson.subject
timetableSmallItemTimeStart.text = lesson.start.toFormattedString("HH:mm")
timetableSmallItemRoom.text = lesson.room
@ -97,6 +98,7 @@ class TimetableAdapter @Inject constructor() :
with(binding) {
timetableItemNumber.text = lesson.number.toString()
timetableItemNumber.isVisible = item.isLessonNumberVisible
timetableItemSubject.text = lesson.subject
timetableItemGroup.text = lesson.group
timetableItemRoom.text = lesson.room

View File

@ -7,12 +7,14 @@ sealed class TimetableItem(val type: TimetableItemType) {
data class Small(
val lesson: Timetable,
val isLessonNumberVisible: Boolean,
val onClick: (Timetable) -> Unit,
) : TimetableItem(TimetableItemType.SMALL)
data class Normal(
val lesson: Timetable,
val showGroupsInPlan: Boolean,
val isLessonNumberVisible: Boolean,
val timeLeft: TimeLeft?,
val onClick: (Timetable) -> Unit,
) : TimetableItem(TimetableItemType.NORMAL)

View File

@ -57,6 +57,7 @@ class TimetablePresenter @Inject constructor(
private var initialDate: LocalDate? = null
private var isWeekendHasLessons: Boolean = false
private var isEduOne: Boolean = false
var currentDate: LocalDate? = null
private set
@ -149,6 +150,7 @@ class TimetablePresenter @Inject constructor(
val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student)
isEduOne = student.isEduOne == true
checkInitialAndCurrentDate(semester)
timetableRepository.getTimetable(
student = student,
@ -234,9 +236,8 @@ class TimetablePresenter @Inject constructor(
if (prefRepository.showWholeClassPlan == TimetableMode.ONLY_CURRENT_GROUP) {
it.isStudentPlan
} else true
}.sortedWith(
compareBy({ item -> item.number }, { item -> !item.isStudentPlan })
)
}
.sortedWith(compareBy({ item -> item.start }, { item -> !item.isStudentPlan }))
var prevNum = when (prefRepository.showTimetableGaps) {
BETWEEN_AND_BEFORE_LESSONS -> 0
@ -257,13 +258,15 @@ class TimetablePresenter @Inject constructor(
lesson = it,
showGroupsInPlan = prefRepository.showGroupsInPlan,
timeLeft = filteredItems.getTimeLeftForLesson(it, i),
onClick = ::onTimetableItemSelected
onClick = ::onTimetableItemSelected,
isLessonNumberVisible = !isEduOne
)
add(normalLesson)
} else {
val smallLesson = TimetableItem.Small(
lesson = it,
onClick = ::onTimetableItemSelected
onClick = ::onTimetableItemSelected,
isLessonNumberVisible = !isEduOne
)
add(smallLesson)
}

View File

@ -46,11 +46,8 @@ class TimetableWidgetFactory(
) : RemoteViewsService.RemoteViewsFactory {
private var items = emptyList<TimetableWidgetItem>()
private var timetableCanceledColor: Int? = null
private var textColor: Int? = null
private var timetableChangeColor: Int? = null
override fun getLoadingView() = null
@ -81,7 +78,7 @@ class TimetableWidgetFactory(
val lessons = getLessons(student, semester, date)
val lastSync = timetableRepository.getLastRefreshTimestamp(semester, date, date)
createItems(lessons, lastSync)
createItems(lessons, lastSync, !(student.isEduOne ?: false))
}
.onFailure {
items = listOf(TimetableWidgetItem.Error(it))
@ -106,12 +103,13 @@ class TimetableWidgetFactory(
): List<Timetable> {
val timetable = timetableRepository.getTimetable(student, semester, date, date, false)
val lessons = timetable.toFirstResult().dataOrThrow.lessons
return lessons.sortedBy { it.number }
return lessons.sortedBy { it.start }
}
private fun createItems(
lessons: List<Timetable>,
lastSync: Instant?,
isEduOne: Boolean
): List<TimetableWidgetItem> {
var prevNum = when (prefRepository.showTimetableGaps) {
BETWEEN_AND_BEFORE_LESSONS -> 0
@ -127,7 +125,7 @@ class TimetableWidgetFactory(
)
add(emptyItem)
}
add(TimetableWidgetItem.Normal(it))
add(TimetableWidgetItem.Normal(it, isEduOne))
prevNum = it.number
}
add(TimetableWidgetItem.Synchronized(lastSync ?: Instant.MIN))
@ -155,9 +153,11 @@ class TimetableWidgetFactory(
val lessonStartTime = lesson.start.toFormattedString(TIME_FORMAT_STYLE)
val lessonEndTime = lesson.end.toFormattedString(TIME_FORMAT_STYLE)
val lessonNumberVisibility = if (item.isLessonNumberVisible) VISIBLE else GONE
val remoteViews = RemoteViews(context.packageName, R.layout.item_widget_timetable).apply {
setTextViewText(R.id.timetableWidgetItemNumber, lesson.number.toString())
setViewVisibility(R.id.timetableWidgetItemNumber, lessonNumberVisibility)
setTextViewText(R.id.timetableWidgetItemTimeStart, lessonStartTime)
setTextViewText(R.id.timetableWidgetItemTimeFinish, lessonEndTime)
setTextViewText(R.id.timetableWidgetItemSubject, lesson.subject)

View File

@ -7,6 +7,7 @@ sealed class TimetableWidgetItem(val type: TimetableWidgetItemType) {
data class Normal(
val lesson: Timetable,
val isLessonNumberVisible: Boolean,
) : TimetableWidgetItem(TimetableWidgetItemType.NORMAL)
data class Empty(

View File

@ -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)
*/
inline val AttendanceSummary.allPresences: Int
get() = presence + absenceForSchoolReasons + lateness + latenessExcused
private inline val AttendanceSummary.allPresences: Double
get() = presence.toDouble() + absenceForSchoolReasons + lateness + latenessExcused
inline val AttendanceSummary.allAbsences: Int
get() = absence + absenceExcused
private inline val AttendanceSummary.allAbsences: Double
get() = absence.toDouble() + absenceExcused
inline val Attendance.isExcusableOrNotExcused: Boolean
get() = (excusable || ((absence || lateness) && !excused)) && excuseStatus == null
fun AttendanceSummary.calculatePercentage() = calculatePercentage(allPresences.toDouble(), allAbsences.toDouble())
fun AttendanceSummary.calculatePercentage() = calculatePercentage(allPresences, allAbsences)
fun List<AttendanceSummary>.calculatePercentage(): Double {
return calculatePercentage(sumOf { it.allPresences.toDouble() }, sumOf { it.allAbsences.toDouble() })
return calculatePercentage(sumOf { it.allPresences }, sumOf { it.allAbsences })
}
private fun calculatePercentage(presence: Double, absence: Double): Double {

View File

@ -1,11 +1,8 @@
Wersja 2.5.1
Wersja 2.5.3
dodaliśmy wyświetlanie ogłoszeń
dodaliśmy opcję przywracania wiadomości z kosza
dodaliśmy opcję wyciszania nadawców wiadomości
— naprawiliśmy opcjonalne liczenie średniej arytmetycznej, kiedy brak ocen z wagą w drugim semestrze
— usprawniliśmy ładowanie frekwencji i planu lekcji
— naprawiliśmy usprawiedliwianie nieobecności i autoryzację u użytkowników eduOne
— zmieniliśmy komunikat o zmienionym haśle
naprawiliśmy wyświetlanie błędu "Brak uprawnień" po starcie aplikacji u użytkowników eduOne
naprawiliśmy obsługę autoryzacji u użytkowników eduOne
ukryliśmy numery lekcji i oceny klasy u użytkowników eduOne, bo VULCAN te funkcje usunął
— naprawiliśmy inne rzeczy u użytkowników eduOne (jak brak opisu oceny czy ładowanie danych na kilku ekranach)
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,7H9C7.9,7 7,7.9 7,9v10c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V9C21,7.9 20.1,7 19,7zM19,9v2H9V9H19zM13,15v-2h2v2H13zM15,17v2h-2v-2H15zM11,15H9v-2h2V15zM17,13h2v2h-2V13zM9,17h2v2H9V17zM17,19v-2h2v2H17zM6,17H5c-1.1,0 -2,-0.9 -2,-2V5c0,-1.1 0.9,-2 2,-2h10c1.1,0 2,0.9 2,2v1h-2V5H5v10h1V17z"/>
</vector>

View File

@ -1,103 +0,0 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.modules.attendance.calculator.AttendanceCalculatorFragment">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/attendanceCalculatorSwipe"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/attendanceCalculatorRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/item_attendance_calculator_header" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/attendanceCalculatorProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
tools:visibility="gone" />
<LinearLayout
android:id="@+id/attendanceCalculatorEmpty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="invisible"
tools:ignore="UseCompoundDrawables">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
app:srcCompat="@drawable/ic_main_attendance"
app:tint="?colorOnBackground"
tools:ignore="contentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:text="@string/attendance_no_items"
android:textSize="20sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/attendanceCalculatorError"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="invisible"
tools:ignore="UseCompoundDrawables"
tools:visibility="invisible">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
app:srcCompat="@drawable/ic_error"
app:tint="?colorOnBackground"
tools:ignore="contentDescription" />
<TextView
android:id="@+id/attendanceCalculatorErrorMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:padding="8dp"
android:text="@string/error_unknown"
android:textSize="20sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/attendanceCalculatorErrorDetails"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:text="@string/all_details" />
<com.google.android.material.button.MaterialButton
android:id="@+id/attendanceCalculatorErrorRetry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/all_retry" />
</LinearLayout>
</LinearLayout>
</FrameLayout>

View File

@ -29,6 +29,7 @@
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/openSendMessageButton"
style="@style/Widget.Material3.ExtendedFloatingActionButton.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"

View File

@ -1,111 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingTop="6dp"
android:paddingEnd="12dp"
android:paddingBottom="6dp">
<TextView
android:id="@+id/attendanceCalculatorPercentage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="right|center_vertical"
android:minWidth="32dp"
android:minHeight="36dp"
android:textSize="22sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="50" />
<TextView
android:id="@+id/attendanceCalculatorPercentagePercentSign"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="%"
android:textColor="?android:textColorSecondary"
app:layout_constraintBaseline_toBaselineOf="@+id/attendanceCalculatorPercentage"
app:layout_constraintStart_toEndOf="@+id/attendanceCalculatorPercentage" />
<TextView
android:id="@+id/attendanceCalculatorSummaryValues"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="left"
android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="13sp"
app:layout_constraintEnd_toStartOf="@+id/attendanceCalculatorSummaryDot"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="@+id/attendanceCalculatorTitle"
app:layout_constraintTop_toBottomOf="@+id/attendanceCalculatorTitle"
tools:text="11/123 obecności" />
<TextView
android:id="@+id/attendanceCalculatorSummaryDot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="4dp"
android:gravity="center"
android:text="·"
android:textColor="?android:textColorSecondary"
android:textSize="13sp"
app:layout_constraintEnd_toStartOf="@+id/attendanceCalculatorSummaryBalance"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/attendanceCalculatorSummaryValues"
app:layout_constraintTop_toBottomOf="@+id/attendanceCalculatorTitle" />
<TextView
android:id="@+id/attendanceCalculatorSummaryBalance"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="left"
android:maxLines="1"
android:minWidth="24dp"
android:textColor="?android:textColorSecondary"
android:textSize="13sp"
app:layout_constraintEnd_toStartOf="@+id/attendanceCalculatorWarning"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@+id/attendanceCalculatorSummaryDot"
app:layout_constraintTop_toBottomOf="@+id/attendanceCalculatorTitle"
tools:text="12 powyżej celu" />
<TextView
android:id="@+id/attendanceCalculatorTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:maxLines="1"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@+id/attendanceCalculatorWarning"
app:layout_constraintStart_toEndOf="@+id/attendanceCalculatorPercentagePercentSign"
app:layout_constraintTop_toTopOf="parent"
tools:text="Informatyka" />
<ImageView
android:id="@+id/attendanceCalculatorWarning"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription=""
android:layout_marginStart="12dp"
android:gravity="center_vertical|right"
android:minWidth="24dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_all_round_mark"
app:tint="?colorPrimary"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,140 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Taken from https://android.googlesource.com/platform/frameworks/support/+/refs/heads/androidx-camerax-release/preference/preference/res/layout/preference_widget_seekbar_material.xml.
~ Copyright (C) 2018 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<!-- Layout used by SeekBarPreference for the seekbar widget style. -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?android:attr/listPreferredItemHeightSmall"
android:gravity="center_vertical"
android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingRight="?android:attr/listPreferredItemPaddingRight"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
android:background="?android:attr/selectableItemBackground"
android:clipChildren="false"
android:clipToPadding="false"
android:baselineAligned="false">
<include layout="@layout/image_frame"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:clipChildren="false"
android:clipToPadding="false">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_weight="1">
<TextView
android:id="@android:id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceListItem"
android:ellipsize="marquee"/>
<TextView
android:id="@android:id/summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@android:id/title"
android:layout_alignLeft="@android:id/title"
android:layout_alignStart="@android:id/title"
android:layout_gravity="start"
android:textAlignment="viewStart"
android:textColor="?android:attr/textColorSecondary"
android:maxLines="4"
style="@style/PreferenceSummaryTextStyle"/>
</RelativeLayout>
<!-- Using UnPressableLinearLayout as a workaround to disable the pressed state propagation
to the children of this container layout. Otherwise, the animated pressed state will also
play for the thumb in the AbsSeekBar in addition to the preference's ripple background.
The background of the SeekBar is also set to null to disable the ripple background -->
<androidx.preference.UnPressableLinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingLeft="0dp"
android:paddingStart="0dp"
android:paddingRight="16dp"
android:paddingEnd="16dp"
android:clipChildren="false"
android:clipToPadding="false">
<!-- The total height of the Seekbar widget's area should be 48dp - this allows for an
increased touch area so you do not need to exactly tap the thumb to move it. However,
setting the Seekbar height directly causes the thumb and seekbar to be misaligned on
API 22 and 23 - so instead we just set 15dp padding above and below, to account for the
18dp default height of the Seekbar thumb for a total of 48dp.
Note: we set 0dp padding at the start and end of this seekbar to allow it to properly
fit into the layout, but this means that there's no leeway on either side for touch
input - this might be something we should reconsider down the line. -->
<SeekBar
android:id="@+id/seekbar"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:paddingLeft="@dimen/preference_seekbar_padding_horizontal"
android:paddingStart="@dimen/preference_seekbar_padding_horizontal"
android:paddingRight="@dimen/preference_seekbar_padding_horizontal"
android:paddingEnd="@dimen/preference_seekbar_padding_horizontal"
android:paddingTop="@dimen/preference_seekbar_padding_vertical"
android:paddingBottom="@dimen/preference_seekbar_padding_vertical"
android:background="@null"/>
<!-- If the value is shown, we reserve a minimum width of 36dp to allow for consistent
seekbar width for smaller values. If the value is ~4 or more digits, it will expand
into the seekbar width. -->
<TextView
android:id="@+id/seekbar_value"
android:minWidth="@dimen/preference_seekbar_value_minWidth"
android:paddingLeft="8dp"
android:paddingStart="8dp"
android:paddingRight="0dp"
android:paddingEnd="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="right"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceListItem"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:scrollbars="none"/>
<!-- Wulkanowy start -->
<TextView
android:minWidth="0dp"
android:paddingLeft="0dp"
android:paddingStart="0dp"
android:paddingRight="0dp"
android:paddingEnd="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="left"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceListItem"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:scrollbars="none"
android:text="%"
/>
<!-- Wulkanowy end -->
</androidx.preference.UnPressableLinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -1,13 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/attendanceMenuCalculator"
android:icon="@drawable/ic_menu_attendance_calculator"
android:orderInCategory="0"
android:title="@string/attendance_calculator_button"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" />
<item
android:id="@+id/attendanceMenuSummary"
android:icon="@drawable/ic_menu_attendance_summary"

View File

@ -1,10 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="sort_alphabetically">Abecedně</string>
<string name="sort_by_date">Podle data</string>
<string name="sort_by_average">Podle průměru</string>
<string name="sort_by_attendance_percentage">Podle procenta docházky</string>
<string name="sort_by_subject_attendance_balance">Podle rovnováhy docházky předmětu</string>
<string-array name="app_theme_entries" tools:ignore="InconsistentArrays">
<item>Světlý</item>
<item>Tmavý</item>
@ -36,6 +31,11 @@
<item>0,5</item>
<item>0,75</item>
</string-array>
<string-array name="grade_sorting_mode_entries">
<item>Abecedně</item>
<item>Podle data</item>
<item>Podle průměru</item>
</string-array>
<string-array name="grade_color_scheme_entries">
<item>Dzienniczek+</item>
<item>Wulkanowy</item>
@ -59,7 +59,7 @@
<string-array name="dashboard_tile_entries">
<item>Šťastné číslo</item>
<item>Nepřečtené zprávy</item>
<item>Docházka</item>
<item>Frekvence</item>
<item>Lekce</item>
<item>Známky</item>
<item>Domácí úkoly</item>

View File

@ -4,7 +4,7 @@
<string name="login_title">Přihlášení</string>
<string name="main_title">Wulkanowy</string>
<string name="grade_title">Známky</string>
<string name="attendance_title">Docházka</string>
<string name="attendance_title">Frekvence</string>
<string name="exam_title">Zkoušky</string>
<string name="timetable_title">Plán lekce</string>
<string name="settings_title">Nastavení</string>
@ -64,7 +64,7 @@
<string name="login_symbol_helper">Symbol najdete na stránce deníku v &#160;<b>Uczeń</b>&#160;<b>Dostęp Mobilny</b>&#160;<b>Wygeneruj kod dostępu</b>.\n\nUjistěte se, že jste nastavili správnou variantu deníku v poli <b>Variace deníku UONET+</b> na první přihlašovací obrazovce</string>
<string name="login_select_student">Vyberte žáky, kteří se mají do aplikace přihlásit</string>
<string name="login_advanced">Jiné možnosti</string>
<string name="login_advanced_warning_mobile_api">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í</string>
<string name="login_advanced_warning_mobile_api">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í</string>
<string name="login_advanced_warning_scraper">Tento režim zobrazuje stejná data, která se zobrazují na webových stránkách deníka</string>
<string name="login_advanced_warning_hybrid">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</string>
<string name="login_privacy_policy">Ochrana osobních údajů</string>
@ -264,12 +264,7 @@
<string name="additional_lessons_end">Čas ukončení</string>
<string name="additional_lessons_end_time_error">Čas ukončení musí být pozdější než čas zahájení</string>
<!--Attendance-->
<string name="attendance_summary_button">Shrnutí docházky</string>
<string name="attendance_calculator_button">Kalkulačka docházky</string>
<string name="attendance_calculator_summary_balance_positive"><b>%1$d</b> nad cílem</string>
<string name="attendance_calculator_summary_balance_neutral">přesně v cíli</string>
<string name="attendance_calculator_summary_balance_negative"><b>%1$d</b> pod cílem</string>
<string name="attendance_calculator_summary_values">%1$d/%2$d přítomnosti</string>
<string name="attendance_summary_button">Shrnutí frekvencí</string>
<string name="attendance_absence_school">Nepřítomnost ze školních důvodů</string>
<string name="attendance_absence_excused">Omluvená nepřítomnost</string>
<string name="attendance_absence_unexcused">Neomluvená nepřítomnost</string>
@ -287,22 +282,22 @@
<string name="attendance_excuse_no_selection">Musíte vybrat alespoň jednu nepřítomnost!</string>
<string name="attendance_excuse_title">Ospravedlnit</string>
<plurals name="attendance_notify_new_items_title">
<item quantity="one">Nová docházka</item>
<item quantity="few">Nové docházky</item>
<item quantity="many">Nové docházky</item>
<item quantity="other">Nové docházky</item>
<item quantity="one">Nové frekvence</item>
<item quantity="few">Nové frekvence</item>
<item quantity="many">Nové frekvence</item>
<item quantity="other">Nové frekvence</item>
</plurals>
<plurals name="attendance_notify_new_items">
<item quantity="one">%1$d nová docházka</item>
<item quantity="few">%1$d nové docházky</item>
<item quantity="many">%1$d nových docházek</item>
<item quantity="other">%1$d nových docházek</item>
<item quantity="one">%1$d nové frekvence</item>
<item quantity="few">%1$d nové frekvence</item>
<item quantity="many">%1$d nových frekvencí</item>
<item quantity="other">%1$d nových frekvencí</item>
</plurals>
<plurals name="attendance_number_item">
<item quantity="one">%d docházka</item>
<item quantity="few">%d docházky</item>
<item quantity="many">%d docházek</item>
<item quantity="other">%d docházek</item>
<item quantity="one">%d frekvence</item>
<item quantity="few">%d frekvence</item>
<item quantity="many">%d frekvencí</item>
<item quantity="other">%d frekvencí</item>
</plurals>
<!--Attendance summary-->
<string name="attendance_summary_total">Společně</string>
@ -736,8 +731,6 @@
<string name="pref_view_grade_average_mode">Možnosti vypočítaného průměru</string>
<string name="pref_view_grade_average_force_calc">Vynutit průměrný výpočet podle aplikace</string>
<string name="pref_view_present">Zobrazit přítomnost</string>
<string name="pref_attendance_target">Cílová docházka</string>
<string name="pref_view_attendance_calculator_sorting_mode">Třídění kalkulačky docházky</string>
<string name="pref_view_app_theme">Motiv</string>
<string name="pref_view_expand_grade">Rozvíjení známek</string>
<string name="pref_view_timetable_show_groups">Zobrazit skupiny vedle předmětů</string>
@ -804,7 +797,7 @@
<string name="pref_grades_appearance_header">Známky</string>
<string name="pref_dashboard_appearance_header">Domů</string>
<string name="pref_dashboard_appearance_tiles_title">Viditelnost dlaždic</string>
<string name="pref_attendance_appearance_view">Docházka</string>
<string name="pref_attendance_appearance_view">Frekvence</string>
<string name="pref_timetable_appearance_view">Plán lekce</string>
<string name="pref_grades_advanced_header">Známky</string>
<string name="pref_counted_average_advanced_header">Vypočítaný průměr</string>
@ -832,7 +825,7 @@
<string name="channel_upcoming_lessons">Nadcházející lekce</string>
<string name="channel_debug">Ladění</string>
<string name="channel_change_timetable">Změny plánu lekcí</string>
<string name="channel_new_attendance">Nové docházky</string>
<string name="channel_new_attendance">Nové frekvence</string>
<!--Colors-->
<string name="all_black">Černá</string>
<string name="all_red">Červená</string>

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string-array name="app_theme_entries" tools:ignore="InconsistentArrays">
<item>Light</item>
<item>Dark</item>
<item>Black (AMOLED)</item>
</string-array>
<string-array name="app_language_entries">
<item>System language</item>
<item>Polski</item>
<item>English</item>
<item>Pусский</item>
<item>Українська</item>
<item>Deutsch</item>
<item>Čeština</item>
<item>Slovenčina</item>
</string-array>
<string-array name="services_interval_entries">
<item>15 minutes</item>
<item>30 minutes</item>
<item>1 hour</item>
<item>2 hours</item>
<item>6 hours</item>
<item>12 hours</item>
<item>24 hours</item>
</string-array>
<string-array name="grade_modifier_entries">
<item>0,00</item>
<item>0,25</item>
<item>0,33</item>
<item>0,5</item>
<item>0,75</item>
</string-array>
<string-array name="grade_sorting_mode_entries">
<item>Alphabetically</item>
<item>By date</item>
<item>By average</item>
</string-array>
<string-array name="grade_color_scheme_entries">
<item>Dzienniczek+</item>
<item>Wulkanowy</item>
<item>Grade colors in register</item>
</string-array>
<string-array name="default_expand_grade_entries">
<item>Up to 1 at once</item>
<item>Always expanded</item>
<item>Unlimited expansions</item>
</string-array>
<string-array name="grade_average_mode_entries">
<item>Average of grades only from selected semester</item>
<item>Average of averages from both semesters</item>
<item>Average of grades from the whole year</item>
</string-array>
<string-array name="timetable_show_gaps_entries">
<item>Don\'t show</item>
<item>Only between lessons</item>
<item>Before and between lessons</item>
</string-array>
<string-array name="dashboard_tile_entries">
<item>Lucky number</item>
<item>Unread messages</item>
<item>Attendance</item>
<item>Lessons</item>
<item>Grades</item>
<item>Homework</item>
<item>School announcements</item>
<item>Exams</item>
<item>Conferences</item>
</string-array>
</resources>

View File

@ -1,10 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="sort_alphabetically">Alphabetically</string>
<string name="sort_by_date">By date</string>
<string name="sort_by_average">By average</string>
<string name="sort_by_attendance_percentage">By attendance percentage</string>
<string name="sort_by_subject_attendance_balance">By subject attendance balance</string>
<string-array name="app_theme_entries" tools:ignore="InconsistentArrays">
<item>Licht</item>
<item>Dunkel</item>
@ -36,6 +31,11 @@
<item>0,5</item>
<item>0,75</item>
</string-array>
<string-array name="grade_sorting_mode_entries">
<item>Alphabetisch</item>
<item>Nach Datum</item>
<item>Nach Durchschnitt</item>
</string-array>
<string-array name="grade_color_scheme_entries">
<item>Dzienniczek+</item>
<item>Wulkanowy</item>

View File

@ -237,11 +237,6 @@
<string name="additional_lessons_end_time_error">Endzeit muss grösser sein als Startzeit</string>
<!--Attendance-->
<string name="attendance_summary_button">Übersicht über die Schulbesuch</string>
<string name="attendance_calculator_button">Attendance calculator</string>
<string name="attendance_calculator_summary_balance_positive"><b>%1$d</b> over target</string>
<string name="attendance_calculator_summary_balance_neutral">right on target</string>
<string name="attendance_calculator_summary_balance_negative"><b>%1$d</b> under target</string>
<string name="attendance_calculator_summary_values">%1$d/%2$d presences</string>
<string name="attendance_absence_school">Aus schulischen Gründen abwesend</string>
<string name="attendance_absence_excused">Entschuldigte Abwesenheit</string>
<string name="attendance_absence_unexcused">Unentschuldigtes Abwesenheit</string>
@ -642,8 +637,6 @@
<string name="pref_view_grade_average_mode">Berechnete Durchschnittsoptionen</string>
<string name="pref_view_grade_average_force_calc">Mittelwertberechnung durch App erzwingen</string>
<string name="pref_view_present">Anwesendheit zeigen</string>
<string name="pref_attendance_target">Attendance target</string>
<string name="pref_view_attendance_calculator_sorting_mode">Attendance calculator sorting</string>
<string name="pref_view_app_theme">Thema</string>
<string name="pref_view_expand_grade">Steigende Sorten</string>
<string name="pref_view_timetable_show_groups">Gruppen neben Schulfächen anzeigen</string>

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string-array name="app_theme_entries" tools:ignore="InconsistentArrays">
<item>Light</item>
<item>Dark</item>
<item>Black (AMOLED)</item>
</string-array>
<string-array name="app_language_entries">
<item>System language</item>
<item>Polski</item>
<item>English</item>
<item>Pусский</item>
<item>Українська</item>
<item>Deutsch</item>
<item>Čeština</item>
<item>Slovenčina</item>
</string-array>
<string-array name="services_interval_entries">
<item>15 minutes</item>
<item>30 minutes</item>
<item>1 hour</item>
<item>2 hours</item>
<item>6 hours</item>
<item>12 hours</item>
<item>24 hours</item>
</string-array>
<string-array name="grade_modifier_entries">
<item>0,00</item>
<item>0,25</item>
<item>0,33</item>
<item>0,5</item>
<item>0,75</item>
</string-array>
<string-array name="grade_sorting_mode_entries">
<item>Alphabetically</item>
<item>By date</item>
<item>By average</item>
</string-array>
<string-array name="grade_color_scheme_entries">
<item>Dzienniczek+</item>
<item>Wulkanowy</item>
<item>Grade colors in register</item>
</string-array>
<string-array name="default_expand_grade_entries">
<item>Up to 1 at once</item>
<item>Always expanded</item>
<item>Unlimited expansions</item>
</string-array>
<string-array name="grade_average_mode_entries">
<item>Average of grades only from selected semester</item>
<item>Average of averages from both semesters</item>
<item>Average of grades from the whole year</item>
</string-array>
<string-array name="timetable_show_gaps_entries">
<item>Don\'t show</item>
<item>Only between lessons</item>
<item>Before and between lessons</item>
</string-array>
<string-array name="dashboard_tile_entries">
<item>Lucky number</item>
<item>Unread messages</item>
<item>Attendance</item>
<item>Lessons</item>
<item>Grades</item>
<item>Homework</item>
<item>School announcements</item>
<item>Exams</item>
<item>Conferences</item>
</string-array>
</resources>

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string-array name="app_theme_entries" tools:ignore="InconsistentArrays">
<item>Light</item>
<item>Dark</item>
<item>Black (AMOLED)</item>
</string-array>
<string-array name="app_language_entries">
<item>System language</item>
<item>Polski</item>
<item>English</item>
<item>Pусский</item>
<item>Українська</item>
<item>Deutsch</item>
<item>Čeština</item>
<item>Slovenčina</item>
</string-array>
<string-array name="services_interval_entries">
<item>15 minutes</item>
<item>30 minutes</item>
<item>1 hour</item>
<item>2 hours</item>
<item>6 hours</item>
<item>12 hours</item>
<item>24 hours</item>
</string-array>
<string-array name="grade_modifier_entries">
<item>0,00</item>
<item>0,25</item>
<item>0,33</item>
<item>0,5</item>
<item>0,75</item>
</string-array>
<string-array name="grade_sorting_mode_entries">
<item>Alphabetically</item>
<item>By date</item>
<item>By average</item>
</string-array>
<string-array name="grade_color_scheme_entries">
<item>Dzienniczek+</item>
<item>Wulkanowy</item>
<item>Grade colors in register</item>
</string-array>
<string-array name="default_expand_grade_entries">
<item>Up to 1 at once</item>
<item>Always expanded</item>
<item>Unlimited expansions</item>
</string-array>
<string-array name="grade_average_mode_entries">
<item>Average of grades only from selected semester</item>
<item>Average of averages from both semesters</item>
<item>Average of grades from the whole year</item>
</string-array>
<string-array name="timetable_show_gaps_entries">
<item>Don\'t show</item>
<item>Only between lessons</item>
<item>Before and between lessons</item>
</string-array>
<string-array name="dashboard_tile_entries">
<item>Lucky number</item>
<item>Unread messages</item>
<item>Attendance</item>
<item>Lessons</item>
<item>Grades</item>
<item>Homework</item>
<item>School announcements</item>
<item>Exams</item>
<item>Conferences</item>
</string-array>
</resources>

View File

@ -1,10 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="sort_alphabetically">Alfabetycznie</string>
<string name="sort_by_date">Według daty</string>
<string name="sort_by_average">Według średniej</string>
<string name="sort_by_attendance_percentage">Według procentu obecności</string>
<string name="sort_by_subject_attendance_balance">Według balansu frekwencji przedmiotu</string>
<string-array name="app_theme_entries" tools:ignore="InconsistentArrays">
<item>Jasny</item>
<item>Ciemny</item>
@ -36,6 +31,11 @@
<item>0,5</item>
<item>0,75</item>
</string-array>
<string-array name="grade_sorting_mode_entries">
<item>Alfabetycznie</item>
<item>Według daty</item>
<item>Według średniej</item>
</string-array>
<string-array name="grade_color_scheme_entries">
<item>Dzienniczek+</item>
<item>Wulkanowy</item>

View File

@ -265,11 +265,6 @@
<string name="additional_lessons_end_time_error">Godzina zakończenia musi być późniejsza niż godzina rozpoczęcia</string>
<!--Attendance-->
<string name="attendance_summary_button">Podsumowanie frekwencji</string>
<string name="attendance_calculator_button">Kalkulator obecności</string>
<string name="attendance_calculator_summary_balance_positive"><b>%1$d</b> powyżej celu</string>
<string name="attendance_calculator_summary_balance_neutral">dokładnie u celu</string>
<string name="attendance_calculator_summary_balance_negative"><b>%1$d</b> poniżej celu</string>
<string name="attendance_calculator_summary_values">%1$d/%2$d obecności</string>
<string name="attendance_absence_school">Nieobecność z przyczyn szkolnych</string>
<string name="attendance_absence_excused">Nieobecność usprawiedliwiona</string>
<string name="attendance_absence_unexcused">Nieobecność nieusprawiedliwiona</string>
@ -736,8 +731,6 @@
<string name="pref_view_grade_average_mode">Opcje obliczonej średniej</string>
<string name="pref_view_grade_average_force_calc">Wymuś obliczanie średniej przez aplikację</string>
<string name="pref_view_present">Pokazuj obecność</string>
<string name="pref_attendance_target">Docelowa obecność</string>
<string name="pref_view_attendance_calculator_sorting_mode">Sortowanie kalkulatora obecności</string>
<string name="pref_view_app_theme">Motyw</string>
<string name="pref_view_expand_grade">Rozwijanie ocen</string>
<string name="pref_view_timetable_show_groups">Pokazuj grupę obok przedmiotu</string>

View File

@ -1,10 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="sort_alphabetically">Alphabetically</string>
<string name="sort_by_date">By date</string>
<string name="sort_by_average">By average</string>
<string name="sort_by_attendance_percentage">By attendance percentage</string>
<string name="sort_by_subject_attendance_balance">By subject attendance balance</string>
<string-array name="app_theme_entries" tools:ignore="InconsistentArrays">
<item>Светлая</item>
<item>Тёмная</item>
@ -36,6 +31,11 @@
<item>0,5</item>
<item>0,75</item>
</string-array>
<string-array name="grade_sorting_mode_entries">
<item>В алфавитном порядке</item>
<item>По дате</item>
<item>По средней</item>
</string-array>
<string-array name="grade_color_scheme_entries">
<item>Dzienniczek+</item>
<item>Wulkanowy</item>

View File

@ -265,11 +265,6 @@
<string name="additional_lessons_end_time_error">Время окончания должно быть больше, чем время начала</string>
<!--Attendance-->
<string name="attendance_summary_button">Итоговая посещаемость</string>
<string name="attendance_calculator_button">Attendance calculator</string>
<string name="attendance_calculator_summary_balance_positive"><b>%1$d</b> over target</string>
<string name="attendance_calculator_summary_balance_neutral">right on target</string>
<string name="attendance_calculator_summary_balance_negative"><b>%1$d</b> under target</string>
<string name="attendance_calculator_summary_values">%1$d/%2$d presences</string>
<string name="attendance_absence_school">Отсутствие по школьным причинам</string>
<string name="attendance_absence_excused">Отсутствие по уважительной причине</string>
<string name="attendance_absence_unexcused">Отсутствие по неуважительной причине</string>
@ -736,8 +731,6 @@
<string name="pref_view_grade_average_mode">Параметры расчёта средних оценок</string>
<string name="pref_view_grade_average_force_calc">Принудительно высчитать среднюю оценку через приложение</string>
<string name="pref_view_present">Показывать присутствия</string>
<string name="pref_attendance_target">Attendance target</string>
<string name="pref_view_attendance_calculator_sorting_mode">Attendance calculator sorting</string>
<string name="pref_view_app_theme">Тема</string>
<string name="pref_view_expand_grade">Разворачивание оценок</string>
<string name="pref_view_timetable_show_groups">Показать группы рядом с темами</string>

View File

@ -1,10 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="sort_alphabetically">Abecedne</string>
<string name="sort_by_date">Podľa dátumu</string>
<string name="sort_by_average">Podľa priemeru</string>
<string name="sort_by_attendance_percentage">Podľa percenta dochádzky</string>
<string name="sort_by_subject_attendance_balance">Podľa rovnováhy dochádzky predmetu</string>
<string-array name="app_theme_entries" tools:ignore="InconsistentArrays">
<item>Svetlý</item>
<item>Tmavý</item>
@ -36,6 +31,11 @@
<item>0,5</item>
<item>0,75</item>
</string-array>
<string-array name="grade_sorting_mode_entries">
<item>Abecedne</item>
<item>Podľa dátumu</item>
<item>Podľa priemeru</item>
</string-array>
<string-array name="grade_color_scheme_entries">
<item>Dzienniczek+</item>
<item>Wulkanowy</item>
@ -59,7 +59,7 @@
<string-array name="dashboard_tile_entries">
<item>Šťastné číslo</item>
<item>Neprečítané správy</item>
<item>Dochádzka</item>
<item>Frekvencia</item>
<item>Lekcie</item>
<item>Známky</item>
<item>Domáce úlohy</item>

View File

@ -4,7 +4,7 @@
<string name="login_title">Prihlásenie</string>
<string name="main_title">Wulkanowy</string>
<string name="grade_title">Známky</string>
<string name="attendance_title">Dochádzka</string>
<string name="attendance_title">Frekvencia</string>
<string name="exam_title">Skúšky</string>
<string name="timetable_title">Plán lekcie</string>
<string name="settings_title">Nastavenia</string>
@ -64,7 +64,7 @@
<string name="login_symbol_helper">Symbol nájdete na stránke denníka v &#160;<b>Uczeń</b>&#160;<b>Dostęp Mobilny</b>&#160;<b>Wygeneruj kod dostępu</b>.\n\nUistite sa, že ste nastavili správny variant denníka v poli <b>Variácia denníka UONET+</b> na prvej prihlasovacej obrazovke</string>
<string name="login_select_student">Vyberte žiakov, ktorí sa majú do aplikácie prihlásiť</string>
<string name="login_advanced">Iné možnosti</string>
<string name="login_advanced_warning_mobile_api">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í</string>
<string name="login_advanced_warning_mobile_api">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í</string>
<string name="login_advanced_warning_scraper">Tento režim zobrazuje rovnaké dáta, ktoré sa zobrazujú na webových stránkach denníka</string>
<string name="login_advanced_warning_hybrid">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</string>
<string name="login_privacy_policy">Ochrana osobných údajov</string>
@ -264,12 +264,7 @@
<string name="additional_lessons_end">Čas ukončenia</string>
<string name="additional_lessons_end_time_error">Čas ukončenia musí byť neskorší ako čas začatia</string>
<!--Attendance-->
<string name="attendance_summary_button">Zhrnutie dochádzky</string>
<string name="attendance_calculator_button">Kalkulačka dochádzky</string>
<string name="attendance_calculator_summary_balance_positive"><b>%1$d</b> nad cieľom</string>
<string name="attendance_calculator_summary_balance_neutral">presne v cieli</string>
<string name="attendance_calculator_summary_balance_negative"><b>%1$d</b> pod cieľom</string>
<string name="attendance_calculator_summary_values">%1$d/%2$d prítomnosti</string>
<string name="attendance_summary_button">Zhrnutie frekvencií</string>
<string name="attendance_absence_school">Neprítomnosť zo školských dôvodov</string>
<string name="attendance_absence_excused">Ospravedlnená neprítomnosť</string>
<string name="attendance_absence_unexcused">Neospravedlnená neprítomnosť</string>
@ -287,22 +282,22 @@
<string name="attendance_excuse_no_selection">Musíte vybrať aspoň jednu neprítomnosť!</string>
<string name="attendance_excuse_title">Ospravedlniť</string>
<plurals name="attendance_notify_new_items_title">
<item quantity="one">Nová dochádzka</item>
<item quantity="few">Nové dochádzky</item>
<item quantity="many">Nové dochádzky</item>
<item quantity="other">Nové dochádzky</item>
<item quantity="one">Nová frekvencia</item>
<item quantity="few">Nové frekvencie</item>
<item quantity="many">Nové frekvencie</item>
<item quantity="other">Nové frekvencie</item>
</plurals>
<plurals name="attendance_notify_new_items">
<item quantity="one">%1$d nová dochádzka</item>
<item quantity="few">%1$d nové dochádzky</item>
<item quantity="many">%1$d nových dochádzok</item>
<item quantity="other">%1$d nových dochádzok</item>
<item quantity="one">%1$d nová frekvencia</item>
<item quantity="few">%1$d nové frekvencie</item>
<item quantity="many">%1$d nových frekvencií</item>
<item quantity="other">%1$d nových frekvencií</item>
</plurals>
<plurals name="attendance_number_item">
<item quantity="one">%d dochádzka</item>
<item quantity="few">%d dochádzky</item>
<item quantity="many">%d dochádzok</item>
<item quantity="other">%d dochádzok</item>
<item quantity="one">%d frekvencia</item>
<item quantity="few">%d frekvencie</item>
<item quantity="many">%d frekvencií</item>
<item quantity="other">%d frekvencií</item>
</plurals>
<!--Attendance summary-->
<string name="attendance_summary_total">Spoločne</string>
@ -736,8 +731,6 @@
<string name="pref_view_grade_average_mode">Možnosti vypočítaného priemeru</string>
<string name="pref_view_grade_average_force_calc">Vynútiť priemerný výpočet podľa aplikácie</string>
<string name="pref_view_present">Zobraziť prítomnosť</string>
<string name="pref_attendance_target">Cieľová dochádzka</string>
<string name="pref_view_attendance_calculator_sorting_mode">Triedenie kalkulačky dochádzky</string>
<string name="pref_view_app_theme">Motív</string>
<string name="pref_view_expand_grade">Rozvijanie známok</string>
<string name="pref_view_timetable_show_groups">Zobraziť skupiny vedľa predmetov</string>
@ -804,7 +797,7 @@
<string name="pref_grades_appearance_header">Známky</string>
<string name="pref_dashboard_appearance_header">Domov</string>
<string name="pref_dashboard_appearance_tiles_title">Viditeľnosť dlaždíc</string>
<string name="pref_attendance_appearance_view">Dochádzka</string>
<string name="pref_attendance_appearance_view">Frekvencia</string>
<string name="pref_timetable_appearance_view">Plán lekcie</string>
<string name="pref_grades_advanced_header">Známky</string>
<string name="pref_counted_average_advanced_header">Vypočítaný priemer</string>
@ -832,7 +825,7 @@
<string name="channel_upcoming_lessons">Nadchádzajúce lekcie</string>
<string name="channel_debug">Ladenie</string>
<string name="channel_change_timetable">Zmeny plánu lekcií</string>
<string name="channel_new_attendance">Nové dochádzky</string>
<string name="channel_new_attendance">Nové frekvencie</string>
<!--Colors-->
<string name="all_black">Čierna</string>
<string name="all_red">Červená</string>

View File

@ -1,10 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="sort_alphabetically">За алфавітом</string>
<string name="sort_by_date">За датою</string>
<string name="sort_by_average">За середньою</string>
<string name="sort_by_attendance_percentage">За відсотком відвідуваності</string>
<string name="sort_by_subject_attendance_balance">За балансом відвідування теми</string>
<string-array name="app_theme_entries" tools:ignore="InconsistentArrays">
<item>Світла</item>
<item>Темна</item>
@ -36,6 +31,11 @@
<item>0,5</item>
<item>0,75</item>
</string-array>
<string-array name="grade_sorting_mode_entries">
<item>За алфавітом</item>
<item>За датою</item>
<item>За середньою</item>
</string-array>
<string-array name="grade_color_scheme_entries">
<item>Dzienniczek+</item>
<item>Wulkanowy</item>

View File

@ -265,11 +265,6 @@
<string name="additional_lessons_end_time_error">Час завершення має бути пізніше часу початку</string>
<!--Attendance-->
<string name="attendance_summary_button">Підсумок відвідуваності</string>
<string name="attendance_calculator_button">Калькулятор відвідуваності</string>
<string name="attendance_calculator_summary_balance_positive"><b>%1$d</b> понад ціль</string>
<string name="attendance_calculator_summary_balance_neutral">точно у цілі</string>
<string name="attendance_calculator_summary_balance_negative"><b>%1$d</b> під ціллю</string>
<string name="attendance_calculator_summary_values">%1$d/%2$d відвідуваності</string>
<string name="attendance_absence_school">Відсутність зі шкільних причин</string>
<string name="attendance_absence_excused">Відсутність з поважних причин</string>
<string name="attendance_absence_unexcused">Відсутність без поважних причин</string>
@ -736,8 +731,6 @@
<string name="pref_view_grade_average_mode">Параметри розраховування середніх оцінок</string>
<string name="pref_view_grade_average_force_calc">Примусово розраховувати середню оцінку через додаток</string>
<string name="pref_view_present">Показувати присутність</string>
<string name="pref_attendance_target">Цільова відвідуваність</string>
<string name="pref_view_attendance_calculator_sorting_mode">Сортування калькулятора відвідування</string>
<string name="pref_view_app_theme">Тема</string>
<string name="pref_view_expand_grade">Розгортання оцінок</string>
<string name="pref_view_timetable_show_groups">Показувати групи поруч з темами</string>

View File

@ -1,12 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorIcon">#d32f2f</color>
<color name="colorPrimary">#9c423a</color>
<color name="colorOnPrimary">#ffffff</color>
<color name="colorPrimaryContainer">#ffdad6</color>
<color name="colorOnPrimaryContainer">#410002</color>
<color name="colorPrimary">#B91B21</color>
<color name="colorOnPrimary">#FFFFFF</color>
<color name="colorPrimaryContainer">#FFDAD5</color>
<color name="colorOnPrimaryContainer">#410003</color>
<color name="colorSurface">#FCFCFC</color>
<color name="colorOnSurface">#201A1A</color>
<color name="colorSurfaceVariant">#F4DDDB</color>
@ -16,12 +14,10 @@
<color name="colorOnSurfaceInverse">#FAEEEE</color>
<color name="colorPrimaryInverse">#FFB3AA</color>
<color name="colorPrimaryDark">#ffb4ab</color>
<color name="colorOnPrimaryDark">#5f1411</color>
<color name="colorPrimaryContainerDark">#7d2b25</color>
<color name="colorOnPrimaryContainerDark">#ffdad6</color>
<color name="colorPrimaryDark">#FFB3AA</color>
<color name="colorOnPrimaryDark">#680006</color>
<color name="colorPrimaryContainerDark">#93000E</color>
<color name="colorOnPrimaryContainerDark">#FFDAD5</color>
<color name="colorSurfaceDark">#201A1A</color>
<color name="colorOnSurfaceDark">#ECE0E0</color>
<color name="colorSurfaceVariantDark">#534341</color>
@ -31,16 +27,15 @@
<color name="colorOnSurfaceInverseDark">#201A1A</color>
<color name="colorPrimaryInverseDark">#B91B21</color>
<color name="colorPrimaryDarker">#9a0007</color>
<color name="colorError">#ff5722</color>
<color name="colorErrorLight">#e84853</color>
<color name="colorNavigationBarLight">#f4ecec</color>
<color name="colorNavigationBarLight">#f5e8e9</color>
<color name="colorNavigationBarDark">#312624</color>
<color name="colorNavigationBarBlack">#150e0e</color>
<color name="colorStatusBarLight">#f2eae9</color>
<color name="colorStatusBarLight">#f5e5e6</color>
<color name="colorStatusBarDark">#342826</color>
<color name="colorStatusBarBlack">#181010</color>

View File

@ -2,9 +2,6 @@
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="pref_default_startup">0</string>
<bool name="pref_default_attendance_present">true</bool>
<integer name="pref_default_attendance_target">50</integer>
<string name="pref_default_attendance_calculator_sorting_mode">alphabetic</string>
<bool name="pref_default_attendance_calculator_show_empty_subjects">false</bool>
<string name="pref_default_grade_average_mode">only_one_semester</string>
<bool name="pref_default_grade_average_force_calc">false</bool>
<string name="pref_default_expand_grade_mode">one</string>

View File

@ -2,9 +2,6 @@
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
<string name="pref_key_start_menu">default_menu_index</string>
<string name="pref_key_attendance_present">attendance_present</string>
<string name="pref_key_attendance_target">attendance_target</string>
<string name="pref_key_attendance_calculator_sorting_mode">attendance_calculator_sorting_mode</string>
<string name="pref_key_attendance_calculator_show_empty_subjects">attendance_calculator_show_empty_subjects</string>
<string name="pref_key_app_theme">app_theme</string>
<string name="pref_key_dashboard_tiles">dashboard_tiles</string>
<string name="pref_key_grade_color_scheme">grade_color_scheme</string>

View File

@ -1,12 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="sort_alphabetically">Alphabetically</string>
<string name="sort_by_date">By date</string>
<string name="sort_by_average">By average</string>
<string name="sort_by_attendance_percentage">By attendance percentage</string>
<string name="sort_by_subject_attendance_balance">By subject attendance balance</string>
<string-array name="startup_tab_entries" translatable="false">
<item>@string/dashboard_title</item>
<item>@string/grade_title</item>
@ -86,21 +79,10 @@
<item>0.75</item>
</string-array>
<string-array name="attendance_calculator_sorting_mode_entries" translatable="false">
<item>@string/sort_alphabetically</item>
<item>@string/sort_by_attendance_percentage</item>
<item>@string/sort_by_subject_attendance_balance</item>
</string-array>
<string-array name="attendance_calculator_sorting_mode_values" translatable="false">
<item>alphabetic</item>
<item>attendance_percentage</item>
<item>lesson_balance</item>
</string-array>
<string-array name="grade_sorting_mode_entries" translatable="false">
<item>@string/sort_alphabetically</item>
<item>@string/sort_by_date</item>
<item>@string/sort_by_average</item>
<string-array name="grade_sorting_mode_entries">
<item>Alphabetically</item>
<item>By date</item>
<item>By average</item>
</string-array>
<string-array name="grade_sorting_mode_values" translatable="false">
<item>alphabetic</item>

View File

@ -258,12 +258,6 @@
<!--Attendance-->
<string name="attendance_summary_button">Attendance summary</string>
<string name="attendance_calculator_button">Attendance calculator</string>
<string name="attendance_calculator_summary_balance_positive"><b>%1$d</b> over target</string>
<string name="attendance_calculator_summary_balance_neutral">right on target</string>
<string name="attendance_calculator_summary_balance_negative"><b>%1$d</b> under target</string>
<string name="attendance_calculator_summary_values">%1$d/%2$d presences</string>
<string name="attendance_calculator_summary_values_empty">No attendances recorded</string>
<string name="attendance_absence_school">Absent for school reasons</string>
<string name="attendance_absence_excused">Excused absence</string>
<string name="attendance_absence_unexcused">Unexcused absence</string>
@ -721,9 +715,6 @@
<string name="pref_view_grade_average_mode">Calculated average options</string>
<string name="pref_view_grade_average_force_calc">Force average calculation by app</string>
<string name="pref_view_present">Show presence</string>
<string name="pref_attendance_target">Attendance target</string>
<string name="pref_attendance_calculator_show_empty_subjects">Show subjects without any attendances</string>
<string name="pref_view_attendance_calculator_sorting_mode">Attendance calculator sorting</string>
<string name="pref_view_app_theme">Theme</string>
<string name="pref_view_expand_grade">Grades expanding</string>
<string name="pref_view_timetable_show_groups">Show groups next to subjects</string>

View File

@ -4,22 +4,18 @@
<style name="BaseWulkanowyTheme" parent="@style/Theme.Material3.Light">
<item name="android:forceDarkAllowed" tools:targetApi="Q">false</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorOnPrimary">@color/colorOnPrimary</item>
<item name="colorOnPrimary">@android:color/white</item>
<item name="colorPrimaryContainer">@color/colorPrimaryContainer</item>
<item name="colorOnPrimaryContainer">@color/colorOnPrimaryContainer</item>
<item name="colorSecondary">@color/colorPrimary</item>
<item name="colorOnSecondary">@color/colorOnPrimary</item>
<item name="colorOnSecondary">@android:color/white</item>
<item name="colorSecondaryContainer">@color/colorPrimaryContainer</item>
<item name="colorOnSecondaryContainer">@color/colorOnPrimaryContainer</item>
<item name="colorTertiary">@color/colorPrimary</item>
<item name="colorOnTertiary">@color/colorOnPrimary</item>
<item name="colorOnTertiary">@android:color/white</item>
<item name="colorTertiaryContainer">@color/colorPrimaryContainer</item>
<item name="colorOnTertiaryContainer">@color/colorOnPrimaryContainer</item>
<item name="colorSurface">@color/colorSurface</item>
<item name="colorTimetableCanceled">@color/timetable_canceled_light</item>
<item name="colorTimetableChange">@color/timetable_change_light</item>

View File

@ -85,30 +85,6 @@
app:iconSpaceReserved="false"
app:key="@string/pref_key_attendance_present"
app:title="@string/pref_view_present" />
<SeekBarPreference
app:defaultValue="@integer/pref_default_attendance_target"
app:iconSpaceReserved="false"
android:layout="@layout/pref_target_attendance"
app:key="@string/pref_key_attendance_target"
app:title="@string/pref_attendance_target"
app:min="1"
app:updatesContinuously="true"
android:max="99"
app:showSeekBarValue="true"
/>
<ListPreference
app:defaultValue="@string/pref_default_attendance_calculator_sorting_mode"
app:entries="@array/attendance_calculator_sorting_mode_entries"
app:entryValues="@array/attendance_calculator_sorting_mode_values"
app:iconSpaceReserved="false"
app:key="@string/pref_key_attendance_calculator_sorting_mode"
app:title="@string/pref_view_attendance_calculator_sorting_mode"
app:useSimpleSummaryProvider="true" />
<SwitchPreferenceCompat
app:defaultValue="@bool/pref_default_attendance_calculator_show_empty_subjects"
app:iconSpaceReserved="false"
app:key="@string/pref_key_attendance_calculator_show_empty_subjects"
app:title="@string/pref_attendance_calculator_show_empty_subjects" />
</PreferenceCategory>
<PreferenceCategory
android:layout_height="wrap_content"

View File

@ -93,8 +93,12 @@ class AdsHelper @Inject constructor(
private fun initializeMobileAds() {
if (isMobileAdsInitializeCalled.getAndSet(true)) return
MobileAds.initialize(context) {
isMobileAdsSdkInitialized.value = true
try {
MobileAds.initialize(context) {
isMobileAdsSdkInitialized.value = true
}
} catch (e: Exception) {
Timber.e(e)
}
}

View File

@ -0,0 +1,42 @@
{
"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"
}

View File

@ -5,8 +5,8 @@ import io.github.wulkanowy.data.db.entities.MailboxType
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.sdk.Sdk
import java.time.LocalDate
import java.time.Instant.now
import java.time.LocalDate
import io.github.wulkanowy.sdk.pojo.Semester as SdkSemester
fun getSemesterEntity(diaryId: Int = 1, semesterId: Int = 1, start: LocalDate = LocalDate.now(), end: LocalDate = LocalDate.now(), semesterName: Int = 1) = Semester(
@ -72,6 +72,8 @@ fun getStudentEntity(mode: Sdk.Mode = Sdk.Mode.HEBE) = Student(
symbol = "",
userLoginId = 1,
userName = "",
isEduOne = false,
isAuthorized = false
).apply {
id = 1
}

View File

@ -2,11 +2,12 @@ package io.github.wulkanowy
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.sdk.Sdk
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
fun createWulkanowySdkFactoryMock(sdk: Sdk) = mockk<WulkanowySdkFactory>()
.apply {
every { create() } returns sdk
every { create(any(), any()) } answers { callOriginal() }
coEvery { create(any(), any()) } returns sdk
}

View File

@ -1,10 +1,6 @@
package io.github.wulkanowy.data
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerifyOrder
import io.mockk.just
import io.mockk.mockk
import io.mockk.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flowOf
@ -46,6 +42,7 @@ class ResourceTest {
// first
networkBoundResource(
isResultEmpty = { false },
showSavedOnLoading = false,
query = { repo.query() },
fetch = {
val data = repo.fetch()
@ -60,6 +57,7 @@ class ResourceTest {
// second
networkBoundResource(
isResultEmpty = { false },
showSavedOnLoading = false,
query = { repo.query() },
fetch = {
val data = repo.fetch()
@ -126,6 +124,7 @@ class ResourceTest {
networkBoundResource(
isResultEmpty = { false },
mutex = saveResultMutex,
showSavedOnLoading = false,
query = { repo.query() },
fetch = {
val data = repo.fetch()
@ -144,6 +143,7 @@ class ResourceTest {
networkBoundResource(
isResultEmpty = { false },
mutex = saveResultMutex,
showSavedOnLoading = false,
query = { repo.query() },
fetch = {
val data = repo.fetch()

View File

@ -0,0 +1,129 @@
package io.github.wulkanowy.data
import android.os.Build
import dagger.hilt.android.testing.HiltTestApplication
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentIsEduOne
import io.github.wulkanowy.getStudentEntity
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.pojo.RegisterStudent
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.O_MR1], application = HiltTestApplication::class)
class WulkanowySdkFactoryTest {
private lateinit var wulkanowySdkFactory: WulkanowySdkFactory
private lateinit var studentDao: StudentDao
private lateinit var sdk: Sdk
@Before
fun setUp() {
sdk = mockk(relaxed = true)
studentDao = mockk()
wulkanowySdkFactory = spyk(
WulkanowySdkFactory(
chuckerInterceptor = mockk(),
remoteConfig = mockk(relaxed = true),
webkitCookieManagerProxy = mockk(),
studentDb = studentDao
)
)
every { wulkanowySdkFactory.create() } returns sdk
}
@Test
fun `check sdk flag isEduOne when local student is eduone`() = runTest {
val student = getStudentEntity().copy(isEduOne = true)
wulkanowySdkFactory.create(student)
verify { sdk.isEduOne = true }
coVerify(exactly = 0) { sdk.getCurrentStudent() }
}
@Test
fun `check sdk flag isEduOne when local student is not eduone`() = runTest {
val student = getStudentEntity().copy(isEduOne = false)
wulkanowySdkFactory.create(student)
verify { sdk.isEduOne = false }
coVerify(exactly = 0) { sdk.getCurrentStudent() }
}
@Test
fun `check sdk flag isEduOne when local student is eduone null and remote student is eduone true`() =
runTest {
val studentToProcess = getStudentEntity().copy(isEduOne = null)
val registerStudent = studentToProcess.toRegisterStudent(isEduOne = true)
coEvery { studentDao.loadById(any()) } returns studentToProcess
coEvery { studentDao.update(any(StudentIsEduOne::class)) } just Runs
coEvery { sdk.getCurrentStudent() } returns registerStudent
wulkanowySdkFactory.create(studentToProcess)
verify { sdk.isEduOne = true }
coVerify { sdk.getCurrentStudent() }
}
@Test
fun `check sdk flag isEduOne when local student is eduone null and remote student is eduone false`() =
runTest {
val studentToProcess = getStudentEntity().copy(isEduOne = null)
val registerStudent = studentToProcess.toRegisterStudent(isEduOne = false)
coEvery { studentDao.loadById(any()) } returns studentToProcess
coEvery { studentDao.update(any(StudentIsEduOne::class)) } just Runs
coEvery { sdk.getCurrentStudent() } returns registerStudent
wulkanowySdkFactory.create(studentToProcess)
verify { sdk.isEduOne = false }
coVerify { sdk.getCurrentStudent() }
}
@Test
fun `check sdk flag isEduOne when sdk getCurrentStudent throws error`() =
runTest {
val studentToProcess = getStudentEntity().copy(isEduOne = null)
coEvery { studentDao.loadById(any()) } returns studentToProcess
coEvery { studentDao.update(any(StudentIsEduOne::class)) } just Runs
coEvery { sdk.getCurrentStudent() } throws Exception()
wulkanowySdkFactory.create(studentToProcess)
verify { sdk.isEduOne = false }
coVerify { sdk.getCurrentStudent() }
}
private fun Student.toRegisterStudent(isEduOne: Boolean) = RegisterStudent(
studentId = studentId,
studentName = studentName,
studentSecondName = studentName,
studentSurname = studentName,
className = className,
classId = classId,
isParent = isParent,
isAuthorized = isAuthorized,
semesters = emptyList(),
isEduOne = isEduOne,
)
}

View File

@ -21,10 +21,10 @@ abstract class AbstractMigrationTest {
@get:Rule
val helper: MigrationTestHelper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java,
listOf(Migration55()),
FrameworkSQLiteOpenHelperFactory()
instrumentation = InstrumentationRegistry.getInstrumentation(),
databaseClass = AppDatabase::class.java,
specs = listOf(Migration63()),
openFactory = FrameworkSQLiteOpenHelperFactory()
)
fun runMigrationsAndValidate(migration: Migration) {

View File

@ -7,8 +7,9 @@ import androidx.sqlite.db.SupportSQLiteDatabase
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.Sdk.ScrapperLoginType.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import io.github.wulkanowy.sdk.Sdk.ScrapperLoginType.ADFSLight
import io.github.wulkanowy.sdk.Sdk.ScrapperLoginType.ADFSLightScoped
import io.github.wulkanowy.sdk.Sdk.ScrapperLoginType.STANDARD
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
@ -19,7 +20,6 @@ import kotlin.test.assertEquals
@HiltAndroidTest
@RunWith(RobolectricTestRunner::class)
@OptIn(ExperimentalCoroutinesApi::class)
@Config(sdk = [Build.VERSION_CODES.O_MR1], application = HiltTestApplication::class)
class Migration54Test : AbstractMigrationTest() {

View File

@ -0,0 +1,89 @@
package io.github.wulkanowy.data.db.migrations
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import android.os.Build
import androidx.sqlite.db.SupportSQLiteDatabase
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.HiltTestApplication
import kotlinx.coroutines.test.runTest
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import kotlin.random.Random
import kotlin.test.Test
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
@HiltAndroidTest
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [Build.VERSION_CODES.O_MR1], application = HiltTestApplication::class)
class Migration63Test : AbstractMigrationTest() {
@Test
fun `update is_edu_one to null if 0`() = runTest {
with(helper.createDatabase(dbName, 62)) {
createStudent(1, 0)
close()
}
helper.runMigrationsAndValidate(dbName, 63, true)
val database = getMigratedRoomDatabase()
val studentDb = database.studentDao
val student = studentDb.loadById(1)
assertNull(student!!.isEduOne)
database.close()
}
@Test
fun `check is_edu_one is stay same`() = runTest {
with(helper.createDatabase(dbName, 62)) {
createStudent(1, 1)
close()
}
helper.runMigrationsAndValidate(dbName, 63, true)
val database = getMigratedRoomDatabase()
val studentDb = database.studentDao
val student = studentDb.loadById(1)
val isEduOne = assertNotNull(student!!.isEduOne)
assertTrue(isEduOne)
database.close()
}
private fun SupportSQLiteDatabase.createStudent(id: Long, isEduOneValue: Int) {
insert("Students", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply {
put("scrapper_base_url", "https://fakelog.cf")
put("mobile_base_url", "")
put("login_type", "SCRAPPER")
put("login_mode", "SCRAPPER")
put("certificate_key", "")
put("private_key", "")
put("is_parent", false)
put("email", "jan@fakelog.cf")
put("password", "******")
put("symbol", "symbol")
put("student_id", Random.nextInt())
put("user_login_id", 123)
put("user_name", "studentName")
put("student_name", "studentName")
put("school_id", "123")
put("school_short", "")
put("school_name", "")
put("class_name", "")
put("class_id", Random.nextInt())
put("is_current", false)
put("registration_date", "0")
put("id", id)
put("nick", "")
put("avatar_color", "")
put("is_edu_one", isEduOneValue)
})
}
}

View File

@ -222,5 +222,7 @@ class GetMailboxByStudentUseCaseTest {
symbol = "",
userLoginId = 1,
userName = userName,
isAuthorized = false,
isEduOne = false
)
}

View File

@ -72,7 +72,9 @@ class GradeAverageProviderTest {
className = "",
classId = 1,
isCurrent = true,
registrationDate = Instant.now()
registrationDate = Instant.now(),
isAuthorized = false,
isEduOne = false
)
private val semesters = mutableListOf(

View File

@ -71,6 +71,8 @@ class LoginStudentSelectPresenterTest {
classId = 0,
isParent = false,
semesters = listOf(),
isEduOne = false,
isAuthorized = false,
)
private val school = RegisterUnit(

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.main
import io.github.wulkanowy.MainCoroutineRule
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.services.sync.SyncManager
@ -7,14 +8,23 @@ import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AdsHelper
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo
import io.mockk.*
import io.mockk.MockKAnnotations
import io.mockk.Runs
import io.mockk.clearMocks
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.just
import io.mockk.verify
import kotlinx.serialization.json.Json
import org.junit.Before
import org.junit.Rule
import org.junit.Test
class MainPresenterTest {
@get:Rule
val coroutineRule = MainCoroutineRule()
@MockK(relaxed = true)
lateinit var errorHandler: ErrorHandler

View File

@ -1,6 +1,6 @@
buildscript {
ext {
kotlin_version = '1.9.23'
kotlin_version = '1.9.22'
about_libraries = '11.1.0'
hilt_version = '2.51'
}
@ -13,7 +13,7 @@ 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.19"
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.dagger:hilt-android-gradle-plugin:$hilt_version"
classpath 'com.google.gms:google-services:4.4.1'

Binary file not shown.

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME