1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2025-02-23 14:14:45 +01:00

Merge branch 'release/1.7.0'

This commit is contained in:
Mikołaj Pich 2022-08-22 17:58:41 +02:00
commit c293c76398
121 changed files with 9063 additions and 843 deletions

View File

@ -1,4 +1,4 @@
name: Deploy to app stores name: Deploy release
on: on:
release: release:
@ -7,16 +7,17 @@ on:
jobs: jobs:
deploy-google-play: deploy-google-play:
name: Deploy to google play name: Google Play
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 10
environment: google-play environment: google-play
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-java@v1 - uses: actions/setup-java@v2
with: with:
distribution: 'zulu'
java-version: 11 java-version: 11
- uses: actions/cache@v2 - uses: actions/cache@v3
with: with:
path: | path: |
~/.gradle/caches ~/.gradle/caches
@ -41,16 +42,17 @@ jobs:
run: ./gradlew publishPlayReleaseApps -PenableFirebase --stacktrace; run: ./gradlew publishPlayReleaseApps -PenableFirebase --stacktrace;
deploy-app-gallery: deploy-app-gallery:
name: Deploy to AppGallery name: AppGallery
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 10
environment: app-gallery environment: app-gallery
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-java@v1 - uses: actions/setup-java@v2
with: with:
distribution: 'zulu'
java-version: 11 java-version: 11
- uses: actions/cache@v2 - uses: actions/cache@v3
with: with:
path: | path: |
~/.gradle/caches ~/.gradle/caches

View File

@ -1,4 +1,4 @@
name: Deploy to app tests name: Deploy DEV
on: on:
push: push:
@ -18,11 +18,12 @@ jobs:
timeout-minutes: 10 timeout-minutes: 10
environment: app-center environment: app-center
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-java@v1 - uses: actions/setup-java@v2
with: with:
distribution: 'zulu'
java-version: 11 java-version: 11
- uses: actions/cache@v2 - uses: actions/cache@v3
with: with:
path: | path: |
~/.gradle/caches ~/.gradle/caches
@ -66,7 +67,7 @@ jobs:
BITRISE_KEY_PASSWORD: ${{ secrets.BITRISE_KEY_PASSWORD }} BITRISE_KEY_PASSWORD: ${{ secrets.BITRISE_KEY_PASSWORD }}
run: ./gradlew assembleFdroidDebug --stacktrace run: ./gradlew assembleFdroidDebug --stacktrace
- name: Upload apk to github artifacts - name: Upload apk to github artifacts
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
with: with:
name: wulkanowyDEV-${{ env.RUN_NUMBER }}.apk name: wulkanowyDEV-${{ env.RUN_NUMBER }}.apk
path: app/build/outputs/apk/fdroid/debug/app-fdroid-debug.apk path: app/build/outputs/apk/fdroid/debug/app-fdroid-debug.apk
@ -87,11 +88,12 @@ jobs:
environment: app-distribution environment: app-distribution
if: github.event_name != 'pull_request_target' if: github.event_name != 'pull_request_target'
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-java@v1 - uses: actions/setup-java@v2
with: with:
distribution: 'zulu'
java-version: 11 java-version: 11
- uses: actions/cache@v2 - uses: actions/cache@v3
with: with:
path: | path: |
~/.gradle/caches ~/.gradle/caches
@ -131,7 +133,7 @@ jobs:
BITRISE_KEY_PASSWORD: ${{ secrets.BITRISE_KEY_PASSWORD }} BITRISE_KEY_PASSWORD: ${{ secrets.BITRISE_KEY_PASSWORD }}
run: ./gradlew assemblePlayDebug -PenableFirebase --stacktrace run: ./gradlew assemblePlayDebug -PenableFirebase --stacktrace
- name: Upload apk to github artifacts - name: Upload apk to github artifacts
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
with: with:
name: wulkanowyDEV-${{ env.RUN_NUMBER }}-dev.apk name: wulkanowyDEV-${{ env.RUN_NUMBER }}-dev.apk
path: app/build/outputs/apk/play/debug/app-play-debug.apk path: app/build/outputs/apk/play/debug/app-play-debug.apk

View File

@ -8,18 +8,20 @@ on:
branches: [ master, develop ] branches: [ master, develop ]
jobs: jobs:
unit-tests:
name: Unit tests tests-fdroid:
name: F-Droid
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: fkirc/skip-duplicate-actions@master - uses: fkirc/skip-duplicate-actions@master
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1 - uses: gradle/wrapper-validation-action@v1
- uses: actions/setup-java@v1 - uses: actions/setup-java@v2
with: with:
distribution: 'zulu'
java-version: 11 java-version: 11
- uses: actions/cache@v2 - uses: actions/cache@v3
with: with:
path: | path: |
~/.gradle/caches ~/.gradle/caches
@ -29,6 +31,58 @@ jobs:
run: | run: |
./gradlew testFdroidDebugUnitTest --stacktrace ./gradlew testFdroidDebugUnitTest --stacktrace
./gradlew jacocoTestReport --stacktrace ./gradlew jacocoTestReport --stacktrace
- uses: codecov/codecov-action@v1 - uses: codecov/codecov-action@v3
with:
flags: unit
tests-play:
name: Play
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: fkirc/skip-duplicate-actions@master
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
- uses: actions/setup-java@v2
with:
distribution: 'zulu'
java-version: 11
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
- name: Unit tests
run: |
./gradlew testPlayDebugUnitTest --stacktrace
./gradlew jacocoTestReport --stacktrace
- uses: codecov/codecov-action@v3
with:
flags: unit
tests-hms:
name: HMS
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: fkirc/skip-duplicate-actions@master
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
- uses: actions/setup-java@v2
with:
distribution: 'zulu'
java-version: 11
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
- name: Unit tests
run: |
./gradlew testHmsDebugUnitTest --stacktrace
./gradlew jacocoTestReport --stacktrace
- uses: codecov/codecov-action@v3
with: with:
flags: unit flags: unit

View File

@ -51,7 +51,7 @@ Die aktuelle Version können Sie von der Google Play, F-Droid oder Huawei AppGal
alt="Explore it on AppGallery" alt="Explore it on AppGallery"
height="80">](https://appgallery.cloud.huawei.com/ag/n/app/C101440411?channelId=Badge&id=1b3f7fbb700849a9be0dba6b520b2282&s=EB1D3BF9ED9D1564D869B7B94B18016D3CABFCA5AEFB8E29F675FA04E0DC131D&detailType=0&v=) height="80">](https://appgallery.cloud.huawei.com/ag/n/app/C101440411?channelId=Badge&id=1b3f7fbb700849a9be0dba6b520b2282&s=EB1D3BF9ED9D1564D869B7B94B18016D3CABFCA5AEFB8E29F675FA04E0DC131D&detailType=0&v=)
Sie können auch ein [Entwicklungsversion herunterladen](https://wulkanowy.github.io/#download) das beinhaltet neue Funktionen, die für die nächste Version vorbereitet werden Sie können auch eine [Entwicklungsversion herunterladen](https://wulkanowy.github.io/#download) die beinhaltet neue Funktionen, die für die nächste Version vorbereitet werden
## Gebaut mit ## Gebaut mit

View File

@ -15,33 +15,35 @@ apply from: 'sonarqube.gradle'
apply from: 'hooks.gradle' apply from: 'hooks.gradle'
android { android {
compileSdkVersion 31 namespace 'io.github.wulkanowy'
compileSdkVersion 32
defaultConfig { defaultConfig {
applicationId "io.github.wulkanowy" applicationId "io.github.wulkanowy"
testApplicationId "io.github.tests.wulkanowy" testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 31 targetSdkVersion 32
versionCode 108 versionCode 109
versionName "1.6.4" versionName "1.7.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy" resValue "string", "app_name", "Wulkanowy"
manifestPlaceholders = [ manifestPlaceholders = [
firebase_enabled: project.hasProperty("enableFirebase"), firebase_enabled: project.hasProperty("enableFirebase"),
admob_project_id: "" admob_project_id: ""
] ]
javaCompileOptions { javaCompileOptions {
annotationProcessorOptions { annotationProcessorOptions {
arguments += [ arguments += [
"room.schemaLocation": "$projectDir/schemas".toString(), "room.schemaLocation": "$projectDir/schemas".toString(),
"room.incremental" : "true" "room.incremental" : "true"
] ]
} }
} }
buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null" buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null"
buildConfigField "String", "DASHBOARD_TILE_AD_ID", "null"
if (System.env.SET_BUILD_TIMESTAMP) { if (System.env.SET_BUILD_TIMESTAMP) {
buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis()) buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis())
@ -94,10 +96,12 @@ android {
play { play {
dimension "platform" dimension "platform"
manifestPlaceholders = [ manifestPlaceholders = [
install_channel : "Google Play", install_channel : "Google Play",
admob_project_id: System.getenv("ADMOB_PROJECT_ID") ?: "ca-app-pub-3940256099942544~3347511713" admob_project_id: System.getenv("ADMOB_PROJECT_ID") ?: "ca-app-pub-3940256099942544~3347511713"
] ]
buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "\"${System.getenv("SINGLE_SUPPORT_AD_ID") ?: "ca-app-pub-3940256099942544/5354046379"}\"" buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "\"${System.getenv("SINGLE_SUPPORT_AD_ID") ?: "ca-app-pub-3940256099942544/5354046379"}\""
buildConfigField "String", "DASHBOARD_TILE_AD_ID", "\"${System.getenv("DASHBOARD_TILE_AD_ID") ?: "ca-app-pub-3940256099942544/6300978111"}\""
} }
fdroid { fdroid {
@ -122,6 +126,8 @@ android {
testOptions.unitTests { testOptions.unitTests {
includeAndroidResources = true includeAndroidResources = true
// workaround HMS test errors https://github.com/robolectric/robolectric/issues/2750
all { jvmArgs '-noverify' }
} }
compileOptions { compileOptions {
@ -136,8 +142,10 @@ android {
} }
packagingOptions { packagingOptions {
exclude 'META-INF/library_release.kotlin_module' resources {
exclude 'META-INF/library-core_release.kotlin_module' excludes += ['META-INF/library_release.kotlin_module',
'META-INF/library-core_release.kotlin_module']
}
} }
aboutLibraries { aboutLibraries {
@ -153,8 +161,8 @@ play {
defaultToAppBundles = false defaultToAppBundles = false
track = 'production' track = 'production'
releaseStatus = com.github.triplet.gradle.androidpublisher.ReleaseStatus.IN_PROGRESS releaseStatus = com.github.triplet.gradle.androidpublisher.ReleaseStatus.IN_PROGRESS
userFraction = 0.50d userFraction = 0.05d
updatePriority = 3 updatePriority = 5
enabled.set(false) enabled.set(false)
} }
@ -171,34 +179,34 @@ huaweiPublish {
ext { ext {
work_manager = "2.7.1" work_manager = "2.7.1"
android_hilt = "1.0.0" android_hilt = "1.0.0"
room = "2.4.2" room = "2.4.3"
chucker = "3.5.2" chucker = "3.5.2"
mockk = "1.12.4" mockk = "1.12.5"
coroutines = "1.6.1" coroutines = "1.6.4"
} }
dependencies { dependencies {
implementation "io.github.wulkanowy:sdk:1.6.4" implementation "io.github.wulkanowy:sdk:1.7.0"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
implementation "androidx.core:core-ktx:1.7.0" implementation "androidx.core:core-ktx:1.8.0"
implementation 'androidx.core:core-splashscreen:1.0.0-beta02' implementation 'androidx.core:core-splashscreen:1.0.0'
implementation "androidx.activity:activity-ktx:1.4.0" implementation "androidx.activity:activity-ktx:1.5.1"
implementation "androidx.appcompat:appcompat:1.4.1" implementation "androidx.appcompat:appcompat:1.5.0"
implementation "androidx.fragment:fragment-ktx:1.4.1" implementation "androidx.fragment:fragment-ktx:1.5.2"
implementation "androidx.annotation:annotation:1.3.0" implementation "androidx.annotation:annotation:1.4.0"
implementation "androidx.preference:preference-ktx:1.2.0" implementation "androidx.preference:preference-ktx:1.2.0"
implementation "androidx.recyclerview:recyclerview:1.2.1" implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation "androidx.viewpager2:viewpager2:1.1.0-beta01" implementation "androidx.viewpager2:viewpager2:1.1.0-beta01"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.3" implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0" implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
implementation "com.google.android.material:material:1.5.0" implementation "com.google.android.material:material:1.6.1"
implementation "com.github.wulkanowy:material-chips-input:2.3.1" implementation "com.github.wulkanowy:material-chips-input:2.3.1"
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0" implementation "com.github.PhilJay:MPAndroidChart:v3.1.0"
implementation 'com.github.lopspower:CircularImageView:4.2.0' implementation 'com.github.lopspower:CircularImageView:4.2.0'
@ -206,7 +214,7 @@ dependencies {
implementation "androidx.work:work-runtime-ktx:$work_manager" implementation "androidx.work:work-runtime-ktx:$work_manager"
playImplementation "androidx.work:work-gcm:$work_manager" playImplementation "androidx.work:work-gcm:$work_manager"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.1" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.5.1"
implementation "androidx.room:room-runtime:$room" implementation "androidx.room:room-runtime:$room"
implementation "androidx.room:room-ktx:$room" implementation "androidx.room:room-ktx:$room"
@ -222,27 +230,27 @@ dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0" implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0"
implementation "com.squareup.okhttp3:logging-interceptor:4.9.3" implementation "com.squareup.okhttp3:logging-interceptor:4.10.0"
implementation "com.jakewharton.timber:timber:5.0.1" implementation "com.jakewharton.timber:timber:5.0.1"
implementation "at.favre.lib:slf4j-timber:1.0.1" implementation "at.favre.lib:slf4j-timber:1.0.1"
implementation 'com.github.bastienpaulfr:Treessence:1.0.5' implementation 'com.github.bastienpaulfr:Treessence:1.0.5'
implementation "com.mikepenz:aboutlibraries-core:$about_libraries" implementation "com.mikepenz:aboutlibraries-core:$about_libraries"
implementation "io.coil-kt:coil:2.0.0" implementation "io.coil-kt:coil:2.2.0"
implementation "io.github.wulkanowy:AppKillerManager:3.0.0" implementation "io.github.wulkanowy:AppKillerManager:3.0.0"
implementation 'me.xdrop:fuzzywuzzy:1.4.0' implementation 'me.xdrop:fuzzywuzzy:1.4.0'
implementation 'com.fredporciuncula:flow-preferences:1.7.0' implementation 'com.fredporciuncula:flow-preferences:1.8.0'
playImplementation platform('com.google.firebase:firebase-bom:30.0.1') playImplementation platform('com.google.firebase:firebase-bom:30.3.2')
playImplementation 'com.google.firebase:firebase-analytics-ktx' playImplementation 'com.google.firebase:firebase-analytics-ktx'
playImplementation 'com.google.firebase:firebase-messaging:' playImplementation 'com.google.firebase:firebase-messaging:'
playImplementation 'com.google.firebase:firebase-crashlytics:' playImplementation 'com.google.firebase:firebase-crashlytics:'
playImplementation 'com.google.android.play:core:1.10.3' playImplementation 'com.google.android.play:core:1.10.3'
playImplementation 'com.google.android.play:core-ktx:1.8.1' playImplementation 'com.google.android.play:core-ktx:1.8.1'
playImplementation 'com.google.android.gms:play-services-ads:20.6.0' playImplementation 'com.google.android.gms:play-services-ads:21.1.0'
hmsImplementation 'com.huawei.hms:hianalytics:6.5.0.300' hmsImplementation 'com.huawei.hms:hianalytics:6.7.0.300'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.6.200' hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.7.1.300'
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker" releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,92 @@
{ {
"agcgw":{ "agcgw": {
"backurl":"connect-dre.dbankcloud.cn", "backurl": "connect-dre.hispace.hicloud.com",
"url":"connect-dre.hispace.hicloud.com" "url": "connect-dre.dbankcloud.cn",
}, "websocketbackurl": "connect-ws-dre.hispace.dbankcloud.com",
"client":{ "websocketurl": "connect-ws-dre.hispace.dbankcloud.cn"
"cp_id":"890048000024105546", },
"product_id":"", "agcgw_all": {
"client_id":"", "CN": "connect-drcn.dbankcloud.cn",
"client_secret":"", "CN_back": "connect-drcn.hispace.hicloud.com",
"app_id":"101440411", "DE": "connect-dre.dbankcloud.cn",
"package_name":"io.github.wulkanowy.dev", "DE_back": "connect-dre.hispace.hicloud.com",
"api_key":"" "RU": "connect-drru.hispace.dbankcloud.ru",
}, "RU_back": "connect-drru.hispace.dbankcloud.cn",
"service":{ "SG": "connect-dra.dbankcloud.cn",
"analytics":{ "SG_back": "connect-dra.hispace.hicloud.com"
"collector_url":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn", },
"resource_id":"p1", "websocketgw_all": {
"channel_id":"" "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":{ "search":{
"url":"https://search-dre.cloud.huawei.com" "url":"https://search-dre.cloud.huawei.com"
}, },
"cloudstorage":{ "cloudstorage": {
"storage_url":"https://ops-dre.agcstorage.link" "storage_url_sg_back": "https://agc-storage-dra.cloud.huawei.asia",
}, "storage_url_ru_back": "https://agc-storage-drru.cloud.huawei.ru",
"ml":{ "storage_url_ru": "https://agc-storage-drru.cloud.huawei.ru",
"mlservice_url":"ml-api-dre.ai.dbankcloud.com,ml-api-dre.ai.dbankcloud.cn" "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",
"region":"DE", "storage_url_sg": "https://ops-dra.agcstorage.link",
"configuration_version":"1.0" "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

@ -0,0 +1,28 @@
package io.github.wulkanowy.utils
import android.content.Context
import android.view.View
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import javax.inject.Inject
@Suppress("unused")
class AdsHelper @Inject constructor(
@ApplicationContext private val context: Context,
private val preferencesRepository: PreferencesRepository
) {
fun initialize() {
preferencesRepository.isAdsEnabled = false
preferencesRepository.isAgreeToProcessData = false
preferencesRepository.selectedDashboardTiles -= DashboardItem.Tile.ADS
}
@Suppress("RedundantSuspendModifier", "UNUSED_PARAMETER")
suspend fun getDashboardTileAdBanner(width: Int): AdBanner {
throw IllegalStateException("Can't get ad banner (F-droid)")
}
}
data class AdBanner(val view: View)

View File

@ -0,0 +1,28 @@
package io.github.wulkanowy.utils
import android.content.Context
import android.view.View
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import javax.inject.Inject
@Suppress("unused")
class AdsHelper @Inject constructor(
@ApplicationContext private val context: Context,
private val preferencesRepository: PreferencesRepository
) {
fun initialize() {
preferencesRepository.isAdsEnabled = false
preferencesRepository.isAgreeToProcessData = false
preferencesRepository.selectedDashboardTiles -= DashboardItem.Tile.ADS
}
@Suppress("RedundantSuspendModifier", "UNUSED_PARAMETER")
suspend fun getDashboardTileAdBanner(width: Int): AdBanner {
throw IllegalStateException("Can't get ad banner (HMS)")
}
}
data class AdBanner(val view: View)

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="io.github.wulkanowy"
android:installLocation="internalOnly"> android:installLocation="internalOnly">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />

View File

@ -31,10 +31,14 @@ class WulkanowyApp : Application(), Configuration.Provider {
@Inject @Inject
lateinit var analyticsHelper: AnalyticsHelper lateinit var analyticsHelper: AnalyticsHelper
@Inject
lateinit var adsHelper: AdsHelper
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
initializeAppLanguage() initializeAppLanguage()
themeManager.applyDefaultTheme() themeManager.applyDefaultTheme()
adsHelper.initialize()
initLogging() initLogging()
} }

View File

@ -19,7 +19,6 @@ import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
@ -110,7 +109,6 @@ internal class DataModule {
fun provideSharedPref(@ApplicationContext context: Context): SharedPreferences = fun provideSharedPref(@ApplicationContext context: Context): SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context) PreferenceManager.getDefaultSharedPreferences(context)
@OptIn(ExperimentalCoroutinesApi::class)
@Singleton @Singleton
@Provides @Provides
fun provideFlowSharedPref(sharedPreferences: SharedPreferences) = fun provideFlowSharedPref(sharedPreferences: SharedPreferences) =
@ -197,7 +195,7 @@ internal class DataModule {
@Singleton @Singleton
@Provides @Provides
fun provideReportingUnitDao(database: AppDatabase) = database.reportingUnitDao fun provideMailboxesDao(database: AppDatabase) = database.mailboxDao
@Singleton @Singleton
@Provides @Provides

View File

@ -30,7 +30,7 @@ import javax.inject.Singleton
Subject::class, Subject::class,
LuckyNumber::class, LuckyNumber::class,
CompletedLesson::class, CompletedLesson::class,
ReportingUnit::class, Mailbox::class,
Recipient::class, Recipient::class,
MobileDevice::class, MobileDevice::class,
Teacher::class, Teacher::class,
@ -55,7 +55,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
companion object { companion object {
const val VERSION_SCHEMA = 48 const val VERSION_SCHEMA = 51
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(), Migration2(),
@ -102,6 +102,9 @@ abstract class AppDatabase : RoomDatabase() {
Migration43(), Migration43(),
Migration44(), Migration44(),
Migration46(), Migration46(),
Migration49(),
Migration50(),
Migration51(),
) )
fun newInstance( fun newInstance(
@ -152,7 +155,7 @@ abstract class AppDatabase : RoomDatabase() {
abstract val completedLessonsDao: CompletedLessonsDao abstract val completedLessonsDao: CompletedLessonsDao
abstract val reportingUnitDao: ReportingUnitDao abstract val mailboxDao: MailboxDao
abstract val recipientDao: RecipientDao abstract val recipientDao: RecipientDao

View File

@ -0,0 +1,17 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.Mailbox
import javax.inject.Singleton
@Singleton
@Dao
interface MailboxDao : BaseDao<Mailbox> {
@Query("SELECT * FROM Mailboxes WHERE userLoginId = :userLoginId ")
suspend fun loadAll(userLoginId: Int): List<Mailbox>
@Query("SELECT * FROM Mailboxes WHERE userLoginId = :userLoginId AND studentName = :studentName ")
suspend fun load(userLoginId: Int, studentName: String): Mailbox?
}

View File

@ -11,9 +11,9 @@ import kotlinx.coroutines.flow.Flow
interface MessagesDao : BaseDao<Message> { interface MessagesDao : BaseDao<Message> {
@Transaction @Transaction
@Query("SELECT * FROM Messages WHERE student_id = :studentId AND message_id = :messageId") @Query("SELECT * FROM Messages WHERE message_global_key = :messageGlobalKey")
fun loadMessageWithAttachment(studentId: Int, messageId: Int): Flow<MessageWithAttachment?> fun loadMessageWithAttachment(messageGlobalKey: String): Flow<MessageWithAttachment?>
@Query("SELECT * FROM Messages WHERE student_id = :studentId AND folder_id = :folder ORDER BY date DESC") @Query("SELECT * FROM Messages WHERE mailbox_key = :mailboxKey AND folder_id = :folder ORDER BY date DESC")
fun loadAll(studentId: Int, folder: Int): Flow<List<Message>> fun loadAll(mailboxKey: String, folder: Int): Flow<List<Message>>
} }

View File

@ -8,6 +8,6 @@ import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface MobileDeviceDao : BaseDao<MobileDevice> { interface MobileDeviceDao : BaseDao<MobileDevice> {
@Query("SELECT * FROM MobileDevices WHERE student_id = :userLoginId ORDER BY date DESC") @Query("SELECT * FROM MobileDevices WHERE user_login_id = :userLoginId ORDER BY date DESC")
fun loadAll(userLoginId: Int): Flow<List<MobileDevice>> fun loadAll(userLoginId: Int): Flow<List<MobileDevice>>
} }

View File

@ -2,6 +2,7 @@ package io.github.wulkanowy.data.db.dao
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Query import androidx.room.Query
import io.github.wulkanowy.data.db.entities.MailboxType
import io.github.wulkanowy.data.db.entities.Recipient import io.github.wulkanowy.data.db.entities.Recipient
import javax.inject.Singleton import javax.inject.Singleton
@ -9,6 +10,6 @@ import javax.inject.Singleton
@Dao @Dao
interface RecipientDao : BaseDao<Recipient> { interface RecipientDao : BaseDao<Recipient> {
@Query("SELECT * FROM Recipients WHERE student_id = :studentId AND unit_id = :unitId AND role = :role") @Query("SELECT * FROM Recipients WHERE type = :type AND studentMailboxGlobalKey = :studentMailboxGlobalKey")
suspend fun loadAll(studentId: Int, unitId: Int, role: Int): List<Recipient> suspend fun loadAll(type: MailboxType, studentMailboxGlobalKey: String): List<Recipient>
} }

View File

@ -1,17 +0,0 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.ReportingUnit
import javax.inject.Singleton
@Singleton
@Dao
interface ReportingUnitDao : BaseDao<ReportingUnit> {
@Query("SELECT * FROM ReportingUnits WHERE student_id = :studentId")
suspend fun load(studentId: Int): List<ReportingUnit>
@Query("SELECT * FROM ReportingUnits WHERE student_id = :studentId AND real_id = :unitId")
suspend fun loadOne(studentId: Int, unitId: Int): ReportingUnit?
}

View File

@ -10,6 +10,6 @@ import javax.inject.Singleton
@Singleton @Singleton
interface SchoolAnnouncementDao : BaseDao<SchoolAnnouncement> { interface SchoolAnnouncementDao : BaseDao<SchoolAnnouncement> {
@Query("SELECT * FROM SchoolAnnouncements WHERE student_id = :studentId ORDER BY date DESC") @Query("SELECT * FROM SchoolAnnouncements WHERE user_login_id = :userLoginId ORDER BY date DESC")
fun loadAll(studentId: Int): Flow<List<SchoolAnnouncement>> fun loadAll(userLoginId: Int): Flow<List<SchoolAnnouncement>>
} }

View File

@ -0,0 +1,25 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "Mailboxes")
data class Mailbox(
@PrimaryKey
val globalKey: String,
val fullName: String,
val userName: String,
val userLoginId: Int,
val studentName: String,
val schoolNameShort: String,
val type: MailboxType,
)
enum class MailboxType {
STUDENT,
PARENT,
GUARDIAN,
EMPLOYEE,
UNKNOWN,
}

View File

@ -9,23 +9,16 @@ import java.time.Instant
@Entity(tableName = "Messages") @Entity(tableName = "Messages")
data class Message( data class Message(
@ColumnInfo(name = "student_id") @ColumnInfo(name = "message_global_key")
val studentId: Long, val messageGlobalKey: String,
@ColumnInfo(name = "real_id") @ColumnInfo(name = "mailbox_key")
val realId: Int, val mailboxKey: String,
@ColumnInfo(name = "message_id") @ColumnInfo(name = "message_id")
val messageId: Int, val messageId: Int,
@ColumnInfo(name = "sender_name") val correspondents: String,
val sender: String,
@ColumnInfo(name = "sender_id")
val senderId: Int,
@ColumnInfo(name = "recipient_name")
val recipient: String,
val subject: String, val subject: String,
@ -36,8 +29,6 @@ data class Message(
var unread: Boolean, var unread: Boolean,
val removed: Boolean,
@ColumnInfo(name = "has_attachments") @ColumnInfo(name = "has_attachments")
val hasAttachments: Boolean val hasAttachments: Boolean
) : Serializable { ) : Serializable {
@ -48,11 +39,7 @@ data class Message(
@ColumnInfo(name = "is_notified") @ColumnInfo(name = "is_notified")
var isNotified: Boolean = true var isNotified: Boolean = true
@ColumnInfo(name = "unread_by")
var unreadBy: Int = 0
@ColumnInfo(name = "read_by")
var readBy: Int = 0
var content: String = "" var content: String = ""
var sender: String? = null
var recipients: String? = null
} }

View File

@ -12,11 +12,8 @@ data class MessageAttachment(
@ColumnInfo(name = "real_id") @ColumnInfo(name = "real_id")
val realId: Int, val realId: Int,
@ColumnInfo(name = "message_id") @ColumnInfo(name = "message_global_key")
val messageId: Int, val messageGlobalKey: String,
@ColumnInfo(name = "one_drive_id")
val oneDriveId: String,
@ColumnInfo(name = "url") @ColumnInfo(name = "url")
val url: String, val url: String,

View File

@ -7,6 +7,6 @@ data class MessageWithAttachment(
@Embedded @Embedded
val message: Message, val message: Message,
@Relation(parentColumn = "message_id", entityColumn = "message_id") @Relation(parentColumn = "message_global_key", entityColumn = "message_global_key")
val attachments: List<MessageAttachment> val attachments: List<MessageAttachment>
) )

View File

@ -9,7 +9,7 @@ import java.time.Instant
@Entity(tableName = "MobileDevices") @Entity(tableName = "MobileDevices")
data class MobileDevice( data class MobileDevice(
@ColumnInfo(name = "student_id") @ColumnInfo(name = "user_login_id")
val userLoginId: Int, val userLoginId: Int,
@ColumnInfo(name = "device_id") @ColumnInfo(name = "device_id")

View File

@ -1,6 +1,5 @@
package io.github.wulkanowy.data.db.entities package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import java.io.Serializable import java.io.Serializable
@ -8,32 +7,16 @@ import java.io.Serializable
@kotlinx.serialization.Serializable @kotlinx.serialization.Serializable
@Entity(tableName = "Recipients") @Entity(tableName = "Recipients")
data class Recipient( data class Recipient(
val mailboxGlobalKey: String,
@ColumnInfo(name = "student_id") val studentMailboxGlobalKey: String,
val studentId: Int, val fullName: String,
val userName: String,
@ColumnInfo(name = "real_id") val schoolShortName: String,
val realId: String, val type: MailboxType,
val name: String,
@ColumnInfo(name = "real_name")
val realName: String,
@ColumnInfo(name = "login_id")
val loginId: Int,
@ColumnInfo(name = "unit_id")
val unitId: Int,
val role: Int,
val hash: String
) : Serializable { ) : Serializable {
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
var id: Long = 0 var id: Long = 0
override fun toString() = name override fun toString() = userName
} }

View File

@ -1,32 +0,0 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable
@Entity(tableName = "ReportingUnits")
data class ReportingUnit(
@ColumnInfo(name = "student_id")
val studentId: Int,
@ColumnInfo(name = "real_id")
val unitId: Int,
@ColumnInfo(name = "short")
val shortName: String,
@ColumnInfo(name = "sender_id")
val senderId: Int,
@ColumnInfo(name = "sender_name")
val senderName: String,
val roles: List<Int>
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

View File

@ -9,8 +9,8 @@ import java.time.LocalDate
@Entity(tableName = "SchoolAnnouncements") @Entity(tableName = "SchoolAnnouncements")
data class SchoolAnnouncement( data class SchoolAnnouncement(
@ColumnInfo(name = "student_id") @ColumnInfo(name = "user_login_id")
val studentId: Int, val userLoginId: Int,
val date: LocalDate, val date: LocalDate,

View File

@ -0,0 +1,23 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration49 : Migration(48, 49) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE IF EXISTS SchoolAnnouncements")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `SchoolAnnouncements` (
`user_login_id` INTEGER NOT NULL,
`date` INTEGER NOT NULL,
`subject` TEXT NOT NULL,
`content` TEXT NOT NULL,
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`is_notified` INTEGER NOT NULL)
""".trimIndent()
)
}
}

View File

@ -0,0 +1,21 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration50 : Migration(49, 50) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE IF EXISTS MobileDevices")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `MobileDevices` (
`user_login_id` INTEGER NOT NULL,
`device_id` INTEGER NOT NULL,
`name` TEXT NOT NULL,
`date` INTEGER NOT NULL,
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)
""".trimIndent()
)
}
}

View File

@ -0,0 +1,88 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration51 : Migration(50, 51) {
override fun migrate(database: SupportSQLiteDatabase) {
createMailboxTable(database)
recreateMessagesTable(database)
recreateMessageAttachmentsTable(database)
recreateRecipientsTable(database)
deleteReportingUnitTable(database)
}
private fun createMailboxTable(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE IF EXISTS Mailboxes")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `Mailboxes` (
`globalKey` TEXT NOT NULL,
`fullName` TEXT NOT NULL,
`userName` TEXT NOT NULL,
`userLoginId` INTEGER NOT NULL,
`studentName` TEXT NOT NULL,
`schoolNameShort` TEXT NOT NULL,
`type` TEXT NOT NULL,
PRIMARY KEY(`globalKey`)
)""".trimIndent()
)
}
private fun recreateMessagesTable(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE IF EXISTS Messages")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `Messages` (
`message_global_key` TEXT NOT NULL,
`mailbox_key` TEXT NOT NULL,
`message_id` INTEGER NOT NULL,
`correspondents` TEXT NOT NULL,
`subject` TEXT NOT NULL,
`date` INTEGER NOT NULL,
`folder_id` INTEGER NOT NULL,
`unread` INTEGER NOT NULL,
`has_attachments` INTEGER NOT NULL,
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
`is_notified` INTEGER NOT NULL,
`content` TEXT NOT NULL,
`sender` TEXT, `recipients` TEXT
)""".trimIndent()
)
}
private fun recreateMessageAttachmentsTable(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE IF EXISTS MessageAttachments")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `MessageAttachments` (
`real_id` INTEGER NOT NULL,
`message_global_key` TEXT NOT NULL,
`url` TEXT NOT NULL,
`filename` TEXT NOT NULL,
PRIMARY KEY(`real_id`)
)""".trimIndent()
)
}
private fun recreateRecipientsTable(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE IF EXISTS Recipients")
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS `Recipients` (
`mailboxGlobalKey` TEXT NOT NULL,
`studentMailboxGlobalKey` TEXT NOT NULL,
`fullName` TEXT NOT NULL,
`userName` TEXT NOT NULL,
`schoolShortName` TEXT NOT NULL,
`type` TEXT NOT NULL,
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
)""".trimIndent()
)
}
private fun deleteReportingUnitTable(database: SupportSQLiteDatabase) {
database.execSQL("DROP TABLE IF EXISTS ReportingUnits")
}
}

View File

@ -2,9 +2,10 @@ package io.github.wulkanowy.data.enums
enum class GradeSortingMode(val value: String) { enum class GradeSortingMode(val value: String) {
ALPHABETIC("alphabetic"), ALPHABETIC("alphabetic"),
DATE("date"); DATE("date"),
AVERAGE("average");
companion object { companion object {
fun getByValue(value: String) = values().find { it.value == value } ?: ALPHABETIC fun getByValue(value: String) = values().find { it.value == value } ?: ALPHABETIC
} }
} }

View File

@ -6,7 +6,7 @@ import io.github.wulkanowy.sdk.pojo.DirectorInformation as SdkDirectorInformatio
fun List<SdkDirectorInformation>.mapToEntities(student: Student) = map { fun List<SdkDirectorInformation>.mapToEntities(student: Student) = map {
SchoolAnnouncement( SchoolAnnouncement(
studentId = student.userLoginId, userLoginId = student.userLoginId,
date = it.date, date = it.date,
subject = it.subject, subject = it.subject,
content = it.content, content = it.content,

View File

@ -0,0 +1,18 @@
package io.github.wulkanowy.data.mappers
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.MailboxType
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.sdk.pojo.Mailbox as SdkMailbox
fun List<SdkMailbox>.mapToEntities(student: Student) = map {
Mailbox(
globalKey = it.globalKey,
fullName = it.fullName,
userName = it.userName,
userLoginId = student.userLoginId,
studentName = it.studentName,
schoolNameShort = it.schoolNameShort,
type = MailboxType.valueOf(it.type.name),
)
}

View File

@ -1,40 +1,31 @@
package io.github.wulkanowy.data.mappers package io.github.wulkanowy.data.mappers
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.*
import io.github.wulkanowy.data.db.entities.MessageAttachment import io.github.wulkanowy.sdk.pojo.MailboxType
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.Student
import java.time.Instant
import io.github.wulkanowy.sdk.pojo.Message as SdkMessage import io.github.wulkanowy.sdk.pojo.Message as SdkMessage
import io.github.wulkanowy.sdk.pojo.MessageAttachment as SdkMessageAttachment import io.github.wulkanowy.sdk.pojo.MessageAttachment as SdkMessageAttachment
import io.github.wulkanowy.sdk.pojo.Recipient as SdkRecipient import io.github.wulkanowy.sdk.pojo.Recipient as SdkRecipient
fun List<SdkMessage>.mapToEntities(student: Student) = map { fun List<SdkMessage>.mapToEntities(mailbox: Mailbox) = map {
Message( Message(
studentId = student.id, messageGlobalKey = it.globalKey,
realId = it.id ?: 0, mailboxKey = mailbox.globalKey,
messageId = it.messageId ?: 0, messageId = it.id,
sender = it.sender?.name.orEmpty(), correspondents = it.correspondents,
senderId = it.sender?.loginId ?: 0,
recipient = it.recipients.singleOrNull()?.name ?: "Wielu adresatów",
subject = it.subject.trim(), subject = it.subject.trim(),
date = it.dateZoned?.toInstant() ?: Instant.now(), date = it.dateZoned.toInstant(),
folderId = it.folderId, folderId = it.folderId,
unread = it.unread ?: false, unread = it.unread,
removed = it.removed,
hasAttachments = it.hasAttachments hasAttachments = it.hasAttachments
).apply { ).apply {
content = it.content.orEmpty() content = it.content.orEmpty()
unreadBy = it.unreadBy ?: 0
readBy = it.readBy ?: 0
} }
} }
fun List<SdkMessageAttachment>.mapToEntities() = map { fun List<SdkMessageAttachment>.mapToEntities(messageGlobalKey: String) = map {
MessageAttachment( MessageAttachment(
realId = it.id, messageGlobalKey = messageGlobalKey,
messageId = it.messageId, realId = it.url.hashCode(),
oneDriveId = it.oneDriveId,
url = it.url, url = it.url,
filename = it.filename filename = it.filename
) )
@ -42,12 +33,11 @@ fun List<SdkMessageAttachment>.mapToEntities() = map {
fun List<Recipient>.mapFromEntities() = map { fun List<Recipient>.mapFromEntities() = map {
SdkRecipient( SdkRecipient(
id = it.realId, fullName = it.fullName,
name = it.realName, userName = it.userName,
loginId = it.loginId, studentName = it.userName,
reportingUnitId = it.unitId, mailboxGlobalKey = it.mailboxGlobalKey,
role = it.role, schoolNameShort = it.schoolShortName,
hash = it.hash, type = MailboxType.valueOf(it.type.name),
shortName = it.name
) )
} }

View File

@ -1,14 +1,14 @@
package io.github.wulkanowy.data.mappers package io.github.wulkanowy.data.mappers
import io.github.wulkanowy.data.db.entities.MobileDevice import io.github.wulkanowy.data.db.entities.MobileDevice
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.pojos.MobileDeviceToken import io.github.wulkanowy.data.pojos.MobileDeviceToken
import io.github.wulkanowy.sdk.pojo.Device as SdkDevice import io.github.wulkanowy.sdk.pojo.Device as SdkDevice
import io.github.wulkanowy.sdk.pojo.Token as SdkToken import io.github.wulkanowy.sdk.pojo.Token as SdkToken
fun List<SdkDevice>.mapToEntities(semester: Semester) = map { fun List<SdkDevice>.mapToEntities(student: Student) = map {
MobileDevice( MobileDevice(
userLoginId = semester.studentId, userLoginId = student.userLoginId,
date = it.createDateZoned.toInstant(), date = it.createDateZoned.toInstant(),
deviceId = it.id, deviceId = it.id,
name = it.name name = it.name

View File

@ -1,17 +1,16 @@
package io.github.wulkanowy.data.mappers package io.github.wulkanowy.data.mappers
import io.github.wulkanowy.data.db.entities.MailboxType
import io.github.wulkanowy.data.db.entities.Recipient import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.sdk.pojo.Recipient as SdkRecipient import io.github.wulkanowy.sdk.pojo.Recipient as SdkRecipient
fun List<SdkRecipient>.mapToEntities(userLoginId: Int) = map { fun List<SdkRecipient>.mapToEntities(studentMailboxGlobalKey: String) = map {
Recipient( Recipient(
studentId = userLoginId, mailboxGlobalKey = it.mailboxGlobalKey,
realId = it.id, fullName = it.fullName,
realName = it.name, userName = it.userName,
name = it.shortName, studentMailboxGlobalKey = studentMailboxGlobalKey,
hash = it.hash, schoolShortName = it.schoolNameShort,
loginId = it.loginId, type = MailboxType.valueOf(it.type.name),
role = it.role,
unitId = it.reportingUnitId ?: 0
) )
} }

View File

@ -1,16 +0,0 @@
package io.github.wulkanowy.data.mappers
import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.sdk.pojo.ReportingUnit as SdkReportingUnit
fun List<SdkReportingUnit>.mapToEntities(student: Student) = map {
ReportingUnit(
studentId = student.id.toInt(),
unitId = it.id,
roles = it.roles,
senderId = it.senderId,
senderName = it.senderName,
shortName = it.short
)
}

View File

@ -0,0 +1,48 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.db.dao.MailboxDao
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.uniqueSubtract
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class MailboxRepository @Inject constructor(
private val mailboxDao: MailboxDao,
private val sdk: Sdk,
private val refreshHelper: AutoRefreshHelper,
) {
private val cacheKey = "mailboxes"
suspend fun refreshMailboxes(student: Student) {
val new = sdk.init(student).getMailboxes().mapToEntities(student)
val old = mailboxDao.loadAll(student.userLoginId)
mailboxDao.deleteAll(old uniqueSubtract new)
mailboxDao.insertAll(new uniqueSubtract old)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
}
suspend fun getMailbox(student: Student): Mailbox {
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
val mailbox = mailboxDao.load(student.userLoginId, student.studentName)
return if (isExpired || mailbox == null) {
refreshMailboxes(student)
val newMailbox = mailboxDao.load(student.userLoginId, student.studentName)
requireNotNull(newMailbox) {
"Mailbox for ${student.userName} - ${student.studentName} not found!"
}
newMailbox
} else mailbox
}
}

View File

@ -10,24 +10,24 @@ import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.entities.* import io.github.wulkanowy.data.db.entities.*
import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED
import io.github.wulkanowy.data.enums.MessageFolder.TRASHED
import io.github.wulkanowy.data.mappers.mapFromEntities import io.github.wulkanowy.data.mappers.mapFromEntities
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.data.pojos.MessageDraft import io.github.wulkanowy.data.pojos.MessageDraft
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.pojo.Folder import io.github.wulkanowy.sdk.pojo.Folder
import io.github.wulkanowy.sdk.pojo.SentMessage
import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.init import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.uniqueSubtract import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import timber.log.Timber import timber.log.Timber
import java.time.LocalDateTime.now
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -49,7 +49,7 @@ class MessageRepository @Inject constructor(
@Suppress("UNUSED_PARAMETER") @Suppress("UNUSED_PARAMETER")
fun getMessages( fun getMessages(
student: Student, student: Student,
semester: Semester, mailbox: Mailbox,
folder: MessageFolder, folder: MessageFolder,
forceRefresh: Boolean, forceRefresh: Boolean,
notify: Boolean = false, notify: Boolean = false,
@ -62,42 +62,20 @@ class MessageRepository @Inject constructor(
) )
it.isEmpty() || forceRefresh || isExpired it.isEmpty() || forceRefresh || isExpired
}, },
query = { messagesDb.loadAll(student.id.toInt(), folder.id) }, query = { messagesDb.loadAll(mailbox.globalKey, folder.id) },
fetch = { fetch = {
sdk.init(student).getMessages(Folder.valueOf(folder.name), now().minusMonths(3), now()) sdk.init(student).getMessages(Folder.valueOf(folder.name)).mapToEntities(mailbox)
.mapToEntities(student)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
messagesDb.deleteAll(old uniqueSubtract new) messagesDb.deleteAll(old uniqueSubtract new)
messagesDb.insertAll((new uniqueSubtract old).onEach { messagesDb.insertAll((new uniqueSubtract old).onEach {
it.isNotified = !notify it.isNotified = !notify
}) })
messagesDb.updateAll(getMessagesWithReadByChange(old, new, !notify))
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student, folder)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student, folder))
} }
) )
private fun getMessagesWithReadByChange(
old: List<Message>,
new: List<Message>,
setNotified: Boolean
): List<Message> {
val oldMeta = old.map { Triple(it, it.readBy, it.unreadBy) }
val newMeta = new.map { Triple(it, it.readBy, it.unreadBy) }
val updatedItems = newMeta uniqueSubtract oldMeta
return updatedItems.map {
val oldItem = old.find { item -> item.messageId == it.first.messageId }
it.first.apply {
id = oldItem?.id ?: 0
isNotified = oldItem?.isNotified ?: setNotified
content = oldItem?.content.orEmpty()
}
}
}
fun getMessage( fun getMessage(
student: Student, student: Student,
message: Message, message: Message,
@ -106,34 +84,34 @@ class MessageRepository @Inject constructor(
isResultEmpty = { it?.message?.content.isNullOrBlank() }, isResultEmpty = { it?.message?.content.isNullOrBlank() },
shouldFetch = { shouldFetch = {
checkNotNull(it) { "This message no longer exist!" } checkNotNull(it) { "This message no longer exist!" }
Timber.d("Message content in db empty: ${it.message.content.isEmpty()}") Timber.d("Message content in db empty: ${it.message.content.isBlank()}")
it.message.unread || it.message.content.isEmpty() it.message.unread || it.message.content.isBlank()
}, },
query = { messagesDb.loadMessageWithAttachment(student.id.toInt(), message.messageId) }, query = { messagesDb.loadMessageWithAttachment(message.messageGlobalKey) },
fetch = { fetch = {
sdk.init(student).getMessageDetails( sdk.init(student).getMessageDetails(it!!.message.messageGlobalKey)
messageId = it!!.message.messageId,
folderId = message.folderId,
read = markAsRead,
id = message.realId
).let { details ->
details.content to details.attachments.mapToEntities()
}
}, },
saveFetchResult = { old, (downloadedMessage, attachments) -> saveFetchResult = { old, new ->
checkNotNull(old) { "Fetched message no longer exist!" } checkNotNull(old) { "Fetched message no longer exist!" }
messagesDb.updateAll(listOf(old.message.apply { messagesDb.updateAll(
id = old.message.id listOf(old.message.apply {
unread = !markAsRead id = message.id
content = content.ifBlank { downloadedMessage } unread = !markAsRead
})) sender = new.sender
messageAttachmentDao.insertAttachments(attachments) recipients = new.recipients.firstOrNull() ?: "Wielu adresoatów"
content = content.ifBlank { new.content }
})
)
messageAttachmentDao.insertAttachments(
items = new.attachments.mapToEntities(message.messageGlobalKey),
)
Timber.d("Message ${message.messageId} with blank content: ${old.message.content.isBlank()}, marked as read") Timber.d("Message ${message.messageId} with blank content: ${old.message.content.isBlank()}, marked as read")
} }
) )
fun getMessagesFromDatabase(student: Student): Flow<List<Message>> { fun getMessagesFromDatabase(mailbox: Mailbox): Flow<List<Message>> {
return messagesDb.loadAll(student.id.toInt(), RECEIVED.id) return messagesDb.loadAll(mailbox.globalKey, RECEIVED.id)
} }
suspend fun updateMessages(messages: List<Message>) { suspend fun updateMessages(messages: List<Message>) {
@ -145,32 +123,48 @@ class MessageRepository @Inject constructor(
subject: String, subject: String,
content: String, content: String,
recipients: List<Recipient>, recipients: List<Recipient>,
): SentMessage = sdk.init(student).sendMessage( mailboxId: String,
subject = subject, ) {
content = content, sdk.init(student).sendMessage(
recipients = recipients.mapFromEntities() subject = subject,
) content = content,
recipients = recipients.mapFromEntities(),
mailboxId = mailboxId,
)
}
suspend fun deleteMessages(student: Student, messages: List<Message>) { suspend fun deleteMessages(student: Student, mailbox: Mailbox, messages: List<Message>) {
val folderId = messages.first().folderId val firstMessage = messages.first()
val isDeleted = sdk.init(student) sdk.init(student).deleteMessages(
.deleteMessages(messages = messages.map { it.messageId }, folderId = folderId) messages = messages.map { it.messageGlobalKey },
removeForever = firstMessage.folderId == TRASHED.id,
)
if (folderId != MessageFolder.TRASHED.id && isDeleted) { if (firstMessage.folderId != TRASHED.id) {
val deletedMessages = messages.map { val deletedMessages = messages.map {
it.copy(folderId = MessageFolder.TRASHED.id) it.copy(folderId = TRASHED.id)
.apply { .apply {
id = it.id id = it.id
content = it.content content = it.content
sender = it.sender
recipients = it.recipients
} }
} }
messagesDb.updateAll(deletedMessages) messagesDb.updateAll(deletedMessages)
} else messagesDb.deleteAll(messages) } else messagesDb.deleteAll(messages)
getMessages(
student = student,
mailbox = mailbox,
folder = TRASHED,
forceRefresh = true,
).first()
} }
suspend fun deleteMessage(student: Student, message: Message) = suspend fun deleteMessage(student: Student, mailbox: Mailbox, message: Message) {
deleteMessages(student, listOf(message)) deleteMessages(student, mailbox, listOf(message))
}
var draftMessage: MessageDraft? var draftMessage: MessageDraft?
get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_send_draft)) get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_send_draft))

View File

@ -39,12 +39,12 @@ class MobileDeviceRepository @Inject constructor(
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student)) val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
it.isEmpty() || forceRefresh || isExpired it.isEmpty() || forceRefresh || isExpired
}, },
query = { mobileDb.loadAll(student.userLoginId.takeIf { it != 0 } ?: student.studentId) }, query = { mobileDb.loadAll(student.userLoginId) },
fetch = { fetch = {
sdk.init(student) sdk.init(student)
.switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear) .switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear)
.getRegisteredDevices() .getRegisteredDevices()
.mapToEntities(semester) .mapToEntities(student)
}, },
saveFetchResult = { old, new -> saveFetchResult = { old, new ->
mobileDb.deleteAll(old uniqueSubtract new) mobileDb.deleteAll(old uniqueSubtract new)

View File

@ -222,19 +222,31 @@ class PreferencesRepository @Inject constructor(
get() = selectedDashboardTilesPreference.asFlow() get() = selectedDashboardTilesPreference.asFlow()
.map { set -> .map { set ->
set.map { DashboardItem.Tile.valueOf(it) } set.map { DashboardItem.Tile.valueOf(it) }
.plus(DashboardItem.Tile.ACCOUNT) .plus(
.plus(DashboardItem.Tile.ADMIN_MESSAGE) listOfNotNull(
DashboardItem.Tile.ACCOUNT,
DashboardItem.Tile.ADMIN_MESSAGE,
DashboardItem.Tile.ADS.takeIf { isAdsEnabled }
)
)
.toSet() .toSet()
} }
var selectedDashboardTiles: Set<DashboardItem.Tile> var selectedDashboardTiles: Set<DashboardItem.Tile>
get() = selectedDashboardTilesPreference.get() get() = selectedDashboardTilesPreference.get()
.map { DashboardItem.Tile.valueOf(it) } .map { DashboardItem.Tile.valueOf(it) }
.plus(DashboardItem.Tile.ACCOUNT) .plus(
.plus(DashboardItem.Tile.ADMIN_MESSAGE) listOfNotNull(
DashboardItem.Tile.ACCOUNT,
DashboardItem.Tile.ADMIN_MESSAGE,
DashboardItem.Tile.ADS.takeIf { isAdsEnabled }
)
)
.toSet() .toSet()
set(value) { set(value) {
val filteredValue = value.filterNot { it == DashboardItem.Tile.ACCOUNT } val filteredValue = value.filterNot {
it == DashboardItem.Tile.ACCOUNT || it == DashboardItem.Tile.ADMIN_MESSAGE
}
.map { it.name } .map { it.name }
.toSet() .toSet()
@ -271,7 +283,38 @@ class PreferencesRepository @Inject constructor(
var isAppReviewDone: Boolean var isAppReviewDone: Boolean
get() = sharedPref.getBoolean(PREF_KEY_IN_APP_REVIEW_DONE, false) get() = sharedPref.getBoolean(PREF_KEY_IN_APP_REVIEW_DONE, false)
set(value) = sharedPref.edit().putBoolean(PREF_KEY_IN_APP_REVIEW_DONE, value).apply() set(value) = sharedPref.edit { putBoolean(PREF_KEY_IN_APP_REVIEW_DONE, value) }
var isAppSupportShown: Boolean
get() = sharedPref.getBoolean(PREF_KEY_APP_SUPPORT_SHOWN, false)
set(value) = sharedPref.edit { putBoolean(PREF_KEY_APP_SUPPORT_SHOWN, value) }
var isAgreeToProcessData: Boolean
get() = getBoolean(
R.string.pref_key_ads_consent_data_processing,
R.bool.pref_default_ads_consent_data_processing
)
set(value) = sharedPref.edit {
putBoolean(context.getString(R.string.pref_key_ads_consent_data_processing), value)
}
var isPersonalizedAdsEnabled: Boolean
get() = sharedPref.getBoolean(PREF_KEY_PERSONALIZED_ADS_ENABLED, false)
set(value) = sharedPref.edit { putBoolean(PREF_KEY_PERSONALIZED_ADS_ENABLED, value) }
val isAdsEnabledFlow = flowSharedPref.getBoolean(
context.getString(R.string.pref_key_ads_enabled),
context.resources.getBoolean(R.bool.pref_default_ads_enabled)
).asFlow()
var isAdsEnabled: Boolean
get() = getBoolean(
R.string.pref_key_ads_enabled,
R.bool.pref_default_ads_enabled
)
set(value) = sharedPref.edit {
putBoolean(context.getString(R.string.pref_key_ads_enabled), value)
}
private fun getLong(id: Int, default: Int) = getLong(context.getString(id), default) private fun getLong(id: Int, default: Int) = getLong(context.getString(id), default)
@ -301,6 +344,10 @@ class PreferencesRepository @Inject constructor(
private const val PREF_KEY_IN_APP_REVIEW_DONE = "in_app_review_done" private const val PREF_KEY_IN_APP_REVIEW_DONE = "in_app_review_done"
private const val PREF_KEY_APP_SUPPORT_SHOWN = "app_support_shown"
private const val PREF_KEY_PERSONALIZED_ADS_ENABLED = "personalized_ads_enabled"
private const val PREF_KEY_ADMIN_DISMISSED_MESSAGE_IDS = "admin_message_dismissed_ids" private const val PREF_KEY_ADMIN_DISMISSED_MESSAGE_IDS = "admin_message_dismissed_ids"
} }
} }

View File

@ -1,10 +1,7 @@
package io.github.wulkanowy.data.repositories package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.db.dao.RecipientDao import io.github.wulkanowy.data.db.dao.RecipientDao
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.*
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper import io.github.wulkanowy.utils.AutoRefreshHelper
@ -23,9 +20,10 @@ class RecipientRepository @Inject constructor(
private val cacheKey = "recipient" private val cacheKey = "recipient"
suspend fun refreshRecipients(student: Student, unit: ReportingUnit, role: Int) { suspend fun refreshRecipients(student: Student, mailbox: Mailbox, type: MailboxType) {
val new = sdk.init(student).getRecipients(unit.unitId, role).mapToEntities(unit.studentId) val new = sdk.init(student).getRecipients(mailbox.globalKey)
val old = recipientDb.loadAll(unit.studentId, unit.unitId, role) .mapToEntities(mailbox.globalKey)
val old = recipientDb.loadAll(type, mailbox.globalKey)
recipientDb.deleteAll(old uniqueSubtract new) recipientDb.deleteAll(old uniqueSubtract new)
recipientDb.insertAll(new uniqueSubtract old) recipientDb.insertAll(new uniqueSubtract old)
@ -33,18 +31,27 @@ class RecipientRepository @Inject constructor(
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student)) refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
} }
suspend fun getRecipients(student: Student, unit: ReportingUnit, role: Int): List<Recipient> { suspend fun getRecipients(
val cached = recipientDb.loadAll(unit.studentId, unit.unitId, role) student: Student,
mailbox: Mailbox,
type: MailboxType
): List<Recipient> {
val cached = recipientDb.loadAll(type, mailbox.globalKey)
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student)) val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
return if (cached.isEmpty() || isExpired) { return if (cached.isEmpty() || isExpired) {
refreshRecipients(student, unit, role) refreshRecipients(student, mailbox, type)
recipientDb.loadAll(unit.studentId, unit.unitId, role) recipientDb.loadAll(type, mailbox.globalKey)
} else cached } else cached
} }
suspend fun getMessageRecipients(student: Student, message: Message): List<Recipient> { suspend fun getMessageSender(
return sdk.init(student).getMessageRecipients(message.messageId, message.senderId) student: Student,
.mapToEntities(student.studentId) mailbox: Mailbox,
} message: Message
): List<Recipient> = sdk.init(student)
.getMessageReplayDetails(message.messageGlobalKey)
.sender
.let(::listOf)
.mapToEntities(mailbox.globalKey)
} }

View File

@ -1,42 +0,0 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.db.dao.ReportingUnitDao
import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.uniqueSubtract
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ReportingUnitRepository @Inject constructor(
private val reportingUnitDb: ReportingUnitDao,
private val sdk: Sdk
) {
suspend fun refreshReportingUnits(student: Student) {
val new = sdk.init(student).getReportingUnits().mapToEntities(student)
val old = reportingUnitDb.load(student.id.toInt())
reportingUnitDb.deleteAll(old.uniqueSubtract(new))
reportingUnitDb.insertAll(new.uniqueSubtract(old))
}
suspend fun getReportingUnits(student: Student): List<ReportingUnit> {
return reportingUnitDb.load(student.id.toInt()).ifEmpty {
refreshReportingUnits(student)
reportingUnitDb.load(student.id.toInt())
}
}
suspend fun getReportingUnit(student: Student, unitId: Int): ReportingUnit? {
return reportingUnitDb.loadOne(student.id.toInt(), unitId) ?: run {
refreshReportingUnits(student)
return reportingUnitDb.loadOne(student.id.toInt(), unitId)
}
}
}

View File

@ -28,7 +28,8 @@ class SchoolAnnouncementRepository @Inject constructor(
fun getSchoolAnnouncements( fun getSchoolAnnouncements(
student: Student, student: Student,
forceRefresh: Boolean, notify: Boolean = false forceRefresh: Boolean,
notify: Boolean = false
) = networkBoundResource( ) = networkBoundResource(
mutex = saveFetchResultMutex, mutex = saveFetchResultMutex,
isResultEmpty = { it.isEmpty() }, isResultEmpty = { it.isEmpty() },
@ -37,7 +38,7 @@ class SchoolAnnouncementRepository @Inject constructor(
it.isEmpty() || forceRefresh || isExpired it.isEmpty() || forceRefresh || isExpired
}, },
query = { query = {
schoolAnnouncementDb.loadAll(student.studentId) schoolAnnouncementDb.loadAll(student.userLoginId)
}, },
fetch = { fetch = {
sdk.init(student) sdk.init(student)
@ -56,7 +57,7 @@ class SchoolAnnouncementRepository @Inject constructor(
) )
fun getSchoolAnnouncementFromDatabase(student: Student): Flow<List<SchoolAnnouncement>> { fun getSchoolAnnouncementFromDatabase(student: Student): Flow<List<SchoolAnnouncement>> {
return schoolAnnouncementDb.loadAll(student.studentId) return schoolAnnouncementDb.loadAll(student.userLoginId)
} }
suspend fun updateSchoolAnnouncement(schoolAnnouncement: List<SchoolAnnouncement>) = suspend fun updateSchoolAnnouncement(schoolAnnouncement: List<SchoolAnnouncement>) =

View File

@ -21,7 +21,7 @@ class NewMessageNotification @Inject constructor(
val notificationDataList = items.map { val notificationDataList = items.map {
NotificationData( NotificationData(
title = context.getPlural(R.plurals.message_new_items, 1), title = context.getPlural(R.plurals.message_new_items, 1),
content = "${it.sender}: ${it.subject}", content = "${it.correspondents}: ${it.subject}",
destination = Destination.Message, destination = Destination.Message,
) )
} }

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.services.sync.works
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED
import io.github.wulkanowy.data.repositories.MailboxRepository
import io.github.wulkanowy.data.repositories.MessageRepository import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.data.waitForResult
import io.github.wulkanowy.services.sync.notifications.NewMessageNotification import io.github.wulkanowy.services.sync.notifications.NewMessageNotification
@ -11,19 +12,21 @@ import javax.inject.Inject
class MessageWork @Inject constructor( class MessageWork @Inject constructor(
private val messageRepository: MessageRepository, private val messageRepository: MessageRepository,
private val mailboxRepository: MailboxRepository,
private val newMessageNotification: NewMessageNotification, private val newMessageNotification: NewMessageNotification,
) : Work { ) : Work {
override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) {
val mailbox = mailboxRepository.getMailbox(student)
messageRepository.getMessages( messageRepository.getMessages(
student = student, student = student,
semester = semester, mailbox = mailbox,
folder = RECEIVED, folder = RECEIVED,
forceRefresh = true, forceRefresh = true,
notify = notify notify = notify
).waitForResult() ).waitForResult()
messageRepository.getMessagesFromDatabase(student).first() messageRepository.getMessagesFromDatabase(mailbox).first()
.filter { !it.isNotified && it.unread }.let { .filter { !it.isNotified && it.unread }.let {
if (it.isNotEmpty()) newMessageNotification.notify(it, student) if (it.isNotEmpty()) newMessageNotification.notify(it, student)
messageRepository.updateMessages(it.onEach { message -> message.isNotified = true }) messageRepository.updateMessages(it.onEach { message -> message.isNotified = true })

View File

@ -1,23 +1,22 @@
package io.github.wulkanowy.services.sync.works package io.github.wulkanowy.services.sync.works
import io.github.wulkanowy.data.db.entities.MailboxType
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.MailboxRepository
import io.github.wulkanowy.data.repositories.RecipientRepository import io.github.wulkanowy.data.repositories.RecipientRepository
import io.github.wulkanowy.data.repositories.ReportingUnitRepository
import javax.inject.Inject import javax.inject.Inject
class RecipientWork @Inject constructor( class RecipientWork @Inject constructor(
private val reportingUnitRepository: ReportingUnitRepository, private val mailboxRepository: MailboxRepository,
private val recipientRepository: RecipientRepository private val recipientRepository: RecipientRepository
) : Work { ) : Work {
override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) { override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) {
reportingUnitRepository.refreshReportingUnits(student) mailboxRepository.refreshMailboxes(student)
reportingUnitRepository.getReportingUnits(student).let { units -> val mailbox = mailboxRepository.getMailbox(student)
units.map {
recipientRepository.refreshRecipients(student, it, 2) recipientRepository.refreshRecipients(student, mailbox, MailboxType.EMPLOYEE)
}
}
} }
} }

View File

@ -6,6 +6,7 @@ import io.github.wulkanowy.data.repositories.SchoolAnnouncementRepository
import io.github.wulkanowy.data.waitForResult import io.github.wulkanowy.data.waitForResult
import io.github.wulkanowy.services.sync.notifications.NewSchoolAnnouncementNotification import io.github.wulkanowy.services.sync.notifications.NewSchoolAnnouncementNotification
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
class SchoolAnnouncementWork @Inject constructor( class SchoolAnnouncementWork @Inject constructor(
@ -20,10 +21,13 @@ class SchoolAnnouncementWork @Inject constructor(
notify = notify, notify = notify,
).waitForResult() ).waitForResult()
schoolAnnouncementRepository.getSchoolAnnouncementFromDatabase(student)
schoolAnnouncementRepository.getSchoolAnnouncementFromDatabase(student).first() .first()
.filter { !it.isNotified }.let { .filter { !it.isNotified && it.date >= LocalDate.now() }
if (it.isNotEmpty()) newSchoolAnnouncementNotification.notify(it, student) .let {
if (it.isNotEmpty()) {
newSchoolAnnouncementNotification.notify(it, student)
}
schoolAnnouncementRepository.updateSchoolAnnouncement(it.onEach { schoolAnnouncement -> schoolAnnouncementRepository.updateSchoolAnnouncement(it.onEach { schoolAnnouncement ->
schoolAnnouncement.isNotified = true schoolAnnouncement.isNotified = true

View File

@ -91,15 +91,19 @@ class AttendancePresenter @Inject constructor(
fun onViewReselected() { fun onViewReselected() {
Timber.i("Attendance view is reselected") Timber.i("Attendance view is reselected")
view?.also { view -> view?.let { view ->
if (view.currentStackSize == 1) { if (view.currentStackSize == 1) {
baseDate.also { baseDate = now().previousOrSameSchoolDay
if (currentDate != it) {
reloadView(it) if (currentDate != baseDate) {
loadData() reloadView(baseDate)
} else if (!view.isViewEmpty) view.resetView() loadData()
} else if (!view.isViewEmpty) {
view.resetView()
} }
} else view.popView() } else {
view.popView()
}
} }
} }

View File

@ -18,6 +18,7 @@ import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsFragment import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsFragment
import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment import io.github.wulkanowy.ui.modules.attendance.summary.AttendanceSummaryFragment
import io.github.wulkanowy.ui.modules.conference.ConferenceFragment import io.github.wulkanowy.ui.modules.conference.ConferenceFragment
import io.github.wulkanowy.ui.modules.dashboard.adapters.DashboardAdapter
import io.github.wulkanowy.ui.modules.exam.ExamFragment import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.homework.HomeworkFragment import io.github.wulkanowy.ui.modules.homework.HomeworkFragment
@ -47,6 +48,14 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
override var subtitleString = override var subtitleString =
LocalDate.now().toFormattedString("EEEE, d MMMM yyyy").capitalise() LocalDate.now().toFormattedString("EEEE, d MMMM yyyy").capitalise()
override val tileWidth: Int
get() {
val recyclerWidth = binding.dashboardRecycler.width
val margin = requireContext().dpToPx(24f).toInt()
return ((recyclerWidth - margin) / resources.displayMetrics.density).toInt()
}
companion object { companion object {
fun newInstance() = DashboardFragment() fun newInstance() = DashboardFragment()

View File

@ -1,13 +1,9 @@
package io.github.wulkanowy.ui.modules.dashboard package io.github.wulkanowy.ui.modules.dashboard
import io.github.wulkanowy.data.db.entities.AdminMessage import io.github.wulkanowy.data.db.entities.*
import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.GradeColorTheme import io.github.wulkanowy.data.enums.GradeColorTheme
import io.github.wulkanowy.data.pojos.TimetableFull import io.github.wulkanowy.data.pojos.TimetableFull
import io.github.wulkanowy.utils.AdBanner
import io.github.wulkanowy.data.db.entities.Homework as EntitiesHomework import io.github.wulkanowy.data.db.entities.Homework as EntitiesHomework
sealed class DashboardItem(val type: Type) { sealed class DashboardItem(val type: Type) {
@ -106,17 +102,26 @@ sealed class DashboardItem(val type: Type) {
override val isDataLoaded get() = conferences != null override val isDataLoaded get() = conferences != null
} }
data class Ads(
val adBanner: AdBanner? = null,
override val error: Throwable? = null,
override val isLoading: Boolean = false
) : DashboardItem(Type.ADS) {
override val isDataLoaded get() = adBanner != null
}
enum class Type { enum class Type {
ADMIN_MESSAGE, ADMIN_MESSAGE,
ACCOUNT, ACCOUNT,
HORIZONTAL_GROUP, HORIZONTAL_GROUP,
LESSONS, LESSONS,
ADS,
GRADES, GRADES,
HOMEWORK, HOMEWORK,
ANNOUNCEMENTS, ANNOUNCEMENTS,
EXAMS, EXAMS,
CONFERENCES, CONFERENCES,
ADS
} }
enum class Tile { enum class Tile {
@ -126,12 +131,12 @@ sealed class DashboardItem(val type: Type) {
MESSAGES, MESSAGES,
ATTENDANCE, ATTENDANCE,
LESSONS, LESSONS,
ADS,
GRADES, GRADES,
HOMEWORK, HOMEWORK,
ANNOUNCEMENTS, ANNOUNCEMENTS,
EXAMS, EXAMS,
CONFERENCES, CONFERENCES,
ADS
} }
} }
@ -148,4 +153,4 @@ fun DashboardItem.Tile.toDashboardItemType() = when (this) {
DashboardItem.Tile.EXAMS -> DashboardItem.Type.EXAMS DashboardItem.Tile.EXAMS -> DashboardItem.Type.EXAMS
DashboardItem.Tile.CONFERENCES -> DashboardItem.Type.CONFERENCES DashboardItem.Tile.CONFERENCES -> DashboardItem.Type.CONFERENCES
DashboardItem.Tile.ADS -> DashboardItem.Type.ADS DashboardItem.Tile.ADS -> DashboardItem.Type.ADS
} }

View File

@ -2,7 +2,8 @@ package io.github.wulkanowy.ui.modules.dashboard
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import java.util.Collections import io.github.wulkanowy.ui.modules.dashboard.adapters.DashboardAdapter
import java.util.*
class DashboardItemMoveCallback( class DashboardItemMoveCallback(
private val dashboardAdapter: DashboardAdapter, private val dashboardAdapter: DashboardAdapter,

View File

@ -8,6 +8,7 @@ import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.repositories.* import io.github.wulkanowy.data.repositories.*
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AdsHelper
import io.github.wulkanowy.utils.calculatePercentage import io.github.wulkanowy.utils.calculatePercentage
import io.github.wulkanowy.utils.nextOrSameSchoolDay import io.github.wulkanowy.utils.nextOrSameSchoolDay
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
@ -24,6 +25,7 @@ class DashboardPresenter @Inject constructor(
private val gradeRepository: GradeRepository, private val gradeRepository: GradeRepository,
private val semesterRepository: SemesterRepository, private val semesterRepository: SemesterRepository,
private val messageRepository: MessageRepository, private val messageRepository: MessageRepository,
private val mailboxRepository: MailboxRepository,
private val attendanceSummaryRepository: AttendanceSummaryRepository, private val attendanceSummaryRepository: AttendanceSummaryRepository,
private val timetableRepository: TimetableRepository, private val timetableRepository: TimetableRepository,
private val homeworkRepository: HomeworkRepository, private val homeworkRepository: HomeworkRepository,
@ -31,7 +33,8 @@ class DashboardPresenter @Inject constructor(
private val conferenceRepository: ConferenceRepository, private val conferenceRepository: ConferenceRepository,
private val preferencesRepository: PreferencesRepository, private val preferencesRepository: PreferencesRepository,
private val schoolAnnouncementRepository: SchoolAnnouncementRepository, private val schoolAnnouncementRepository: SchoolAnnouncementRepository,
private val adminMessageRepository: AdminMessageRepository private val adminMessageRepository: AdminMessageRepository,
private val adsHelper: AdsHelper
) : BasePresenter<DashboardView>(errorHandler, studentRepository) { ) : BasePresenter<DashboardView>(errorHandler, studentRepository) {
private val dashboardItemLoadedList = mutableListOf<DashboardItem>() private val dashboardItemLoadedList = mutableListOf<DashboardItem>()
@ -55,7 +58,11 @@ class DashboardPresenter @Inject constructor(
showContent(false) showContent(false)
} }
preferencesRepository.selectedDashboardTilesFlow merge(
preferencesRepository.selectedDashboardTilesFlow,
preferencesRepository.isAdsEnabledFlow
.map { preferencesRepository.selectedDashboardTiles }
)
.onEach { loadData(tilesToLoad = it) } .onEach { loadData(tilesToLoad = it) }
.launch("dashboard_pref") .launch("dashboard_pref")
} }
@ -166,7 +173,7 @@ class DashboardPresenter @Inject constructor(
DashboardItem.Type.CONFERENCES -> { DashboardItem.Type.CONFERENCES -> {
loadConferences(student, forceRefresh) loadConferences(student, forceRefresh)
} }
DashboardItem.Type.ADS -> TODO() DashboardItem.Type.ADS -> loadAds(forceRefresh)
DashboardItem.Type.ADMIN_MESSAGE -> loadAdminMessage(student, forceRefresh) DashboardItem.Type.ADMIN_MESSAGE -> loadAdminMessage(student, forceRefresh)
} }
} }
@ -221,6 +228,7 @@ class DashboardPresenter @Inject constructor(
private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) { private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) {
flow { flow {
val semester = semesterRepository.getCurrentSemester(student) val semester = semesterRepository.getCurrentSemester(student)
val mailbox = mailboxRepository.getMailbox(student)
val selectedTiles = preferencesRepository.selectedDashboardTiles val selectedTiles = preferencesRepository.selectedDashboardTiles
val flowSuccess = flowOf(Resource.Success(null)) val flowSuccess = flowOf(Resource.Success(null))
@ -232,7 +240,7 @@ class DashboardPresenter @Inject constructor(
val messageFLow = messageRepository.getMessages( val messageFLow = messageRepository.getMessages(
student = student, student = student,
semester = semester, mailbox = mailbox,
folder = MessageFolder.RECEIVED, folder = MessageFolder.RECEIVED,
forceRefresh = forceRefresh forceRefresh = forceRefresh
).takeIf { DashboardItem.Tile.MESSAGES in selectedTiles } ?: flowSuccess ).takeIf { DashboardItem.Tile.MESSAGES in selectedTiles } ?: flowSuccess
@ -595,6 +603,23 @@ class DashboardPresenter @Inject constructor(
.launchWithUniqueRefreshJob("dashboard_admin_messages", forceRefresh) .launchWithUniqueRefreshJob("dashboard_admin_messages", forceRefresh)
} }
private fun loadAds(forceRefresh: Boolean) {
presenterScope.launch {
if (!forceRefresh) {
updateData(DashboardItem.Ads(), forceRefresh)
}
val dashboardAdItem =
runCatching {
DashboardItem.Ads(adsHelper.getDashboardTileAdBanner(view!!.tileWidth))
}
.onFailure { Timber.e(it) }
.getOrElse { DashboardItem.Ads(error = it) }
updateData(dashboardAdItem, forceRefresh)
}
}
private fun updateData(dashboardItem: DashboardItem, forceRefresh: Boolean) { private fun updateData(dashboardItem: DashboardItem, forceRefresh: Boolean) {
val isForceRefreshError = forceRefresh && dashboardItem.error != null val isForceRefreshError = forceRefresh && dashboardItem.error != null
val isFirstRunDataLoadedError = val isFirstRunDataLoadedError =
@ -619,6 +644,18 @@ class DashboardPresenter @Inject constructor(
} }
} }
if (dashboardItem is DashboardItem.Ads) {
if (!dashboardItem.isDataLoaded) {
dashboardItemsToLoad = dashboardItemsToLoad - DashboardItem.Type.ADS
dashboardTileLoadedList = dashboardTileLoadedList - DashboardItem.Tile.ADS
dashboardItemLoadedList.removeAll { it.type == DashboardItem.Type.ADS }
} else {
dashboardItemsToLoad = dashboardItemsToLoad + DashboardItem.Type.ADS
dashboardTileLoadedList = dashboardTileLoadedList + DashboardItem.Tile.ADS
}
}
if (forceRefresh) { if (forceRefresh) {
updateForceRefreshData(dashboardItem) updateForceRefreshData(dashboardItem)
} else { } else {

View File

@ -4,6 +4,8 @@ import io.github.wulkanowy.ui.base.BaseView
interface DashboardView : BaseView { interface DashboardView : BaseView {
val tileWidth: Int
fun initView() fun initView()
fun updateData(data: List<DashboardItem>) fun updateData(data: List<DashboardItem>)
@ -27,4 +29,4 @@ interface DashboardView : BaseView {
fun openNotificationsCenterView() fun openNotificationsCenterView()
fun openInternetBrowser(url: String) fun openInternetBrowser(url: String)
} }

View File

@ -1,4 +1,4 @@
package io.github.wulkanowy.ui.modules.dashboard package io.github.wulkanowy.ui.modules.dashboard.adapters
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.res.ColorStateList import android.content.res.ColorStateList
@ -22,24 +22,15 @@ import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.db.entities.TimetableHeader import io.github.wulkanowy.data.db.entities.TimetableHeader
import io.github.wulkanowy.data.enums.GradeColorTheme import io.github.wulkanowy.data.enums.GradeColorTheme
import io.github.wulkanowy.databinding.ItemDashboardAccountBinding import io.github.wulkanowy.databinding.*
import io.github.wulkanowy.databinding.ItemDashboardAdminMessageBinding import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.databinding.ItemDashboardAnnouncementsBinding import io.github.wulkanowy.utils.*
import io.github.wulkanowy.databinding.ItemDashboardConferencesBinding
import io.github.wulkanowy.databinding.ItemDashboardExamsBinding
import io.github.wulkanowy.databinding.ItemDashboardGradesBinding
import io.github.wulkanowy.databinding.ItemDashboardHomeworkBinding
import io.github.wulkanowy.databinding.ItemDashboardHorizontalGroupBinding
import io.github.wulkanowy.databinding.ItemDashboardLessonsBinding
import io.github.wulkanowy.utils.createNameInitialsDrawable
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.left
import io.github.wulkanowy.utils.nickOrName
import io.github.wulkanowy.utils.toFormattedString
import timber.log.Timber import timber.log.Timber
import java.time.* import java.time.Duration
import java.util.Timer import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.util.*
import javax.inject.Inject import javax.inject.Inject
import kotlin.concurrent.timer import kotlin.concurrent.timer
@ -120,6 +111,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
DashboardItem.Type.ADMIN_MESSAGE.ordinal -> AdminMessageViewHolder( DashboardItem.Type.ADMIN_MESSAGE.ordinal -> AdminMessageViewHolder(
ItemDashboardAdminMessageBinding.inflate(inflater, parent, false) ItemDashboardAdminMessageBinding.inflate(inflater, parent, false)
) )
DashboardItem.Type.ADS.ordinal -> AdsViewHolder(
ItemDashboardAdsBinding.inflate(inflater, parent, false)
)
else -> throw IllegalArgumentException() else -> throw IllegalArgumentException()
} }
} }
@ -135,6 +129,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
is ExamsViewHolder -> bindExamsViewHolder(holder, position) is ExamsViewHolder -> bindExamsViewHolder(holder, position)
is ConferencesViewHolder -> bindConferencesViewHolder(holder, position) is ConferencesViewHolder -> bindConferencesViewHolder(holder, position)
is AdminMessageViewHolder -> bindAdminMessage(holder, position) is AdminMessageViewHolder -> bindAdminMessage(holder, position)
is AdsViewHolder -> bindAdsViewHolder(holder, position)
} }
} }
@ -746,6 +741,20 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
} }
} }
private fun bindAdsViewHolder(adsViewHolder: AdsViewHolder, position: Int) {
val item = (items[position] as DashboardItem.Ads).adBanner ?: return
val binding = adsViewHolder.binding
binding.dashboardAdminMessageItemContent.removeAllViews()
binding.dashboardAdminMessageItemContent.addView(
item.view,
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
)
}
class AccountViewHolder(val binding: ItemDashboardAccountBinding) : class AccountViewHolder(val binding: ItemDashboardAccountBinding) :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)
@ -788,6 +797,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
class AdminMessageViewHolder(val binding: ItemDashboardAdminMessageBinding) : class AdminMessageViewHolder(val binding: ItemDashboardAdminMessageBinding) :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)
class AdsViewHolder(val binding: ItemDashboardAdsBinding) :
RecyclerView.ViewHolder(binding.root)
private class DiffCallback( private class DiffCallback(
private val newList: List<DashboardItem>, private val newList: List<DashboardItem>,
private val oldList: List<DashboardItem> private val oldList: List<DashboardItem>

View File

@ -1,4 +1,4 @@
package io.github.wulkanowy.ui.modules.dashboard package io.github.wulkanowy.ui.modules.dashboard.adapters
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
@ -33,4 +33,4 @@ class DashboardAnnouncementsAdapter :
class ViewHolder(val binding: SubitemDashboardAnnouncementsBinding) : class ViewHolder(val binding: SubitemDashboardAnnouncementsBinding) :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)
} }

View File

@ -1,4 +1,4 @@
package io.github.wulkanowy.ui.modules.dashboard package io.github.wulkanowy.ui.modules.dashboard.adapters
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
@ -33,4 +33,4 @@ class DashboardConferencesAdapter :
class ViewHolder(val binding: SubitemDashboardConferencesBinding) : class ViewHolder(val binding: SubitemDashboardConferencesBinding) :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)
} }

View File

@ -1,4 +1,4 @@
package io.github.wulkanowy.ui.modules.dashboard package io.github.wulkanowy.ui.modules.dashboard.adapters
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.view.LayoutInflater import android.view.LayoutInflater
@ -56,4 +56,4 @@ class DashboardExamsAdapter :
class ViewHolder(val binding: SubitemDashboardExamsBinding) : class ViewHolder(val binding: SubitemDashboardExamsBinding) :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)
} }

View File

@ -1,4 +1,4 @@
package io.github.wulkanowy.ui.modules.dashboard package io.github.wulkanowy.ui.modules.dashboard.adapters
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup

View File

@ -1,4 +1,4 @@
package io.github.wulkanowy.ui.modules.dashboard package io.github.wulkanowy.ui.modules.dashboard.adapters
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.view.LayoutInflater import android.view.LayoutInflater
@ -53,4 +53,4 @@ class DashboardHomeworkAdapter : RecyclerView.Adapter<DashboardHomeworkAdapter.V
class ViewHolder(val binding: SubitemDashboardHomeworkBinding) : class ViewHolder(val binding: SubitemDashboardHomeworkBinding) :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)
} }

View File

@ -17,16 +17,13 @@ val debugMessageItems = listOf(
) )
private fun generateMessage(sender: String, subject: String) = Message( private fun generateMessage(sender: String, subject: String) = Message(
sender = sender,
subject = subject, subject = subject,
studentId = 0, messageId = 123,
realId = 0,
messageId = 0,
senderId = 0,
recipient = "",
date = Instant.now(), date = Instant.now(),
folderId = 0, folderId = 0,
unread = true, unread = true,
removed = false, hasAttachments = false,
hasAttachments = false messageGlobalKey = "",
correspondents = sender,
mailboxKey = "",
) )

View File

@ -19,6 +19,6 @@ val debugSchoolAnnouncementItems = listOf(
private fun generateAnnouncement(subject: String, content: String) = SchoolAnnouncement( private fun generateAnnouncement(subject: String, content: String) = SchoolAnnouncement(
subject = subject, subject = subject,
content = content, content = content,
studentId = 0, userLoginId = 0,
date = LocalDate.now() date = LocalDate.now()
) )

View File

@ -3,8 +3,8 @@ package io.github.wulkanowy.ui.modules.grade.details
import io.github.wulkanowy.data.* import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Grade import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.enums.GradeExpandMode import io.github.wulkanowy.data.enums.GradeExpandMode
import io.github.wulkanowy.data.enums.GradeSortingMode.ALPHABETIC import io.github.wulkanowy.data.enums.GradeSortingMode
import io.github.wulkanowy.data.enums.GradeSortingMode.DATE import io.github.wulkanowy.data.enums.GradeSortingMode.*
import io.github.wulkanowy.data.repositories.GradeRepository import io.github.wulkanowy.data.repositories.GradeRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.SemesterRepository
@ -204,6 +204,7 @@ class GradeDetailsPresenter @Inject constructor(
ALPHABETIC -> gradeSubjects.sortedBy { gradeDetailsWithAverage -> ALPHABETIC -> gradeSubjects.sortedBy { gradeDetailsWithAverage ->
gradeDetailsWithAverage.subject.lowercase() gradeDetailsWithAverage.subject.lowercase()
} }
AVERAGE -> gradeSubjects.sortedByDescending { it.average }
} }
} }
.map { (subject, average, points, _, grades) -> .map { (subject, average, points, _, grades) ->

View File

@ -2,6 +2,9 @@ package io.github.wulkanowy.ui.modules.grade.summary
import io.github.wulkanowy.data.* import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.enums.GradeSortingMode
import io.github.wulkanowy.data.enums.GradeSortingMode.*
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
@ -14,6 +17,7 @@ import javax.inject.Inject
class GradeSummaryPresenter @Inject constructor( class GradeSummaryPresenter @Inject constructor(
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val preferencesRepository: PreferencesRepository,
private val averageProvider: GradeAverageProvider, private val averageProvider: GradeAverageProvider,
private val analytics: AnalyticsHelper private val analytics: AnalyticsHelper
) : BasePresenter<GradeSummaryView>(errorHandler, studentRepository) { ) : BasePresenter<GradeSummaryView>(errorHandler, studentRepository) {
@ -127,7 +131,17 @@ class GradeSummaryPresenter @Inject constructor(
private fun createGradeSummaryItems(items: List<GradeSubject>): List<GradeSummary> { private fun createGradeSummaryItems(items: List<GradeSubject>): List<GradeSummary> {
return items return items
.filter { !checkEmpty(it) } .filter { !checkEmpty(it) }
.sortedBy { it.subject } .let { gradeSubjects ->
when (preferencesRepository.gradeSortingMode) {
DATE -> gradeSubjects.sortedByDescending { gradeDetailsWithAverage ->
gradeDetailsWithAverage.grades.maxByOrNull { it.date }?.date
}
ALPHABETIC -> gradeSubjects.sortedBy { gradeDetailsWithAverage ->
gradeDetailsWithAverage.subject.lowercase()
}
AVERAGE -> gradeSubjects.sortedByDescending { it.average }
}
}
.map { it.summary.copy(average = it.average) } .map { it.summary.copy(average = it.average) }
} }

View File

@ -93,6 +93,7 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
} }
//https://developer.android.com/guide/playcore/in-app-updates#status_callback //https://developer.android.com/guide/playcore/in-app-updates#status_callback
@Deprecated("Deprecated in Java")
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)

View File

@ -172,7 +172,7 @@ class LoginFormPresenter @Inject constructor(
if ("@" in login && "||" !in login && "login" !in host && "email" !in host) { if ("@" in login && "||" !in login && "login" !in host && "email" !in host) {
val emailHost = login.substringAfter("@") val emailHost = login.substringAfter("@")
val emailDomain = URL(host).host val emailDomain = URL(host).host
if (emailHost != emailDomain) { if (!emailHost.equals(emailDomain, true)) {
view?.setErrorEmailInvalid(domain = emailDomain) view?.setErrorEmailInvalid(domain = emailDomain)
isCorrect = false isCorrect = false
} }

View File

@ -6,9 +6,7 @@ import android.os.Bundle
import android.view.View import android.view.View
import android.view.View.GONE import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.webkit.JavascriptInterface import android.webkit.*
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import com.yariksoffice.lingver.Lingver import com.yariksoffice.lingver.Lingver
@ -206,10 +204,9 @@ class LoginRecoverFragment :
} }
override fun onReceivedError( override fun onReceivedError(
view: WebView, view: WebView?,
errorCode: Int, request: WebResourceRequest?,
description: String, error: WebResourceError?
failingUrl: String
) { ) {
recoverWebViewSuccess = false recoverWebViewSuccess = false
} }

View File

@ -12,6 +12,7 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.elevation.ElevationOverlayProvider import com.google.android.material.elevation.ElevationOverlayProvider
import com.ncapdevi.fragnav.FragNavController import com.ncapdevi.fragnav.FragNavController
import com.ncapdevi.fragnav.FragNavController.Companion.HIDE import com.ncapdevi.fragnav.FragNavController.Companion.HIDE
@ -20,6 +21,7 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.databinding.ActivityMainBinding import io.github.wulkanowy.databinding.ActivityMainBinding
import io.github.wulkanowy.databinding.DialogAdsConsentBinding
import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog
@ -100,6 +102,7 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
} }
//https://developer.android.com/guide/playcore/in-app-updates#status_callback //https://developer.android.com/guide/playcore/in-app-updates#status_callback
@Deprecated("Deprecated in Java")
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
@ -287,6 +290,50 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
inAppReviewHelper.showInAppReview(this) inAppReviewHelper.showInAppReview(this)
} }
override fun showAppSupport() {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.main_support_title)
.setMessage(R.string.main_support_description)
.setPositiveButton(R.string.main_support_positive) { _, _ -> presenter.onEnableAdsSelected() }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.setOnDismissListener { }
.show()
}
override fun showPrivacyPolicyDialog() {
val dialogAdsConsentBinding = DialogAdsConsentBinding.inflate(layoutInflater)
val dialog = MaterialAlertDialogBuilder(this)
.setTitle(R.string.pref_ads_consent_title)
.setMessage(R.string.pref_ads_consent_description)
.setView(dialogAdsConsentBinding.root)
.show()
dialogAdsConsentBinding.adsConsentOver.setOnCheckedChangeListener { _, isChecked ->
dialogAdsConsentBinding.adsConsentPersonalised.isEnabled = isChecked
}
dialogAdsConsentBinding.adsConsentPersonalised.setOnClickListener {
presenter.onPrivacyAgree(true)
dialog.dismiss()
}
dialogAdsConsentBinding.adsConsentNonPersonalised.setOnClickListener {
presenter.onPrivacyAgree(false)
dialog.dismiss()
}
dialogAdsConsentBinding.adsConsentPrivacy.setOnClickListener { presenter.onPrivacySelected() }
dialogAdsConsentBinding.adsConsentCancel.setOnClickListener { dialog.cancel() }
}
override fun openPrivacyPolicy() {
openInternetBrowser(
"https://wulkanowy.github.io/polityka-prywatnosci.html",
::showMessage
)
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
navController.onSaveInstanceState(outState) navController.onSaveInstanceState(outState)

View File

@ -18,7 +18,10 @@ import io.github.wulkanowy.ui.modules.grade.GradeView
import io.github.wulkanowy.ui.modules.message.MessageView import io.github.wulkanowy.ui.modules.message.MessageView
import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersView import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersView
import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView
import io.github.wulkanowy.utils.AdsHelper
import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import timber.log.Timber import timber.log.Timber
@ -29,10 +32,12 @@ import javax.inject.Inject
class MainPresenter @Inject constructor( class MainPresenter @Inject constructor(
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val prefRepository: PreferencesRepository, private val preferencesRepository: PreferencesRepository,
private val syncManager: SyncManager, private val syncManager: SyncManager,
private val analytics: AnalyticsHelper, private val analytics: AnalyticsHelper,
private val json: Json private val json: Json,
private val adsHelper: AdsHelper,
private val appInfo: AppInfo
) : BasePresenter<MainView>(errorHandler, studentRepository) { ) : BasePresenter<MainView>(errorHandler, studentRepository) {
private var studentsWitSemesters: List<StudentWithSemesters>? = null private var studentsWitSemesters: List<StudentWithSemesters>? = null
@ -47,7 +52,7 @@ class MainPresenter @Inject constructor(
private val Destination?.startMenuIndex private val Destination?.startMenuIndex
get() = when { get() = when {
this == null -> prefRepository.startMenuIndex this == null -> preferencesRepository.startMenuIndex
destinationType in rootDestinationTypeList -> { destinationType in rootDestinationTypeList -> {
rootDestinationTypeList.indexOf(destinationType) rootDestinationTypeList.indexOf(destinationType)
} }
@ -71,6 +76,8 @@ class MainPresenter @Inject constructor(
syncManager.startPeriodicSyncWorker() syncManager.startPeriodicSyncWorker()
checkAppSupport()
analytics.logEvent("app_open", "destination" to initDestination.toString()) analytics.logEvent("app_open", "destination" to initDestination.toString())
Timber.i("Main view was initialized with $initDestination") Timber.i("Main view was initialized with $initDestination")
} }
@ -155,18 +162,52 @@ class MainPresenter @Inject constructor(
} == true } == true
} }
private fun checkInAppReview() { fun onEnableAdsSelected() {
prefRepository.inAppReviewCount++ view?.showPrivacyPolicyDialog()
}
if (prefRepository.inAppReviewDate == null) { fun onPrivacyAgree(isPersonalizedAds: Boolean) {
prefRepository.inAppReviewDate = Instant.now() preferencesRepository.isAgreeToProcessData = true
preferencesRepository.isPersonalizedAdsEnabled = isPersonalizedAds
adsHelper.initialize()
preferencesRepository.isAdsEnabled = true
}
fun onPrivacySelected() {
view?.openPrivacyPolicy()
}
private fun checkInAppReview() {
preferencesRepository.inAppReviewCount++
if (preferencesRepository.inAppReviewDate == null) {
preferencesRepository.inAppReviewDate = Instant.now()
} }
if (!prefRepository.isAppReviewDone && prefRepository.inAppReviewCount >= 50 && if (!preferencesRepository.isAppReviewDone && preferencesRepository.inAppReviewCount >= 50 &&
Instant.now().minus(Duration.ofDays(14)).isAfter(prefRepository.inAppReviewDate) Instant.now().minus(Duration.ofDays(14)).isAfter(preferencesRepository.inAppReviewDate)
) { ) {
view?.showInAppReview() view?.showInAppReview()
prefRepository.isAppReviewDone = true preferencesRepository.isAppReviewDone = true
}
}
private fun checkAppSupport() {
if (!preferencesRepository.isAppSupportShown && !preferencesRepository.isAdsEnabled
&& appInfo.buildFlavor == "play"
) {
presenterScope.launch {
val student = runCatching { studentRepository.getCurrentStudent(false) }
.onFailure { Timber.e(it) }
.getOrElse { return@launch }
if (Instant.now().minus(Duration.ofDays(28)).isAfter(student.registrationDate)) {
view?.showAppSupport()
preferencesRepository.isAppSupportShown = true
}
}
} }
} }

View File

@ -41,6 +41,12 @@ interface MainView : BaseView {
fun showInAppReview() fun showInAppReview()
fun showAppSupport()
fun showPrivacyPolicyDialog()
fun openPrivacyPolicy()
fun openMoreDestination(destination: Destination) fun openMoreDestination(destination: Destination)
interface MainChildView { interface MainChildView {

View File

@ -4,6 +4,8 @@ import android.annotation.SuppressLint
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_COMPACT
import androidx.core.text.parseAsHtml
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
@ -75,29 +77,25 @@ class MessagePreviewAdapter @Inject constructor() :
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
private fun bindMessage(holder: MessageViewHolder, message: Message) { private fun bindMessage(holder: MessageViewHolder, message: Message) {
val context = holder.binding.root.context val context = holder.binding.root.context
val recipientCount = message.unreadBy + message.readBy
val readText = when { val readTextValue = when {
recipientCount > 1 -> { !message.unread -> R.string.all_yes
context.getString(R.string.message_read_by, message.readBy, recipientCount) else -> R.string.all_no
}
message.readBy == 1 -> {
context.getString(R.string.message_read, context.getString(R.string.all_yes))
}
else -> context.getString(R.string.message_read, context.getString(R.string.all_no))
} }
val readText = context.getString(R.string.message_read, context.getString(readTextValue))
with(holder.binding) { with(holder.binding) {
messagePreviewSubject.text = messagePreviewSubject.text = message.subject.ifBlank {
message.subject.ifBlank { root.context.getString(R.string.message_no_subject) } context.getString(R.string.message_no_subject)
messagePreviewDate.text = root.context.getString( }
messagePreviewDate.text = context.getString(
R.string.message_date, R.string.message_date,
message.date.toFormattedString("yyyy-MM-dd HH:mm:ss") message.date.toFormattedString("yyyy-MM-dd HH:mm:ss")
) )
messagePreviewRead.text = readText messagePreviewRead.text = readText
messagePreviewContent.text = message.content messagePreviewContent.text = message.content.parseAsHtml(FROM_HTML_MODE_COMPACT)
messagePreviewFromSender.text = message.sender messagePreviewFromSender.text = message.sender
messagePreviewToRecipient.text = message.recipient messagePreviewToRecipient.text = message.recipients
} }
} }

View File

@ -57,7 +57,8 @@ class MessagePreviewFragment :
get() = getString(R.string.message_no_subject) get() = getString(R.string.message_no_subject)
override val printHTML: String override val printHTML: String
get() = requireContext().assets.open("message-print-page.html").bufferedReader().use { it.readText() } get() = requireContext().assets.open("message-print-page.html").bufferedReader()
.use { it.readText() }
override val messageNotExists: String override val messageNotExists: String
get() = getString(R.string.message_not_exists) get() = getString(R.string.message_not_exists)
@ -81,7 +82,10 @@ class MessagePreviewFragment :
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding = FragmentMessagePreviewBinding.bind(view) binding = FragmentMessagePreviewBinding.bind(view)
messageContainer = binding.messagePreviewContainer messageContainer = binding.messagePreviewContainer
presenter.onAttachView(this, (savedInstanceState ?: arguments)?.getSerializable(MESSAGE_ID_KEY) as? Message) presenter.onAttachView(
this,
(savedInstanceState ?: arguments)?.getSerializable(MESSAGE_ID_KEY) as? Message
)
} }
override fun initView() { override fun initView() {
@ -101,6 +105,8 @@ class MessagePreviewFragment :
menuShareButton = menu.findItem(R.id.messagePreviewMenuShare) menuShareButton = menu.findItem(R.id.messagePreviewMenuShare)
menuPrintButton = menu.findItem(R.id.messagePreviewMenuPrint) menuPrintButton = menu.findItem(R.id.messagePreviewMenuPrint)
presenter.onCreateOptionsMenu() presenter.onCreateOptionsMenu()
menu.findItem(R.id.mainMenuAccount).isVisible = false
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -129,8 +135,8 @@ class MessagePreviewFragment :
binding.messagePreviewRecycler.visibility = if (show) VISIBLE else GONE binding.messagePreviewRecycler.visibility = if (show) VISIBLE else GONE
} }
override fun showOptions(show: Boolean) { override fun showOptions(show: Boolean, isReplayable: Boolean) {
menuReplyButton?.isVisible = show menuReplyButton?.isVisible = isReplayable
menuForwardButton?.isVisible = show menuForwardButton?.isVisible = show
menuDeleteButton?.isVisible = show menuDeleteButton?.isVisible = show
menuShareButton?.isVisible = show menuShareButton?.isVisible = show
@ -173,7 +179,8 @@ class MessagePreviewFragment :
val webView = WebView(requireContext()) val webView = WebView(requireContext())
webView.webViewClient = object : WebViewClient() { webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest) = false override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest) =
false
override fun onPageFinished(view: WebView, url: String) { override fun onPageFinished(view: WebView, url: String) {
createWebPrintJob(view, jobName) createWebPrintJob(view, jobName)

View File

@ -1,10 +1,12 @@
package io.github.wulkanowy.ui.modules.message.preview package io.github.wulkanowy.ui.modules.message.preview
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.core.text.parseAsHtml
import io.github.wulkanowy.data.* import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.repositories.MailboxRepository
import io.github.wulkanowy.data.repositories.MessageRepository import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
@ -19,6 +21,7 @@ class MessagePreviewPresenter @Inject constructor(
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val messageRepository: MessageRepository, private val messageRepository: MessageRepository,
private val mailboxRepository: MailboxRepository,
private val analytics: AnalyticsHelper private val analytics: AnalyticsHelper
) : BasePresenter<MessagePreviewView>(errorHandler, studentRepository) { ) : BasePresenter<MessagePreviewView>(errorHandler, studentRepository) {
@ -52,7 +55,7 @@ class MessagePreviewPresenter @Inject constructor(
private fun loadData(messageToLoad: Message) { private fun loadData(messageToLoad: Message) {
flatResourceFlow { flatResourceFlow {
val student = studentRepository.getStudentById(messageToLoad.studentId) val student = studentRepository.getCurrentStudent()
messageRepository.getMessage(student, messageToLoad, true) messageRepository.getMessage(student, messageToLoad, true)
} }
.logResourceStatus("message ${messageToLoad.messageId} preview") .logResourceStatus("message ${messageToLoad.messageId} preview")
@ -104,62 +107,69 @@ class MessagePreviewPresenter @Inject constructor(
} }
fun onShare(): Boolean { fun onShare(): Boolean {
message?.let { val message = message ?: return false
var text = val subject = message.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }
"Temat: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }}\n" + when (it.sender.isNotEmpty()) {
true -> "Od: ${it.sender}\n"
false -> "Do: ${it.recipient}\n"
} + "Data: ${it.date.toFormattedString("yyyy-MM-dd HH:mm:ss")}\n\n${it.content}"
attachments?.let { attachments -> val text = buildString {
if (attachments.isNotEmpty()) { appendLine("Temat: $subject")
text += "\n\nZałączniki:" appendLine("Od: ${message.sender}")
appendLine("Do: ${message.recipients}")
appendLine("Data: ${message.date.toFormattedString("yyyy-MM-dd HH:mm:ss")}")
attachments.forEach { attachment -> appendLine()
text += "\n${attachment.filename}: ${attachment.url}"
} appendLine(message.content.parseAsHtml())
}
if (!attachments.isNullOrEmpty()) {
appendLine()
appendLine("Załączniki:")
append(attachments.orEmpty().joinToString(separator = "\n") { attachment ->
"${attachment.filename}: ${attachment.url}"
})
} }
view?.shareText(
text,
"FW: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }}"
)
return true
} }
return false
view?.shareText(
subject = "FW: $subject",
text = text,
)
return true
} }
@SuppressLint("NewApi") @SuppressLint("NewApi")
fun onPrint(): Boolean { fun onPrint(): Boolean {
message?.let { val message = message ?: return false
val dateString = it.date.toFormattedString("yyyy-MM-dd HH:mm:ss") val subject = message.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }
val infoContent = "<div><h4>Data wysłania</h4>$dateString</div>" + when {
it.sender.isNotEmpty() -> "<div><h4>Od</h4>${it.sender}</div>"
else -> "<div><h4>Do</h4>${it.recipient}</div>"
}
val messageContent = "<p>${it.content}</p>" val dateString = message.date.toFormattedString("yyyy-MM-dd HH:mm:ss")
.replace(Regex("[\\n\\r]{2,}"), "</p><p>")
.replace(Regex("[\\n\\r]"), "<br>")
val jobName = "Wiadomość " + when { val infoContent = buildString {
it.sender.isNotEmpty() -> "od ${it.sender}" append("<div><h4>Data wysłania</h4>$dateString</div>")
else -> "do ${it.recipient}"
} + " $dateString: ${it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() }} | Wulkanowy"
view?.apply { append("<div><h4>Od</h4>${message.sender}</div>")
val html = printHTML append("<div><h4>DO</h4>${message.recipients}</div>")
.replace(
"%SUBJECT%",
it.subject.ifBlank { view?.messageNoSubjectString.orEmpty() })
.replace("%CONTENT%", messageContent)
.replace("%INFO%", infoContent)
printDocument(html, jobName)
}
return true
} }
return false val messageContent = "<p>${message.content}</p>"
.replace(Regex("[\\n\\r]{2,}"), "</p><p>")
.replace(Regex("[\\n\\r]"), "<br>")
val jobName = buildString {
append("Wiadomość ")
append("od ${message.correspondents}")
append("do ${message.correspondents}")
append(" $dateString: $subject | Wulkanowy")
}
view?.apply {
val html = printHTML
.replace("%SUBJECT%", subject)
.replace("%CONTENT%", messageContent)
.replace("%INFO%", infoContent)
printDocument(html, jobName)
}
return true
} }
private fun deleteMessage() { private fun deleteMessage() {
@ -168,16 +178,17 @@ class MessagePreviewPresenter @Inject constructor(
view?.run { view?.run {
showContent(false) showContent(false)
showProgress(true) showProgress(true)
showOptions(false) showOptions(show = false, isReplayable = false)
showErrorView(false) showErrorView(false)
} }
Timber.i("Delete message ${message?.id}") Timber.i("Delete message ${message?.messageGlobalKey}")
presenterScope.launch { presenterScope.launch {
runCatching { runCatching {
val student = studentRepository.getCurrentStudent() val student = studentRepository.getCurrentStudent(decryptPass = true)
messageRepository.deleteMessage(student, message!!) val mailbox = mailboxRepository.getMailbox(student)
messageRepository.deleteMessage(student, mailbox, message!!)
} }
.onFailure { .onFailure {
retryCallback = { onMessageDelete() } retryCallback = { onMessageDelete() }
@ -211,7 +222,10 @@ class MessagePreviewPresenter @Inject constructor(
private fun initOptions() { private fun initOptions() {
view?.apply { view?.apply {
showOptions(message != null) showOptions(
show = message != null,
isReplayable = message?.folderId != MessageFolder.SENT.id,
)
message?.let { message?.let {
when (it.folderId == MessageFolder.TRASHED.id) { when (it.folderId == MessageFolder.TRASHED.id) {
true -> setDeletedOptionsLabels() true -> setDeletedOptionsLabels()

View File

@ -28,7 +28,7 @@ interface MessagePreviewView : BaseView {
fun setErrorRetryCallback(callback: () -> Unit) fun setErrorRetryCallback(callback: () -> Unit)
fun showOptions(show: Boolean) fun showOptions(show: Boolean, isReplayable: Boolean)
fun setDeletedOptionsLabels() fun setDeletedOptionsLabels()

View File

@ -6,6 +6,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Rect import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.text.Spanned
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.TouchDelegate import android.view.TouchDelegate
@ -13,11 +14,12 @@ import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.widget.Toast import android.widget.Toast
import android.widget.Toast.LENGTH_LONG import android.widget.Toast.LENGTH_LONG
import androidx.core.text.parseAsHtml
import androidx.core.text.toHtml
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.databinding.ActivitySendMessageBinding import io.github.wulkanowy.databinding.ActivitySendMessageBinding
import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.dpToPx
@ -72,17 +74,32 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
override val messageSuccess: String override val messageSuccess: String
get() = getString(R.string.message_send_successful) get() = getString(R.string.message_send_successful)
override val mailboxStudent: String
get() = getString(R.string.message_mailbox_type_student)
override val mailboxParent: String
get() = getString(R.string.message_mailbox_type_parent)
override val mailboxGuardian: String
get() = getString(R.string.message_mailbox_type_guardian)
override val mailboxEmployee: String
get() = getString(R.string.message_mailbox_type_employee)
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
public override fun onCreate(savedInstanceState: Bundle?) { public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(ActivitySendMessageBinding.inflate(layoutInflater).apply { binding = this }.root) setContentView(
ActivitySendMessageBinding.inflate(layoutInflater).apply { binding = this }.root
)
setSupportActionBar(binding.sendMessageToolbar) setSupportActionBar(binding.sendMessageToolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
messageContainer = binding.sendMessageContainer messageContainer = binding.sendMessageContainer
formRecipientsData = binding.sendMessageTo.addedChipItems as List<RecipientChipItem> formRecipientsData = binding.sendMessageTo.addedChipItems as List<RecipientChipItem>
formSubjectValue = binding.sendMessageSubject.text.toString() formSubjectValue = binding.sendMessageSubject.text.toString()
formContentValue = binding.sendMessageMessageContent.text.toString() formContentValue =
binding.sendMessageMessageContent.text.toString().parseAsHtml().toString()
presenter.onAttachView( presenter.onAttachView(
view = this, view = this,
@ -110,7 +127,7 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
} }
private fun onMessageContentChange(text: CharSequence?) { private fun onMessageContentChange(text: CharSequence?) {
formContentValue = text.toString() formContentValue = (text as Spanned).toHtml()
presenter.onMessageContentChange() presenter.onMessageContentChange()
} }
@ -132,8 +149,8 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
return presenter.onUpNavigate() return presenter.onUpNavigate()
} }
override fun setReportingUnit(unit: ReportingUnit) { override fun setMailbox(mailbox: String) {
binding.sendMessageFrom.text = unit.senderName binding.sendMessageFrom.text = mailbox
} }
override fun setRecipients(recipients: List<RecipientChipItem>) { override fun setRecipients(recipients: List<RecipientChipItem>) {
@ -165,7 +182,7 @@ class SendMessageActivity : BaseActivity<SendMessagePresenter, ActivitySendMessa
} }
override fun setContent(content: String) { override fun setContent(content: String) {
binding.sendMessageMessageContent.setText(content) binding.sendMessageMessageContent.setText(content.parseAsHtml())
} }
override fun showMessage(text: String) { override fun showMessage(text: String) {

View File

@ -1,6 +1,8 @@
package io.github.wulkanowy.ui.modules.message.send package io.github.wulkanowy.ui.modules.message.send
import io.github.wulkanowy.data.Resource import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.data.db.entities.MailboxType
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.Recipient import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.logResourceStatus import io.github.wulkanowy.data.logResourceStatus
@ -25,9 +27,8 @@ import javax.inject.Inject
class SendMessagePresenter @Inject constructor( class SendMessagePresenter @Inject constructor(
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val semesterRepository: SemesterRepository,
private val messageRepository: MessageRepository, private val messageRepository: MessageRepository,
private val reportingUnitRepository: ReportingUnitRepository, private val mailboxRepository: MailboxRepository,
private val recipientRepository: RecipientRepository, private val recipientRepository: RecipientRepository,
private val preferencesRepository: PreferencesRepository, private val preferencesRepository: PreferencesRepository,
private val analytics: AnalyticsHelper private val analytics: AnalyticsHelper
@ -52,20 +53,21 @@ class SendMessagePresenter @Inject constructor(
message?.let { message?.let {
setSubject( setSubject(
when (reply) { when (reply) {
true -> "Re: " true -> "RE: "
else -> "FW: " else -> "FW: "
} + message.subject } + message.subject
) )
if (preferencesRepository.fillMessageContent || reply != true) { if (preferencesRepository.fillMessageContent || reply != true) {
setContent( setContent(buildString {
when (reply) { if (reply == true) {
true -> "\n\n" append("<br><br>")
else -> "" }
} + when (message.sender.isNotEmpty()) {
true -> "Od: ${message.sender}\n" append("Od: ${message.sender}<br>")
false -> "Do: ${message.recipient}\n" append("Do: ${message.recipients}<br>")
} + "Data: ${message.date.toFormattedString("yyyy-MM-dd HH:mm:ss")}\n\n${message.content}" append("Data: ${message.date.toFormattedString("yyyy-MM-dd HH:mm:ss")}<br><br>")
) append(message.content)
})
} }
} }
} }
@ -111,21 +113,24 @@ class SendMessagePresenter @Inject constructor(
private fun loadData(message: Message?, reply: Boolean?) { private fun loadData(message: Message?, reply: Boolean?) {
resourceFlow { resourceFlow {
val student = studentRepository.getCurrentStudent() val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student) val mailbox = mailboxRepository.getMailbox(student)
val unit = reportingUnitRepository.getReportingUnit(student, semester.unitId)
Timber.i("Loading recipients started") Timber.i("Loading recipients started")
val recipients = when { val recipients = createChips(
unit != null -> recipientRepository.getRecipients(student, unit, 2) recipients = recipientRepository.getRecipients(
else -> listOf() student = student,
}.let { createChips(it) } mailbox = mailbox,
type = MailboxType.EMPLOYEE,
)
)
Timber.i("Loading recipients result: Success, fetched %d recipients", recipients.size) Timber.i("Loading recipients result: Success, fetched %d recipients", recipients.size)
Timber.i("Loading message recipients started") Timber.i("Loading message recipients started")
val messageRecipients = when { val messageRecipients = when {
message != null && reply == true -> recipientRepository.getMessageRecipients( message != null && reply == true -> recipientRepository.getMessageSender(
student, student = student,
message message = message,
mailbox = mailbox,
) )
else -> emptyList() else -> emptyList()
}.let { createChips(it) } }.let { createChips(it) }
@ -134,7 +139,7 @@ class SendMessagePresenter @Inject constructor(
messageRecipients.size messageRecipients.size
) )
Triple(unit, recipients, messageRecipients) Triple(mailbox, recipients, messageRecipients)
} }
.logResourceStatus("load recipients") .logResourceStatus("load recipients")
.onEach { .onEach {
@ -143,19 +148,14 @@ class SendMessagePresenter @Inject constructor(
showProgress(true) showProgress(true)
showContent(false) showContent(false)
} }
is Resource.Success -> it.data.let { (reportingUnit, recipientChips, selectedRecipientChips) -> is Resource.Success -> it.data.let { (mailbox, recipientChips, selectedRecipientChips) ->
view?.run { view?.run {
if (reportingUnit != null) { setMailbox(getMailboxName(mailbox))
setReportingUnit(reportingUnit) setRecipients(recipientChips)
setRecipients(recipientChips) if (selectedRecipientChips.isNotEmpty()) setSelectedRecipients(
if (selectedRecipientChips.isNotEmpty()) setSelectedRecipients( selectedRecipientChips
selectedRecipientChips )
) showContent(true)
showContent(true)
} else {
Timber.i("Loading recipients result: Can't find the reporting unit")
view?.showEmpty(true)
}
} }
} }
is Resource.Error -> { is Resource.Error -> {
@ -171,7 +171,14 @@ class SendMessagePresenter @Inject constructor(
private fun sendMessage(subject: String, content: String, recipients: List<Recipient>) { private fun sendMessage(subject: String, content: String, recipients: List<Recipient>) {
resourceFlow { resourceFlow {
val student = studentRepository.getCurrentStudent() val student = studentRepository.getCurrentStudent()
messageRepository.sendMessage(student, subject, content, recipients) val mailbox = mailboxRepository.getMailbox(student)
messageRepository.sendMessage(
student = student,
subject = subject,
content = content,
recipients = recipients,
mailboxId = mailbox.globalKey,
)
}.logResourceStatus("sending message").onEach { }.logResourceStatus("sending message").onEach {
when (it) { when (it) {
is Resource.Loading -> view?.run { is Resource.Loading -> view?.run {
@ -201,31 +208,44 @@ class SendMessagePresenter @Inject constructor(
} }
private fun createChips(recipients: List<Recipient>): List<RecipientChipItem> { private fun createChips(recipients: List<Recipient>): List<RecipientChipItem> {
fun generateCorrectSummary(recipientRealName: String): String {
val substring = recipientRealName.substringBeforeLast("-")
return when {
substring == recipientRealName -> recipientRealName
substring.indexOf("(") != -1 -> {
recipientRealName.indexOf("(")
.let { recipientRealName.substring(if (it != -1) it else 0) }
}
substring.indexOf("[") != -1 -> {
recipientRealName.indexOf("[")
.let { recipientRealName.substring(if (it != -1) it else 0) }
}
else -> recipientRealName.substringAfter("-")
}.trim()
}
return recipients.map { return recipients.map {
RecipientChipItem( RecipientChipItem(
title = it.name, title = it.userName,
summary = generateCorrectSummary(it.realName), summary = buildString {
getMailboxType(it.type)?.let(::append)
if (isNotBlank()) append(" ")
append("(${it.schoolShortName})")
},
recipient = it recipient = it
) )
} }
} }
private fun getMailboxName(mailbox: Mailbox): String {
return buildString {
append(mailbox.userName)
append(" - ")
append(getMailboxType(mailbox.type))
if (mailbox.type == MailboxType.PARENT) {
append(" - ")
append(mailbox.studentName)
}
append(" - ")
append("(${mailbox.schoolNameShort})")
}
}
private fun getMailboxType(type: MailboxType): String? = when (type) {
MailboxType.STUDENT -> view?.mailboxStudent
MailboxType.PARENT -> view?.mailboxParent
MailboxType.GUARDIAN -> view?.mailboxGuardian
MailboxType.EMPLOYEE -> view?.mailboxEmployee
MailboxType.UNKNOWN -> null
}
fun onMessageContentChange() { fun onMessageContentChange() {
presenterScope.launch { presenterScope.launch {
messageUpdateChannel.send(Unit) messageUpdateChannel.send(Unit)
@ -263,7 +283,7 @@ class SendMessagePresenter @Inject constructor(
fun getRecipientsNames(): String { fun getRecipientsNames(): String {
return messageRepository.draftMessage?.recipients.orEmpty() return messageRepository.draftMessage?.recipients.orEmpty()
.joinToString { it.recipient.name } .joinToString { it.recipient.userName }
} }
fun clearDraft() { fun clearDraft() {

View File

@ -1,6 +1,6 @@
package io.github.wulkanowy.ui.modules.message.send package io.github.wulkanowy.ui.modules.message.send
import io.github.wulkanowy.data.db.entities.ReportingUnit import io.github.wulkanowy.data.db.entities.Mailbox
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
interface SendMessageView : BaseView { interface SendMessageView : BaseView {
@ -18,9 +18,17 @@ interface SendMessageView : BaseView {
val messageSuccess: String val messageSuccess: String
val mailboxStudent: String
val mailboxParent: String
val mailboxGuardian: String
val mailboxEmployee: String
fun initView() fun initView()
fun setReportingUnit(unit: ReportingUnit) fun setMailbox(mailbox: String)
fun setRecipients(recipients: List<RecipientChipItem>) fun setRecipients(recipients: List<RecipientChipItem>)

View File

@ -8,7 +8,6 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.databinding.ItemMessageBinding import io.github.wulkanowy.databinding.ItemMessageBinding
import io.github.wulkanowy.databinding.ItemMessageChipsBinding import io.github.wulkanowy.databinding.ItemMessageChipsBinding
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
@ -88,12 +87,8 @@ class MessageTabAdapter @Inject constructor() :
with(holder.binding) { with(holder.binding) {
val style = if (message.unread) Typeface.BOLD else Typeface.NORMAL val style = if (message.unread) Typeface.BOLD else Typeface.NORMAL
messageItemAuthor.run { with(messageItemAuthor) {
text = if (message.folderId == MessageFolder.SENT.id) { text = message.correspondents
message.recipient
} else {
message.sender
}
setTypeface(null, style) setTypeface(null, style)
} }
messageItemSubject.run { messageItemSubject.run {
@ -145,7 +140,7 @@ class MessageTabAdapter @Inject constructor() :
val newItem = new[newItemPosition] val newItem = new[newItemPosition]
return if (oldItem is MessageTabDataItem.MessageItem && newItem is MessageTabDataItem.MessageItem) { return if (oldItem is MessageTabDataItem.MessageItem && newItem is MessageTabDataItem.MessageItem) {
oldItem.message.id == newItem.message.id oldItem.message.messageGlobalKey == newItem.message.messageGlobalKey
} else { } else {
oldItem.viewType == newItem.viewType oldItem.viewType == newItem.viewType
} }

View File

@ -3,8 +3,8 @@ package io.github.wulkanowy.ui.modules.message.tab
import io.github.wulkanowy.data.* import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.Message import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.repositories.MailboxRepository
import io.github.wulkanowy.data.repositories.MessageRepository import io.github.wulkanowy.data.repositories.MessageRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
@ -26,7 +26,7 @@ class MessageTabPresenter @Inject constructor(
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val messageRepository: MessageRepository, private val messageRepository: MessageRepository,
private val semesterRepository: SemesterRepository, private val mailboxRepository: MailboxRepository,
private val analytics: AnalyticsHelper private val analytics: AnalyticsHelper
) : BasePresenter<MessageTabView>(errorHandler, studentRepository) { ) : BasePresenter<MessageTabView>(errorHandler, studentRepository) {
@ -122,7 +122,8 @@ class MessageTabPresenter @Inject constructor(
runCatching { runCatching {
val student = studentRepository.getCurrentStudent(true) val student = studentRepository.getCurrentStudent(true)
messageRepository.deleteMessages(student, messageList) val mailbox = mailboxRepository.getMailbox(student)
messageRepository.deleteMessages(student, mailbox, messageList)
} }
.onFailure(errorHandler::dispatch) .onFailure(errorHandler::dispatch)
.onSuccess { view?.showMessagesDeleted() } .onSuccess { view?.showMessagesDeleted() }
@ -159,7 +160,7 @@ class MessageTabPresenter @Inject constructor(
} }
fun onMessageItemSelected(messageItem: MessageTabDataItem.MessageItem, position: Int) { fun onMessageItemSelected(messageItem: MessageTabDataItem.MessageItem, position: Int) {
Timber.i("Select message ${messageItem.message.id} item (position: $position)") Timber.i("Select message ${messageItem.message.messageGlobalKey} item (position: $position)")
if (!isActionMode) { if (!isActionMode) {
view?.run { view?.run {
@ -206,8 +207,8 @@ class MessageTabPresenter @Inject constructor(
flatResourceFlow { flatResourceFlow {
val student = studentRepository.getCurrentStudent() val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student) val mailbox = mailboxRepository.getMailbox(student)
messageRepository.getMessages(student, semester, folder, forceRefresh) messageRepository.getMessages(student, mailbox, folder, forceRefresh)
} }
.logResourceStatus("load $folder message") .logResourceStatus("load $folder message")
.onResourceData { .onResourceData {
@ -333,7 +334,7 @@ class MessageTabPresenter @Inject constructor(
addAll(data.map { message -> addAll(data.map { message ->
MessageTabDataItem.MessageItem( MessageTabDataItem.MessageItem(
message = message, message = message,
isSelected = messagesToDelete.any { it.id == message.id }, isSelected = messagesToDelete.any { it.messageGlobalKey == message.messageGlobalKey },
isActionMode = isActionMode isActionMode = isActionMode
) )
}) })
@ -345,10 +346,9 @@ class MessageTabPresenter @Inject constructor(
private fun calculateMatchRatio(message: Message, query: String): Int { private fun calculateMatchRatio(message: Message, query: String): Int {
val subjectRatio = FuzzySearch.tokenSortPartialRatio(query.lowercase(), message.subject) val subjectRatio = FuzzySearch.tokenSortPartialRatio(query.lowercase(), message.subject)
val senderOrRecipientRatio = FuzzySearch.tokenSortPartialRatio( val correspondentsRatio = FuzzySearch.tokenSortPartialRatio(
query.lowercase(), query.lowercase(),
if (message.sender.isNotEmpty()) message.sender.lowercase() message.correspondents
else message.recipient.lowercase()
) )
val dateRatio = listOf( val dateRatio = listOf(
@ -364,7 +364,7 @@ class MessageTabPresenter @Inject constructor(
return (subjectRatio.toDouble().pow(2) return (subjectRatio.toDouble().pow(2)
+ senderOrRecipientRatio.toDouble().pow(2) + correspondentsRatio.toDouble().pow(2)
+ dateRatio.toDouble().pow(2) * 2 + dateRatio.toDouble().pow(2) * 2
).toInt() ).toInt()
} }

View File

@ -87,15 +87,19 @@ class TimetablePresenter @Inject constructor(
fun onViewReselected() { fun onViewReselected() {
Timber.i("Timetable view is reselected") Timber.i("Timetable view is reselected")
view?.also { view -> view?.let { view ->
if (view.currentStackSize == 1) { if (view.currentStackSize == 1) {
baseDate.also { baseDate = now().nextOrSameSchoolDay
if (currentDate != it) {
reloadView(it) if (currentDate != baseDate) {
loadData() reloadView(baseDate)
} else if (!view.isViewEmpty) view.resetView() loadData()
} else if (!view.isViewEmpty) {
view.resetView()
} }
} else view.popView() } else {
view.popView()
}
} }
} }

View File

@ -1,8 +1,6 @@
package io.github.wulkanowy.ui.modules.timetablewidget package io.github.wulkanowy.ui.modules.timetablewidget
import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE import android.appwidget.AppWidgetManager.*
import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID
import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_IDS
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@ -17,6 +15,7 @@ import io.github.wulkanowy.databinding.ActivityWidgetConfigureBinding
import io.github.wulkanowy.ui.base.BaseActivity import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.base.WidgetConfigureAdapter import io.github.wulkanowy.ui.base.WidgetConfigureAdapter
import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.EXTRA_FROM_CONFIGURE
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.EXTRA_FROM_PROVIDER import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider.Companion.EXTRA_FROM_PROVIDER
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import javax.inject.Inject import javax.inject.Inject
@ -92,6 +91,7 @@ class TimetableWidgetConfigureActivity :
.apply { .apply {
action = ACTION_APPWIDGET_UPDATE action = ACTION_APPWIDGET_UPDATE
putExtra(EXTRA_APPWIDGET_IDS, intArrayOf(widgetId)) putExtra(EXTRA_APPWIDGET_IDS, intArrayOf(widgetId))
putExtra(EXTRA_FROM_CONFIGURE, true)
}) })
} }

View File

@ -1,6 +1,5 @@
package io.github.wulkanowy.ui.modules.timetablewidget package io.github.wulkanowy.ui.modules.timetablewidget
import android.annotation.SuppressLint
import android.app.PendingIntent import android.app.PendingIntent
import android.appwidget.AppWidgetManager import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetManager.* import android.appwidget.AppWidgetManager.*
@ -61,6 +60,8 @@ class TimetableWidgetProvider : BroadcastReceiver() {
private const val BUTTON_RESET = "buttonReset" private const val BUTTON_RESET = "buttonReset"
const val EXTRA_FROM_CONFIGURE = "extraFromConfigure"
const val EXTRA_FROM_PROVIDER = "extraFromProvider" const val EXTRA_FROM_PROVIDER = "extraFromProvider"
fun getDateWidgetKey(appWidgetId: Int) = "timetable_widget_date_$appWidgetId" fun getDateWidgetKey(appWidgetId: Int) = "timetable_widget_date_$appWidgetId"
@ -87,12 +88,22 @@ class TimetableWidgetProvider : BroadcastReceiver() {
} }
private suspend fun onUpdate(context: Context, intent: Intent) { private suspend fun onUpdate(context: Context, intent: Intent) {
if (intent.getStringExtra(EXTRA_BUTTON_TYPE) === null) { if (intent.getStringExtra(EXTRA_BUTTON_TYPE) == null) {
intent.getIntArrayExtra(EXTRA_APPWIDGET_IDS)?.forEach { appWidgetId -> val isFromConfigure = intent.getBooleanExtra(EXTRA_FROM_CONFIGURE, false)
val appWidgetIds = intent.getIntArrayExtra(EXTRA_APPWIDGET_IDS) ?: return
appWidgetIds.forEach { appWidgetId ->
val student = val student =
getStudent(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId) getStudent(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId)
val savedDataEpochDay = sharedPref.getLong(getDateWidgetKey(appWidgetId), 0)
updateWidget(context, appWidgetId, getWidgetDateToLoad(appWidgetId), student) val dateToLoad = if (isFromConfigure && savedDataEpochDay != 0L) {
LocalDate.ofEpochDay(savedDataEpochDay)
} else {
getWidgetDefaultDateToLoad(appWidgetId)
}
updateWidget(context, appWidgetId, dateToLoad, student)
} }
} else { } else {
val buttonType = intent.getStringExtra(EXTRA_BUTTON_TYPE) val buttonType = intent.getStringExtra(EXTRA_BUTTON_TYPE)
@ -104,10 +115,10 @@ class TimetableWidgetProvider : BroadcastReceiver() {
val savedDate = val savedDate =
LocalDate.ofEpochDay(sharedPref.getLong(getDateWidgetKey(toggledWidgetId), 0)) LocalDate.ofEpochDay(sharedPref.getLong(getDateWidgetKey(toggledWidgetId), 0))
val date = when (buttonType) { val date = when (buttonType) {
BUTTON_RESET -> getWidgetDateToLoad(toggledWidgetId) BUTTON_RESET -> getWidgetDefaultDateToLoad(toggledWidgetId)
BUTTON_NEXT -> savedDate.nextSchoolDay BUTTON_NEXT -> savedDate.nextSchoolDay
BUTTON_PREV -> savedDate.previousSchoolDay BUTTON_PREV -> savedDate.previousSchoolDay
else -> getWidgetDateToLoad(toggledWidgetId) else -> getWidgetDefaultDateToLoad(toggledWidgetId)
} }
if (!buttonType.isNullOrBlank()) { if (!buttonType.isNullOrBlank()) {
analytics.logEvent( analytics.logEvent(
@ -132,7 +143,6 @@ class TimetableWidgetProvider : BroadcastReceiver() {
} }
} }
@SuppressLint("DefaultLocale")
private fun updateWidget( private fun updateWidget(
context: Context, context: Context,
appWidgetId: Int, appWidgetId: Int,
@ -273,7 +283,7 @@ class TimetableWidgetProvider : BroadcastReceiver() {
return avatarBitmap return avatarBitmap
} }
private fun getWidgetDateToLoad(appWidgetId: Int): LocalDate { private fun getWidgetDefaultDateToLoad(appWidgetId: Int): LocalDate {
val lastLessonEndTimestamp = val lastLessonEndTimestamp =
sharedPref.getLong(getTodayLastLessonEndDateTimeWidgetKey(appWidgetId), 0) sharedPref.getLong(getTodayLastLessonEndDateTimeWidgetKey(appWidgetId), 0)
val lastLessonEndDateTime = val lastLessonEndDateTime =

View File

@ -13,6 +13,7 @@ import androidx.core.graphics.drawable.RoundedBitmapDrawable
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
@ColorInt @ColorInt
fun Context.getThemeAttrColor(@AttrRes colorAttr: Int): Int { fun Context.getThemeAttrColor(@AttrRes colorAttr: Int): Int {
val array = obtainStyledAttributes(null, intArrayOf(colorAttr)) val array = obtainStyledAttributes(null, intArrayOf(colorAttr))

View File

@ -1,5 +1,9 @@
Wersja 1.6.4 Wersja 1.7.0
- naprawiliśmy błąd ładowania frekwencji na GPE i Lubelskim Portalu Oświatowym - naprawiliśmy logowanie do aplikacji
- dodaliśmy wsparcie nowego modułu Wiadomości Plus
- dodaliśmy nową możliwość wsparcia naszego projektu przez opcjonalne reklamy
- dodaliśmy sortowanie po średniej
- naprawiliśmy też kilka usterek wpływających na komfort używania aplikacji
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

View File

@ -16,8 +16,7 @@
app:layout_constraintBottom_toTopOf="@id/sendMessageScroll" app:layout_constraintBottom_toTopOf="@id/sendMessageScroll"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent" />
tools:targetApi="lollipop" />
<io.github.wulkanowy.materialchipsinput.ConsumedNestedScrollView <io.github.wulkanowy.materialchipsinput.ConsumedNestedScrollView
android:id="@+id/sendMessageScroll" android:id="@+id/sendMessageScroll"

View File

@ -0,0 +1,79 @@
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/ads_consent_privacy"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_marginHorizontal="24dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:padding="0dp"
android:text="Privacy Policy"
android:textAllCaps="false"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/ads_consent_over"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="17dp"
android:layout_marginTop="8dp"
android:text="I am over 18 years old"
android:textColor="?android:textColorSecondary"
android:textSize="14dp"
app:layout_constraintTop_toBottomOf="@id/ads_consent_privacy" />
<com.google.android.material.button.MaterialButton
android:id="@+id/ads_consent_personalised"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="24dp"
android:enabled="false"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:text="Yes, personalized ads"
app:layout_constraintTop_toBottomOf="@id/ads_consent_over" />
<com.google.android.material.button.MaterialButton
android:id="@+id/ads_consent_non_personalised"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="24dp"
android:layout_marginBottom="24dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:text="Yes, non-personalized ads"
app:layout_constraintBottom_toTopOf="@id/ads_consent_cancel"
app:layout_constraintTop_toBottomOf="@id/ads_consent_personalised"
app:layout_constraintVertical_bias="0" />
<com.google.android.material.button.MaterialButton
android:id="@+id/ads_consent_cancel"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="24dp"
android:layout_marginBottom="24dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:text="Cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintVertical_bias="0" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -19,7 +19,9 @@
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/noteRecycler" android:id="@+id/noteRecycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent"
tools:itemCount="4"
tools:listitem="@layout/item_note" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<LinearLayout <LinearLayout

View File

@ -37,7 +37,7 @@
android:padding="10dp" android:padding="10dp"
android:visibility="gone" android:visibility="gone"
tools:ignore="UseCompoundDrawables" tools:ignore="UseCompoundDrawables"
tools:visibility="visible"> tools:visibility="gone">
<ImageView <ImageView
android:layout_width="100dp" android:layout_width="100dp"
@ -63,7 +63,7 @@
android:orientation="vertical" android:orientation="vertical"
android:visibility="invisible" android:visibility="invisible"
tools:ignore="UseCompoundDrawables" tools:ignore="UseCompoundDrawables"
tools:visibility="visible"> tools:visibility="gone">
<ImageView <ImageView
android:layout_width="100dp" android:layout_width="100dp"

View File

@ -0,0 +1,15 @@
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:layout_marginVertical="6dp"
app:cardElevation="4dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/dashboard_admin_message_item_content"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -54,7 +54,7 @@
<TextView <TextView
android:id="@+id/noteItemPoints" android:id="@+id/noteItemPoints"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:textSize="16sp" android:textSize="16sp"

View File

@ -3,10 +3,11 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="64dp" android:layout_height="wrap_content"
android:background="?selectableItemBackground" android:background="?selectableItemBackground"
android:clickable="true" android:clickable="true"
android:focusable="true"> android:focusable="true"
android:minHeight="64dp">
<TextView <TextView
android:id="@+id/student_info_item_title" android:id="@+id/student_info_item_title"
@ -16,12 +17,12 @@
android:layout_marginTop="13dp" android:layout_marginTop="13dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:gravity="bottom" android:gravity="bottom"
android:maxLines="1"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
android:textSize="12sp" android:textSize="12sp"
app:layout_constraintEnd_toStartOf="@id/student_info_item_arrow" app:layout_constraintEnd_toStartOf="@id/student_info_item_arrow"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:maxLines="1"
tools:text="@tools:sample/lorem/random" /> tools:text="@tools:sample/lorem/random" />
<TextView <TextView
@ -31,12 +32,12 @@
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:gravity="bottom" android:gravity="bottom"
android:maxLines="1"
android:textColor="?android:textColorPrimary" android:textColor="?android:textColorPrimary"
android:textSize="16sp" android:textSize="16sp"
app:layout_constraintEnd_toStartOf="@id/student_info_item_arrow" app:layout_constraintEnd_toStartOf="@id/student_info_item_arrow"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/student_info_item_title" app:layout_constraintTop_toBottomOf="@id/student_info_item_title"
tools:maxLines="1"
tools:text="@tools:sample/lorem/random" /> tools:text="@tools:sample/lorem/random" />
<ImageView <ImageView

View File

@ -34,6 +34,7 @@
<string-array name="grade_sorting_mode_entries"> <string-array name="grade_sorting_mode_entries">
<item>Abecedně</item> <item>Abecedně</item>
<item>Podle data</item> <item>Podle data</item>
<item>Podle průměru</item>
</string-array> </string-array>
<string-array name="grade_color_scheme_entries"> <string-array name="grade_color_scheme_entries">
<item>Dzienniczek+</item> <item>Dzienniczek+</item>

View File

@ -77,6 +77,9 @@
<string name="main_log_in">Přihlásit se</string> <string name="main_log_in">Přihlásit se</string>
<string name="main_session_expired">Relace vypršela</string> <string name="main_session_expired">Relace vypršela</string>
<string name="main_session_relogin">Relace vypršela. Přihlaste se prosím znovu</string> <string name="main_session_relogin">Relace vypršela. Přihlaste se prosím znovu</string>
<string name="main_support_title">Podpora aplikace</string>
<string name="main_support_description">Líbí se Vám tato aplikace? Podpořte její vývoj tím, že povolíte neinvazivní reklamy, které můžete kdykoliv vypnout</string>
<string name="main_support_positive">Zapnout reklamy</string>
<!--Grade--> <!--Grade-->
<string name="grade_header">Známka</string> <string name="grade_header">Známka</string>
<string name="grade_semester">Semestr %d</string> <string name="grade_semester">Semestr %d</string>
@ -94,7 +97,7 @@
<string name="grade_summary_predicted_grade">Předpokládaná známka</string> <string name="grade_summary_predicted_grade">Předpokládaná známka</string>
<string name="grade_summary_calculated_average">Vypočítaný průměr</string> <string name="grade_summary_calculated_average">Vypočítaný průměr</string>
<string name="grade_summary_calculated_average_help_dialog_title">Jak funguje vypočítaný průměr?</string> <string name="grade_summary_calculated_average_help_dialog_title">Jak funguje vypočítaný průměr?</string>
<string name="grade_summary_calculated_average_help_dialog_message">Vypočítaný průměr je aritmetický průměr vypočítaný z průměrů předmětů. Umožňuje vám to znát přibližný konečný průměr. Vypočítává se způsobem zvoleným uživatelem v nastavení aplikaci. Doporučuje se vybrat příslušnou možnost. Důvodem je rozdílný výpočet školních průměrů. Pokud vaše škola navíc uvádí průměr předmětů na stránce deníku Vulcan, aplikace si je stáhne a tyto průměry nepočítá. To lze změnit vynucením výpočtu průměru v nastavení aplikaci.\n\n<b>Průměr známek pouze z vybraného semestru</b>:\n1. Výpočet váženého průměru pro každý předmět v daném semestru\n2. Sčítání vypočítaných průměrů\n3. Výpočet aritmetického průměru součtených průměrů\n\n<b>Průměr průměrů z obou semestrů</b>:\n1. Výpočet váženého průměru pro každý předmět v semestru 1 a 2\n2. Výpočet aritmetického průměru vypočítaných průměrů za semestry 1 a 2 pro každý předmět.\n3. Sčítání vypočítaných průměrů\n4. Výpočet aritmetického průměru sečtených průměrů\n\n<b>Průměr známek z celého roku:</b>\n1. Výpočet váženého průměru za rok pro každý předmět. Konečný průměr v 1. semestru je nepodstatný.\n3. Sčítání vypočítaných průměrů\n4. Výpočet aritmetického průměru součtených průměrů</string> <string name="grade_summary_calculated_average_help_dialog_message">Vypočítaný průměr je aritmetický průměr vypočítaný z průměrů předmětů. Umožňuje vám to znát přibližný konečný průměr. Vypočítává se způsobem zvoleným uživatelem v nastavení aplikaci. Doporučuje se vybrat příslušnou možnost. Důvodem je rozdílný výpočet školních průměrů. Pokud vaše škola navíc uvádí průměr předmětů na stránce deníku Vulcan, aplikace si je stáhne a tyto průměry nepočítá. To lze změnit vynucením výpočtu průměru v nastavení aplikaci.\n\n<b>Průměr známek pouze z vybraného semestru</b>:\n1. Výpočet váženého průměru pro každý předmět v daném semestru\n2. Sčítání vypočítaných průměrů\n3. Výpočet aritmetického průměru součtených průměrů\n\n<b>Průměr průměrů z obou semestrů</b>:\n1. Výpočet váženého průměru pro každý předmět v semestru 1 a 2\n2. Výpočet aritmetického průměru vypočítaných průměrů za semestry 1 a 2 pro každý předmět.\n3. Sčítání vypočítaných průměrů\n4. Výpočet aritmetického průměru sečtených průměrů\n\n<b>Průměr známek z celého roku:</b>\n1. Výpočet váženého průměru za rok pro každý předmět. Konečný průměr v 1. semestru je nepodstatný.\n2. Sčítání vypočítaných průměrů\n3. Výpočet aritmetického průměru součtených průměrů</string>
<string name="grade_summary_final_average_help_dialog_title">Jak funguje konečný průměr?</string> <string name="grade_summary_final_average_help_dialog_title">Jak funguje konečný průměr?</string>
<string name="grade_summary_final_average_help_dialog_message">Konečný průměr je aritmetický průměr vypočítaný ze všech aktuálně dostupných konečných známek v daném semestru.\n\nSchéma výpočtu se skládá z následujících kroků:\n1. Sčítání konečných známek zadaných učiteli\n2. Děleno počtem předmětů, pro které už byly uděleny známky</string> <string name="grade_summary_final_average_help_dialog_message">Konečný průměr je aritmetický průměr vypočítaný ze všech aktuálně dostupných konečných známek v daném semestru.\n\nSchéma výpočtu se skládá z následujících kroků:\n1. Sčítání konečných známek zadaných učiteli\n2. Děleno počtem předmětů, pro které už byly uděleny známky</string>
<string name="grade_summary_final_average">Konečný průměr</string> <string name="grade_summary_final_average">Konečný průměr</string>
@ -294,6 +297,10 @@
<string name="message_move_to_trash">Přesunout do koše</string> <string name="message_move_to_trash">Přesunout do koše</string>
<string name="message_delete_forever">Odstranit natrvalo</string> <string name="message_delete_forever">Odstranit natrvalo</string>
<string name="message_delete_success">Zpráva byla úspěšně odstraněna</string> <string name="message_delete_success">Zpráva byla úspěšně odstraněna</string>
<string name="message_mailbox_type_student">žák</string>
<string name="message_mailbox_type_parent">rodič</string>
<string name="message_mailbox_type_guardian">opatrovník</string>
<string name="message_mailbox_type_employee">pracovník</string>
<string name="message_share">Sdílet</string> <string name="message_share">Sdílet</string>
<string name="message_print">Vytisknout</string> <string name="message_print">Vytisknout</string>
<string name="message_subject">Předmět</string> <string name="message_subject">Předmět</string>
@ -305,7 +312,6 @@
<string name="message_chip_only_unread">Pouze nepřečtené</string> <string name="message_chip_only_unread">Pouze nepřečtené</string>
<string name="message_chip_only_with_attachments">Pouze s přílohami</string> <string name="message_chip_only_with_attachments">Pouze s přílohami</string>
<string name="message_read">Přečtena: %s</string> <string name="message_read">Přečtena: %s</string>
<string name="message_read_by">Přečtena přes: %1$d z %2$d osob</string>
<plurals name="message_number_item"> <plurals name="message_number_item">
<item quantity="one">%1$d zpráva</item> <item quantity="one">%1$d zpráva</item>
<item quantity="few">%1$d zprávy</item> <item quantity="few">%1$d zprávy</item>
@ -721,6 +727,10 @@
<string name="pref_ads_privacy_link">Ochrana osobních údajů</string> <string name="pref_ads_privacy_link">Ochrana osobních údajů</string>
<string name="pref_ads_loading">Reklama se načítá</string> <string name="pref_ads_loading">Reklama se načítá</string>
<string name="pref_ads_once_per_visit">Děkujeme za vaši podporu, vraťte se později pro více reklam</string> <string name="pref_ads_once_per_visit">Děkujeme za vaši podporu, vraťte se později pro více reklam</string>
<string name="pref_ads_consent_title">Můžeme použít Vaše data k zobrazení reklam?</string>
<string name="pref_ads_consent_description">Volbu můžete kdykoliv změnit v nastavení aplikace. Můžeme použít Vaše data k zobrazení reklam šitých pro vás nebo pomocí méně vašich dat zobrazovat nepřizpůsobené reklamy. Podrobnosti naleznete v našich Zásadách ochrany osobních údajů</string>
<string name="pref_ads_summary_personalized">Přizpůsobené reklamy</string>
<string name="pref_ads_summary_non_personalized">Nepřizpůsobené reklamy</string>
<string name="pref_settings_advanced_title">Pokročilé</string> <string name="pref_settings_advanced_title">Pokročilé</string>
<string name="pref_settings_appearance_title">Vzhled a chování</string> <string name="pref_settings_appearance_title">Vzhled a chování</string>
<string name="pref_settings_notifications_title">Oznámení</string> <string name="pref_settings_notifications_title">Oznámení</string>

View File

@ -34,6 +34,7 @@
<string-array name="grade_sorting_mode_entries"> <string-array name="grade_sorting_mode_entries">
<item>Alphabetisch</item> <item>Alphabetisch</item>
<item>Nach Datum</item> <item>Nach Datum</item>
<item>Nach Durchschnitt</item>
</string-array> </string-array>
<string-array name="grade_color_scheme_entries"> <string-array name="grade_color_scheme_entries">
<item>Dzienniczek+</item> <item>Dzienniczek+</item>

View File

@ -77,6 +77,9 @@
<string name="main_log_in">Anmelden</string> <string name="main_log_in">Anmelden</string>
<string name="main_session_expired">Die Sitzung ist abgelaufen</string> <string name="main_session_expired">Die Sitzung ist abgelaufen</string>
<string name="main_session_relogin">Die Sitzung ist abgelaufen, bitte loggen Sie sich erneut ein</string> <string name="main_session_relogin">Die Sitzung ist abgelaufen, bitte loggen Sie sich erneut ein</string>
<string name="main_support_title">Anwendungsunterstützung</string>
<string name="main_support_description">Gefällt Ihnen diese App? Unterstützen Sie ihre Entwicklung, indem Sie nicht-invasive Werbung aktivieren, die Sie jederzeit deaktivieren können</string>
<string name="main_support_positive">Werbung aktivieren</string>
<!--Grade--> <!--Grade-->
<string name="grade_header">Note</string> <string name="grade_header">Note</string>
<string name="grade_semester">Semester %d</string> <string name="grade_semester">Semester %d</string>
@ -94,7 +97,7 @@
<string name="grade_summary_predicted_grade">Vorhergesagte Note</string> <string name="grade_summary_predicted_grade">Vorhergesagte Note</string>
<string name="grade_summary_calculated_average">Berechnender Durchschnitt</string> <string name="grade_summary_calculated_average">Berechnender Durchschnitt</string>
<string name="grade_summary_calculated_average_help_dialog_title">Wie funktioniert der berechnete Durchschnitt?</string> <string name="grade_summary_calculated_average_help_dialog_title">Wie funktioniert der berechnete Durchschnitt?</string>
<string name="grade_summary_calculated_average_help_dialog_message">Der berechnete Mittelwert ist das arithmetische Mittel, das aus den Durchschnittswerten der Probanden errechnet wird. Es erlaubt Ihnen, den ungefähre endgültigen Durchschnitt zu kennen. Sie wird auf eine vom Anwender in den Anwendungseinstellungen gewählte Weise berechnet. Es wird empfohlen, die entsprechende Option zu wählen. Das liegt daran, dass die Berechnung der Schuldurchschnitte unterschiedlich ist. Wenn Ihre Schule den Durchschnitt der Fächer auf der Vulcan-Seite angibt, lädt die Anwendung diese Fächer herunter und berechnet nicht den Durchschnitt. Dies kann geändert werden, indem die Berechnung des Durchschnitts in den Anwendungseinstellungen erzwungen wird. \n\n<b>Durchschnitt der Noten nur aus dem ausgewählten Semester </b>:\n1. Berechnung des gewichteten Durchschnitts für jedes Fach in einem bestimmten Semester\n2. Addition der berechneten Durchschnittswerte\n3. Berechnung des arithmetischen Mittels der summierten Durchschnitte\n<b>Durchschnitt der Durchschnitte aus beiden Semestern</b>:\n1. Berechnung des gewichteten Durchschnitts für jedes Fach in Semester 1 und 2\n2. Berechnung des arithmetischen Mittels der berechneten Durchschnitte für Semester 1 und 2 für jedes Fach. \n3. Hinzufügen von berechneten Durchschnittswerten\n4. Berechnung des arithmetischen Mittels der summierten Durchschnitte\n<b>Durchschnitt der Noten aus dem ganzen Jahr:</b>\n1. Berechnung des gewichteten Jahresdurchschnitts für jedes Fach. Der Abschlussdurchschnitt im 1. Semester ist irrelevant. \n3. Addition der berechneten Durchschnittswerte\n4. Berechnung des arithmetischen Mittels der summierten Mittelwerte</string> <string name="grade_summary_calculated_average_help_dialog_message">Der berechnete Mittelwert ist das arithmetische Mittel, das aus den Durchschnittswerten der Probanden errechnet wird. Es erlaubt Ihnen, den ungefähre endgültigen Durchschnitt zu kennen. Sie wird auf eine vom Anwender in den Anwendungseinstellungen gewählte Weise berechnet. Es wird empfohlen, die entsprechende Option zu wählen. Das liegt daran, dass die Berechnung der Schuldurchschnitte unterschiedlich ist. Wenn Ihre Schule den Durchschnitt der Fächer auf der Vulcan-Seite angibt, lädt die Anwendung diese Fächer herunter und berechnet nicht den Durchschnitt. Dies kann geändert werden, indem die Berechnung des Durchschnitts in den Anwendungseinstellungen erzwungen wird. \n\n<b>Durchschnitt der Noten nur aus dem ausgewählten Semester </b>:\n1. Berechnung des gewichteten Durchschnitts für jedes Fach in einem bestimmten Semester\n2. Addition der berechneten Durchschnittswerte\n3. Berechnung des arithmetischen Mittels der summierten Durchschnitte\n<b>Durchschnitt der Durchschnitte aus beiden Semestern</b>:\n1. Berechnung des gewichteten Durchschnitts für jedes Fach in Semester 1 und 2\n2. Berechnung des arithmetischen Mittels der berechneten Durchschnitte für Semester 1 und 2 für jedes Fach. \n3. Hinzufügen von berechneten Durchschnittswerten\n4. Berechnung des arithmetischen Mittels der summierten Durchschnitte\n<b>Durchschnitt der Noten aus dem ganzen Jahr:</b>\n1. Berechnung des gewichteten Jahresdurchschnitts für jedes Fach. Der Abschlussdurchschnitt im 1. Semester ist irrelevant. \n2. Addition der berechneten Durchschnittswerte\n3. Berechnung des arithmetischen Mittels der summierten Mittelwerte</string>
<string name="grade_summary_final_average_help_dialog_title">Wie funktioniert der endgültige Durchschnitt?</string> <string name="grade_summary_final_average_help_dialog_title">Wie funktioniert der endgültige Durchschnitt?</string>
<string name="grade_summary_final_average_help_dialog_message">Der Final Average ist das arithmetische Mittel, das aus allen derzeit verfügbaren Abschlussnoten des jeweiligen Semesters berechnet wird. \n\nDas Berechnungsschema besteht aus folgenden Schritten:\n1. Zusammenfassung der von den Lehrern gegebenen Abschlussnoten\n2. Division durch die Anzahl der Fächer, die bereits bewertet wurden</string> <string name="grade_summary_final_average_help_dialog_message">Der Final Average ist das arithmetische Mittel, das aus allen derzeit verfügbaren Abschlussnoten des jeweiligen Semesters berechnet wird. \n\nDas Berechnungsschema besteht aus folgenden Schritten:\n1. Zusammenfassung der von den Lehrern gegebenen Abschlussnoten\n2. Division durch die Anzahl der Fächer, die bereits bewertet wurden</string>
<string name="grade_summary_final_average">Finaler Durchschnitt</string> <string name="grade_summary_final_average">Finaler Durchschnitt</string>
@ -260,6 +263,10 @@
<string name="message_move_to_trash">In Papierkorb verschieben</string> <string name="message_move_to_trash">In Papierkorb verschieben</string>
<string name="message_delete_forever">Dauerhaft löschen</string> <string name="message_delete_forever">Dauerhaft löschen</string>
<string name="message_delete_success">Nachricht erfolgreich gelöscht</string> <string name="message_delete_success">Nachricht erfolgreich gelöscht</string>
<string name="message_mailbox_type_student">schüler</string>
<string name="message_mailbox_type_parent">Eltern</string>
<string name="message_mailbox_type_guardian">Betreuer</string>
<string name="message_mailbox_type_employee">Mitarbeiter</string>
<string name="message_share">Teilen</string> <string name="message_share">Teilen</string>
<string name="message_print">Drucken</string> <string name="message_print">Drucken</string>
<string name="message_subject">Thema</string> <string name="message_subject">Thema</string>
@ -271,7 +278,6 @@
<string name="message_chip_only_unread">Nur ungelesen</string> <string name="message_chip_only_unread">Nur ungelesen</string>
<string name="message_chip_only_with_attachments">Nur mit Anhängen</string> <string name="message_chip_only_with_attachments">Nur mit Anhängen</string>
<string name="message_read">Lesen: %s</string> <string name="message_read">Lesen: %s</string>
<string name="message_read_by">Lesen von: %1$d von %2$d Personen</string>
<plurals name="message_number_item"> <plurals name="message_number_item">
<item quantity="one">%1$d Nachricht</item> <item quantity="one">%1$d Nachricht</item>
<item quantity="other">%1$d Nachrichten</item> <item quantity="other">%1$d Nachrichten</item>
@ -633,6 +639,10 @@
<string name="pref_ads_privacy_link">Datenschutzerklärung</string> <string name="pref_ads_privacy_link">Datenschutzerklärung</string>
<string name="pref_ads_loading">Anzeige wird geladen</string> <string name="pref_ads_loading">Anzeige wird geladen</string>
<string name="pref_ads_once_per_visit">Vielen Dank für Ihre Unterstützung, kommen Sie später wieder für weitere Anzeigen</string> <string name="pref_ads_once_per_visit">Vielen Dank für Ihre Unterstützung, kommen Sie später wieder für weitere Anzeigen</string>
<string name="pref_ads_consent_title">Können wir Ihre Daten zur Anzeige von Werbung verwenden?</string>
<string name="pref_ads_consent_description">Sie können Ihre Wahl jederzeit in den App-Einstellungen ändern. Wir verwenden Ihre Daten, um auf Sie zugeschnittene Anzeigen anzuzeigen oder unter Verwendung weniger Ihrer Daten nicht personalisierte Werbung anzuzeigen. Bitte lesen Sie unsere Datenschutzerklärung für Details</string>
<string name="pref_ads_summary_personalized">Personalisierte Werbung</string>
<string name="pref_ads_summary_non_personalized">keine personalisierte Werbung</string>
<string name="pref_settings_advanced_title">Erweitert</string> <string name="pref_settings_advanced_title">Erweitert</string>
<string name="pref_settings_appearance_title">Aussehen &amp; Verhalten</string> <string name="pref_settings_appearance_title">Aussehen &amp; Verhalten</string>
<string name="pref_settings_notifications_title">Benachrichtigungen</string> <string name="pref_settings_notifications_title">Benachrichtigungen</string>

View File

@ -34,6 +34,7 @@
<string-array name="grade_sorting_mode_entries"> <string-array name="grade_sorting_mode_entries">
<item>Alfabetycznie</item> <item>Alfabetycznie</item>
<item>Według daty</item> <item>Według daty</item>
<item>Według średniej</item>
</string-array> </string-array>
<string-array name="grade_color_scheme_entries"> <string-array name="grade_color_scheme_entries">
<item>Dzienniczek+</item> <item>Dzienniczek+</item>

View File

@ -77,6 +77,9 @@
<string name="main_log_in">Zaloguj się</string> <string name="main_log_in">Zaloguj się</string>
<string name="main_session_expired">Sesja wygasła</string> <string name="main_session_expired">Sesja wygasła</string>
<string name="main_session_relogin">Sesja wygasła, zaloguj się ponownie</string> <string name="main_session_relogin">Sesja wygasła, zaloguj się ponownie</string>
<string name="main_support_title">Wparcie aplikacji</string>
<string name="main_support_description">Podoba Ci się ta aplikacja? Wspieraj jej rozwój poprzez włączenie nieinwazyjnych reklam, które możesz wyłączyć w dowolnym momencie</string>
<string name="main_support_positive">Włącz reklamy</string>
<!--Grade--> <!--Grade-->
<string name="grade_header">Ocena</string> <string name="grade_header">Ocena</string>
<string name="grade_semester">Semestr %d</string> <string name="grade_semester">Semestr %d</string>
@ -94,7 +97,7 @@
<string name="grade_summary_predicted_grade">Przewidywana ocena</string> <string name="grade_summary_predicted_grade">Przewidywana ocena</string>
<string name="grade_summary_calculated_average">Obliczona średnia</string> <string name="grade_summary_calculated_average">Obliczona średnia</string>
<string name="grade_summary_calculated_average_help_dialog_title">Jak działa obliczona średnia?</string> <string name="grade_summary_calculated_average_help_dialog_title">Jak działa obliczona średnia?</string>
<string name="grade_summary_calculated_average_help_dialog_message">Obliczona średnia jest średnią arytmetyczną obliczoną ze średnich przedmiotów. Pozwala ona na poznanie przybliżonej średniej końcowej. Jest obliczana w sposób wybrany przez użytkownika w ustawieniach aplikacji. Zaleca się wybranie odpowiedniej opcji. Dzieje się tak dlatego, że obliczanie średnich w szkołach różni się. Dodatkowo, jeśli twoja szkoła ma włączone średnie przedmiotów na stronie dziennika Vulcan, aplikacja pobiera je i ich nie oblicza. Można to zmienić, wymuszając obliczanie średniej w ustawieniach aplikacji.\n\n<b>Średnia ocen tylko z wybranego semestru</b>:\n1. Obliczanie średniej arytmetycznej każdego przedmiotu w danym semestrze\n2. Zsumowanie obliczonych średnich\n3. Obliczanie średniej arytmetycznej zsumowanych średnich\n\n<b>Średnia ze średnich z obu semestrów</b>:\n1.Obliczanie średniej arytmetycznej każdego przedmiotu w semestrze 1 i 2\n2. Obliczanie średniej arytmetycznej obliczonych średnich w semetrze 1 i 2 każdego przedmiotu.\n3. Zsumowanie obliczonych średnich\n4. Obliczanie średniej arytmetycznej zsumowanych średnich\n\n<b>Średnia wszystkich ocen z całego roku:</b>\n1. Obliczanie średniej arytmetycznej z każdego przedmiotu w ciągu całego roku. Końcowa ocena w 1 semestrze jest bez znaczenia.\n3. Zsumowanie obliczonych średnich\n4. Obliczanie średniej arytmetycznej z zsumowanych średnich</string> <string name="grade_summary_calculated_average_help_dialog_message">Obliczona średnia jest średnią arytmetyczną obliczoną ze średnich przedmiotów. Pozwala ona na poznanie przybliżonej średniej końcowej. Jest obliczana w sposób wybrany przez użytkownika w ustawieniach aplikacji. Zaleca się wybranie odpowiedniej opcji. Dzieje się tak dlatego, że obliczanie średnich w szkołach różni się. Dodatkowo, jeśli twoja szkoła ma włączone średnie przedmiotów na stronie dziennika Vulcan, aplikacja pobiera je i ich nie oblicza. Można to zmienić, wymuszając obliczanie średniej w ustawieniach aplikacji.\n\n<b>Średnia ocen tylko z wybranego semestru</b>:\n1. Obliczanie średniej arytmetycznej każdego przedmiotu w danym semestrze\n2. Zsumowanie obliczonych średnich\n3. Obliczanie średniej arytmetycznej zsumowanych średnich\n\n<b>Średnia ze średnich z obu semestrów</b>:\n1.Obliczanie średniej arytmetycznej każdego przedmiotu w semestrze 1 i 2\n2. Obliczanie średniej arytmetycznej obliczonych średnich w semestrze 1 i 2 każdego przedmiotu.\n3. Zsumowanie obliczonych średnich\n4. Obliczanie średniej arytmetycznej zsumowanych średnich\n\n<b>Średnia wszystkich ocen z całego roku:</b>\n1. Obliczanie średniej arytmetycznej z każdego przedmiotu w ciągu całego roku. Końcowa ocena w 1 semestrze jest bez znaczenia.\n2. Zsumowanie obliczonych średnich\n3. Obliczanie średniej arytmetycznej z zsumowanych średnich</string>
<string name="grade_summary_final_average_help_dialog_title">Jak działa końcowa średnia?</string> <string name="grade_summary_final_average_help_dialog_title">Jak działa końcowa średnia?</string>
<string name="grade_summary_final_average_help_dialog_message">Średnią końcową jest średnia arytmetyczna obliczona na podstawie wszystkich obecnie dostępnych ocen końcowych w danym semestrze.\n\nSchemat obliczeń składa się z następujących kroków:\n1. Sumowanie końcowych ocen wpisanych przez nauczycieli\n2. Dzielenie przez liczbę przedmiotów, z których oceny zostały już wystawione</string> <string name="grade_summary_final_average_help_dialog_message">Średnią końcową jest średnia arytmetyczna obliczona na podstawie wszystkich obecnie dostępnych ocen końcowych w danym semestrze.\n\nSchemat obliczeń składa się z następujących kroków:\n1. Sumowanie końcowych ocen wpisanych przez nauczycieli\n2. Dzielenie przez liczbę przedmiotów, z których oceny zostały już wystawione</string>
<string name="grade_summary_final_average">Końcowa średnia</string> <string name="grade_summary_final_average">Końcowa średnia</string>
@ -294,6 +297,10 @@
<string name="message_move_to_trash">Przenieś do kosza</string> <string name="message_move_to_trash">Przenieś do kosza</string>
<string name="message_delete_forever">Usuń trwale</string> <string name="message_delete_forever">Usuń trwale</string>
<string name="message_delete_success">Wiadomość usunięta pomyślnie</string> <string name="message_delete_success">Wiadomość usunięta pomyślnie</string>
<string name="message_mailbox_type_student">uczeń</string>
<string name="message_mailbox_type_parent">rodzic</string>
<string name="message_mailbox_type_guardian">opiekun</string>
<string name="message_mailbox_type_employee">pracownik</string>
<string name="message_share">Udostępnij</string> <string name="message_share">Udostępnij</string>
<string name="message_print">Drukuj</string> <string name="message_print">Drukuj</string>
<string name="message_subject">Temat</string> <string name="message_subject">Temat</string>
@ -305,7 +312,6 @@
<string name="message_chip_only_unread">Tylko nieprzeczytane</string> <string name="message_chip_only_unread">Tylko nieprzeczytane</string>
<string name="message_chip_only_with_attachments">Tylko z załącznikami</string> <string name="message_chip_only_with_attachments">Tylko z załącznikami</string>
<string name="message_read">Przeczytana: %s</string> <string name="message_read">Przeczytana: %s</string>
<string name="message_read_by">Przeczytana przez: %1$d z %2$d osób</string>
<plurals name="message_number_item"> <plurals name="message_number_item">
<item quantity="one">%1$d wiadomość</item> <item quantity="one">%1$d wiadomość</item>
<item quantity="few">%1$d wiadomości</item> <item quantity="few">%1$d wiadomości</item>
@ -701,7 +707,7 @@
<string name="pref_notification_go_to_settings">Przejdź do ustawień</string> <string name="pref_notification_go_to_settings">Przejdź do ustawień</string>
<string name="pref_services_header">Synchronizacja</string> <string name="pref_services_header">Synchronizacja</string>
<string name="pref_services_switch">Automatyczna aktualizacja</string> <string name="pref_services_switch">Automatyczna aktualizacja</string>
<string name="pref_services_suspended">Zawieszona na wakacjach</string> <string name="pref_services_suspended">Wstrzymana podczas wakacji</string>
<string name="pref_services_interval">Interwał aktualizacji</string> <string name="pref_services_interval">Interwał aktualizacji</string>
<string name="pref_services_wifi">Tylko WiFi</string> <string name="pref_services_wifi">Tylko WiFi</string>
<string name="pref_services_force_sync">Synchronizuj teraz</string> <string name="pref_services_force_sync">Synchronizuj teraz</string>
@ -721,6 +727,10 @@
<string name="pref_ads_privacy_link">Polityka prywatności</string> <string name="pref_ads_privacy_link">Polityka prywatności</string>
<string name="pref_ads_loading">Ładowanie reklamy</string> <string name="pref_ads_loading">Ładowanie reklamy</string>
<string name="pref_ads_once_per_visit">Dziękujemy za wsparcie, wróć później po więcej reklam</string> <string name="pref_ads_once_per_visit">Dziękujemy za wsparcie, wróć później po więcej reklam</string>
<string name="pref_ads_consent_title">Czy możemy używać Twoich danych do wyświetlania reklam?</string>
<string name="pref_ads_consent_description">Możesz zmienić swój wybór w dowolnym momencie w ustawieniach aplikacji. Możemy wykorzystać Twoje dane do wyświetlania reklam dostosowanych do Ciebie lub, przy użyciu mniejszej ilości danych, wyświetlić niepersonalizowane reklamy. Zobacz naszą Politykę Prywatności, aby uzyskać więcej informacji</string>
<string name="pref_ads_summary_personalized">Spersonalizowane reklamy</string>
<string name="pref_ads_summary_non_personalized">Niespersonalizowane reklamy</string>
<string name="pref_settings_advanced_title">Zaawansowane</string> <string name="pref_settings_advanced_title">Zaawansowane</string>
<string name="pref_settings_appearance_title">Wygląd i zachowanie</string> <string name="pref_settings_appearance_title">Wygląd i zachowanie</string>
<string name="pref_settings_notifications_title">Powiadomienia</string> <string name="pref_settings_notifications_title">Powiadomienia</string>

View File

@ -34,6 +34,7 @@
<string-array name="grade_sorting_mode_entries"> <string-array name="grade_sorting_mode_entries">
<item>В алфавитном порядке</item> <item>В алфавитном порядке</item>
<item>По дате</item> <item>По дате</item>
<item>По средней</item>
</string-array> </string-array>
<string-array name="grade_color_scheme_entries"> <string-array name="grade_color_scheme_entries">
<item>Dzienniczek+</item> <item>Dzienniczek+</item>

Some files were not shown because too many files have changed in this diff Show More