1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2025-02-23 07:34:44 +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:
release:
@ -7,16 +7,17 @@ on:
jobs:
deploy-google-play:
name: Deploy to google play
name: Google Play
runs-on: ubuntu-latest
timeout-minutes: 10
environment: google-play
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
- uses: actions/checkout@v3
- uses: actions/setup-java@v2
with:
distribution: 'zulu'
java-version: 11
- uses: actions/cache@v2
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
@ -41,16 +42,17 @@ jobs:
run: ./gradlew publishPlayReleaseApps -PenableFirebase --stacktrace;
deploy-app-gallery:
name: Deploy to AppGallery
name: AppGallery
runs-on: ubuntu-latest
timeout-minutes: 10
environment: app-gallery
steps:
- uses: actions/checkout@v2
- uses: actions/setup-java@v1
- uses: actions/checkout@v3
- uses: actions/setup-java@v2
with:
distribution: 'zulu'
java-version: 11
- uses: actions/cache@v2
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches

View File

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

View File

@ -8,18 +8,20 @@ on:
branches: [ master, develop ]
jobs:
unit-tests:
name: Unit tests
tests-fdroid:
name: F-Droid
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: fkirc/skip-duplicate-actions@master
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: gradle/wrapper-validation-action@v1
- uses: actions/setup-java@v1
- uses: actions/setup-java@v2
with:
distribution: 'zulu'
java-version: 11
- uses: actions/cache@v2
- uses: actions/cache@v3
with:
path: |
~/.gradle/caches
@ -29,6 +31,58 @@ jobs:
run: |
./gradlew testFdroidDebugUnitTest --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:
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"
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

View File

@ -15,33 +15,35 @@ apply from: 'sonarqube.gradle'
apply from: 'hooks.gradle'
android {
compileSdkVersion 31
namespace 'io.github.wulkanowy'
compileSdkVersion 32
defaultConfig {
applicationId "io.github.wulkanowy"
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21
targetSdkVersion 31
versionCode 108
versionName "1.6.4"
targetSdkVersion 32
versionCode 109
versionName "1.7.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy"
manifestPlaceholders = [
firebase_enabled: project.hasProperty("enableFirebase"),
admob_project_id: ""
firebase_enabled: project.hasProperty("enableFirebase"),
admob_project_id: ""
]
javaCompileOptions {
annotationProcessorOptions {
arguments += [
"room.schemaLocation": "$projectDir/schemas".toString(),
"room.incremental" : "true"
"room.schemaLocation": "$projectDir/schemas".toString(),
"room.incremental" : "true"
]
}
}
buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null"
buildConfigField "String", "DASHBOARD_TILE_AD_ID", "null"
if (System.env.SET_BUILD_TIMESTAMP) {
buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis())
@ -94,10 +96,12 @@ android {
play {
dimension "platform"
manifestPlaceholders = [
install_channel : "Google Play",
admob_project_id: System.getenv("ADMOB_PROJECT_ID") ?: "ca-app-pub-3940256099942544~3347511713"
install_channel : "Google Play",
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", "DASHBOARD_TILE_AD_ID", "\"${System.getenv("DASHBOARD_TILE_AD_ID") ?: "ca-app-pub-3940256099942544/6300978111"}\""
}
fdroid {
@ -122,6 +126,8 @@ android {
testOptions.unitTests {
includeAndroidResources = true
// workaround HMS test errors https://github.com/robolectric/robolectric/issues/2750
all { jvmArgs '-noverify' }
}
compileOptions {
@ -136,8 +142,10 @@ android {
}
packagingOptions {
exclude 'META-INF/library_release.kotlin_module'
exclude 'META-INF/library-core_release.kotlin_module'
resources {
excludes += ['META-INF/library_release.kotlin_module',
'META-INF/library-core_release.kotlin_module']
}
}
aboutLibraries {
@ -153,8 +161,8 @@ play {
defaultToAppBundles = false
track = 'production'
releaseStatus = com.github.triplet.gradle.androidpublisher.ReleaseStatus.IN_PROGRESS
userFraction = 0.50d
updatePriority = 3
userFraction = 0.05d
updatePriority = 5
enabled.set(false)
}
@ -171,34 +179,34 @@ huaweiPublish {
ext {
work_manager = "2.7.1"
android_hilt = "1.0.0"
room = "2.4.2"
room = "2.4.3"
chucker = "3.5.2"
mockk = "1.12.4"
coroutines = "1.6.1"
mockk = "1.12.5"
coroutines = "1.6.4"
}
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-coroutines-android:$coroutines"
implementation "androidx.core:core-ktx:1.7.0"
implementation 'androidx.core:core-splashscreen:1.0.0-beta02'
implementation "androidx.activity:activity-ktx:1.4.0"
implementation "androidx.appcompat:appcompat:1.4.1"
implementation "androidx.fragment:fragment-ktx:1.4.1"
implementation "androidx.annotation:annotation:1.3.0"
implementation "androidx.core:core-ktx:1.8.0"
implementation 'androidx.core:core-splashscreen:1.0.0'
implementation "androidx.activity:activity-ktx:1.5.1"
implementation "androidx.appcompat:appcompat:1.5.0"
implementation "androidx.fragment:fragment-ktx:1.5.2"
implementation "androidx.annotation:annotation:1.4.0"
implementation "androidx.preference:preference-ktx:1.2.0"
implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation "androidx.viewpager2:viewpager2:1.1.0-beta01"
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 "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.PhilJay:MPAndroidChart:v3.1.0"
implementation 'com.github.lopspower:CircularImageView:4.2.0'
@ -206,7 +214,7 @@ dependencies {
implementation "androidx.work:work-runtime-ktx:$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-ktx:$room"
@ -222,27 +230,27 @@ dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.9.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 "at.favre.lib:slf4j-timber:1.0.1"
implementation 'com.github.bastienpaulfr:Treessence:1.0.5'
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 '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-messaging:'
playImplementation 'com.google.firebase:firebase-crashlytics:'
playImplementation 'com.google.android.play:core:1.10.3'
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.agconnect:agconnect-crash:1.6.6.200'
hmsImplementation 'com.huawei.hms:hianalytics:6.7.0.300'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.7.1.300'
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":{
"backurl":"connect-dre.dbankcloud.cn",
"url":"connect-dre.hispace.hicloud.com"
},
"client":{
"cp_id":"890048000024105546",
"product_id":"",
"client_id":"",
"client_secret":"",
"app_id":"101440411",
"package_name":"io.github.wulkanowy.dev",
"api_key":""
},
"service":{
"analytics":{
"collector_url":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
"resource_id":"p1",
"channel_id":""
},
"agcgw": {
"backurl": "connect-dre.hispace.hicloud.com",
"url": "connect-dre.dbankcloud.cn",
"websocketbackurl": "connect-ws-dre.hispace.dbankcloud.com",
"websocketurl": "connect-ws-dre.hispace.dbankcloud.cn"
},
"agcgw_all": {
"CN": "connect-drcn.dbankcloud.cn",
"CN_back": "connect-drcn.hispace.hicloud.com",
"DE": "connect-dre.dbankcloud.cn",
"DE_back": "connect-dre.hispace.hicloud.com",
"RU": "connect-drru.hispace.dbankcloud.ru",
"RU_back": "connect-drru.hispace.dbankcloud.cn",
"SG": "connect-dra.dbankcloud.cn",
"SG_back": "connect-dra.hispace.hicloud.com"
},
"websocketgw_all": {
"CN": "connect-ws-drcn.hispace.dbankcloud.cn",
"CN_back": "connect-ws-drcn.hispace.dbankcloud.com",
"DE": "connect-ws-dre.hispace.dbankcloud.cn",
"DE_back": "connect-ws-dre.hispace.dbankcloud.com",
"RU": "connect-ws-drru.hispace.dbankcloud.ru",
"RU_back": "connect-ws-drru.hispace.dbankcloud.cn",
"SG": "connect-ws-dra.hispace.dbankcloud.cn",
"SG_back": "connect-ws-dra.hispace.dbankcloud.com"
},
"client": {
"cp_id": "890048000024105546",
"product_id": "736430079244736562",
"client_id": "514530959291319360",
"client_secret": "C42522DBF17D3D4BBE9D9C1783A54484B7E6844B388B7A67502D36A633A4186B",
"project_id": "736430079244736562",
"app_id": "106552551",
"api_key": "CgB6e3x9BUNiq+r8ebCHNojjjYsMT4pJSjjNDOkm9owtBb6rVI6LjnASoZBRxbjjhObcrV5gANo99fI/eKZDTbWS",
"package_name": "io.github.wulkanowy.dev"
},
"oauth_client": {
"client_id": "106552551",
"client_type": 1
},
"app_info": {
"app_id": "106552551",
"package_name": "io.github.wulkanowy.dev"
},
"service": {
"analytics": {
"collector_url": "datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
"collector_url_ru": "datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com",
"collector_url_sg": "datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn",
"collector_url_de": "datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
"collector_url_cn": "datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn",
"resource_id": "p1",
"channel_id": ""
},
"search":{
"url":"https://search-dre.cloud.huawei.com"
},
"cloudstorage":{
"storage_url":"https://ops-dre.agcstorage.link"
},
"ml":{
"mlservice_url":"ml-api-dre.ai.dbankcloud.com,ml-api-dre.ai.dbankcloud.cn"
}
},
"region":"DE",
"configuration_version":"1.0"
"cloudstorage": {
"storage_url_sg_back": "https://agc-storage-dra.cloud.huawei.asia",
"storage_url_ru_back": "https://agc-storage-drru.cloud.huawei.ru",
"storage_url_ru": "https://agc-storage-drru.cloud.huawei.ru",
"storage_url_de_back": "https://agc-storage-dre.cloud.huawei.eu",
"storage_url_de": "https://ops-dre.agcstorage.link",
"storage_url": "https://agc-storage-drcn.platform.dbankcloud.cn",
"storage_url_sg": "https://ops-dra.agcstorage.link",
"storage_url_cn_back": "https://agc-storage-drcn.cloud.huawei.com.cn",
"storage_url_cn": "https://agc-storage-drcn.platform.dbankcloud.cn"
},
"ml": {
"mlservice_url": "ml-api-dre.ai.dbankcloud.com,ml-api-dre.ai.dbankcloud.cn"
}
},
"region": "DE",
"configuration_version": "3.0",
"appInfos": [
{
"package_name": "io.github.wulkanowy.dev",
"client": {
"app_id": "106552551"
},
"app_info": {
"package_name": "io.github.wulkanowy.dev",
"app_id": "106552551"
},
"oauth_client": {
"client_type": 1,
"client_id": "106552551"
}
}
]
}

View File

@ -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"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="io.github.wulkanowy"
android:installLocation="internalOnly">
<uses-permission android:name="android.permission.INTERNET" />

View File

@ -31,10 +31,14 @@ class WulkanowyApp : Application(), Configuration.Provider {
@Inject
lateinit var analyticsHelper: AnalyticsHelper
@Inject
lateinit var adsHelper: AdsHelper
override fun onCreate() {
super.onCreate()
initializeAppLanguage()
themeManager.applyDefaultTheme()
adsHelper.initialize()
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.sdk.Sdk
import io.github.wulkanowy.utils.AppInfo
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
@ -110,7 +109,6 @@ internal class DataModule {
fun provideSharedPref(@ApplicationContext context: Context): SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context)
@OptIn(ExperimentalCoroutinesApi::class)
@Singleton
@Provides
fun provideFlowSharedPref(sharedPreferences: SharedPreferences) =
@ -197,7 +195,7 @@ internal class DataModule {
@Singleton
@Provides
fun provideReportingUnitDao(database: AppDatabase) = database.reportingUnitDao
fun provideMailboxesDao(database: AppDatabase) = database.mailboxDao
@Singleton
@Provides

View File

@ -30,7 +30,7 @@ import javax.inject.Singleton
Subject::class,
LuckyNumber::class,
CompletedLesson::class,
ReportingUnit::class,
Mailbox::class,
Recipient::class,
MobileDevice::class,
Teacher::class,
@ -55,7 +55,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 48
const val VERSION_SCHEMA = 51
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(),
@ -102,6 +102,9 @@ abstract class AppDatabase : RoomDatabase() {
Migration43(),
Migration44(),
Migration46(),
Migration49(),
Migration50(),
Migration51(),
)
fun newInstance(
@ -152,7 +155,7 @@ abstract class AppDatabase : RoomDatabase() {
abstract val completedLessonsDao: CompletedLessonsDao
abstract val reportingUnitDao: ReportingUnitDao
abstract val mailboxDao: MailboxDao
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> {
@Transaction
@Query("SELECT * FROM Messages WHERE student_id = :studentId AND message_id = :messageId")
fun loadMessageWithAttachment(studentId: Int, messageId: Int): Flow<MessageWithAttachment?>
@Query("SELECT * FROM Messages WHERE message_global_key = :messageGlobalKey")
fun loadMessageWithAttachment(messageGlobalKey: String): Flow<MessageWithAttachment?>
@Query("SELECT * FROM Messages WHERE student_id = :studentId AND folder_id = :folder ORDER BY date DESC")
fun loadAll(studentId: Int, folder: Int): Flow<List<Message>>
@Query("SELECT * FROM Messages WHERE mailbox_key = :mailboxKey AND folder_id = :folder ORDER BY date DESC")
fun loadAll(mailboxKey: String, folder: Int): Flow<List<Message>>
}

View File

@ -8,6 +8,6 @@ import kotlinx.coroutines.flow.Flow
@Dao
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>>
}

View File

@ -2,6 +2,7 @@ package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.MailboxType
import io.github.wulkanowy.data.db.entities.Recipient
import javax.inject.Singleton
@ -9,6 +10,6 @@ import javax.inject.Singleton
@Dao
interface RecipientDao : BaseDao<Recipient> {
@Query("SELECT * FROM Recipients WHERE student_id = :studentId AND unit_id = :unitId AND role = :role")
suspend fun loadAll(studentId: Int, unitId: Int, role: Int): List<Recipient>
@Query("SELECT * FROM Recipients WHERE type = :type AND studentMailboxGlobalKey = :studentMailboxGlobalKey")
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
interface SchoolAnnouncementDao : BaseDao<SchoolAnnouncement> {
@Query("SELECT * FROM SchoolAnnouncements WHERE student_id = :studentId ORDER BY date DESC")
fun loadAll(studentId: Int): Flow<List<SchoolAnnouncement>>
@Query("SELECT * FROM SchoolAnnouncements WHERE user_login_id = :userLoginId ORDER BY date DESC")
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")
data class Message(
@ColumnInfo(name = "student_id")
val studentId: Long,
@ColumnInfo(name = "message_global_key")
val messageGlobalKey: String,
@ColumnInfo(name = "real_id")
val realId: Int,
@ColumnInfo(name = "mailbox_key")
val mailboxKey: String,
@ColumnInfo(name = "message_id")
val messageId: Int,
@ColumnInfo(name = "sender_name")
val sender: String,
@ColumnInfo(name = "sender_id")
val senderId: Int,
@ColumnInfo(name = "recipient_name")
val recipient: String,
val correspondents: String,
val subject: String,
@ -36,8 +29,6 @@ data class Message(
var unread: Boolean,
val removed: Boolean,
@ColumnInfo(name = "has_attachments")
val hasAttachments: Boolean
) : Serializable {
@ -48,11 +39,7 @@ data class Message(
@ColumnInfo(name = "is_notified")
var isNotified: Boolean = true
@ColumnInfo(name = "unread_by")
var unreadBy: Int = 0
@ColumnInfo(name = "read_by")
var readBy: Int = 0
var content: String = ""
var sender: String? = null
var recipients: String? = null
}

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable
@ -8,32 +7,16 @@ import java.io.Serializable
@kotlinx.serialization.Serializable
@Entity(tableName = "Recipients")
data class Recipient(
@ColumnInfo(name = "student_id")
val studentId: Int,
@ColumnInfo(name = "real_id")
val realId: String,
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
val mailboxGlobalKey: String,
val studentMailboxGlobalKey: String,
val fullName: String,
val userName: String,
val schoolShortName: String,
val type: MailboxType,
) : Serializable {
@PrimaryKey(autoGenerate = true)
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")
data class SchoolAnnouncement(
@ColumnInfo(name = "student_id")
val studentId: Int,
@ColumnInfo(name = "user_login_id")
val userLoginId: Int,
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) {
ALPHABETIC("alphabetic"),
DATE("date");
DATE("date"),
AVERAGE("average");
companion object {
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 {
SchoolAnnouncement(
studentId = student.userLoginId,
userLoginId = student.userLoginId,
date = it.date,
subject = it.subject,
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
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.Student
import java.time.Instant
import io.github.wulkanowy.data.db.entities.*
import io.github.wulkanowy.sdk.pojo.MailboxType
import io.github.wulkanowy.sdk.pojo.Message as SdkMessage
import io.github.wulkanowy.sdk.pojo.MessageAttachment as SdkMessageAttachment
import io.github.wulkanowy.sdk.pojo.Recipient as SdkRecipient
fun List<SdkMessage>.mapToEntities(student: Student) = map {
fun List<SdkMessage>.mapToEntities(mailbox: Mailbox) = map {
Message(
studentId = student.id,
realId = it.id ?: 0,
messageId = it.messageId ?: 0,
sender = it.sender?.name.orEmpty(),
senderId = it.sender?.loginId ?: 0,
recipient = it.recipients.singleOrNull()?.name ?: "Wielu adresatów",
messageGlobalKey = it.globalKey,
mailboxKey = mailbox.globalKey,
messageId = it.id,
correspondents = it.correspondents,
subject = it.subject.trim(),
date = it.dateZoned?.toInstant() ?: Instant.now(),
date = it.dateZoned.toInstant(),
folderId = it.folderId,
unread = it.unread ?: false,
removed = it.removed,
unread = it.unread,
hasAttachments = it.hasAttachments
).apply {
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(
realId = it.id,
messageId = it.messageId,
oneDriveId = it.oneDriveId,
messageGlobalKey = messageGlobalKey,
realId = it.url.hashCode(),
url = it.url,
filename = it.filename
)
@ -42,12 +33,11 @@ fun List<SdkMessageAttachment>.mapToEntities() = map {
fun List<Recipient>.mapFromEntities() = map {
SdkRecipient(
id = it.realId,
name = it.realName,
loginId = it.loginId,
reportingUnitId = it.unitId,
role = it.role,
hash = it.hash,
shortName = it.name
fullName = it.fullName,
userName = it.userName,
studentName = it.userName,
mailboxGlobalKey = it.mailboxGlobalKey,
schoolNameShort = it.schoolShortName,
type = MailboxType.valueOf(it.type.name),
)
}

View File

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

View File

@ -1,17 +1,16 @@
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.sdk.pojo.Recipient as SdkRecipient
fun List<SdkRecipient>.mapToEntities(userLoginId: Int) = map {
fun List<SdkRecipient>.mapToEntities(studentMailboxGlobalKey: String) = map {
Recipient(
studentId = userLoginId,
realId = it.id,
realName = it.name,
name = it.shortName,
hash = it.hash,
loginId = it.loginId,
role = it.role,
unitId = it.reportingUnitId ?: 0
mailboxGlobalKey = it.mailboxGlobalKey,
fullName = it.fullName,
userName = it.userName,
studentMailboxGlobalKey = studentMailboxGlobalKey,
schoolShortName = it.schoolNameShort,
type = MailboxType.valueOf(it.type.name),
)
}

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.enums.MessageFolder
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.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.data.pojos.MessageDraft
import io.github.wulkanowy.sdk.Sdk
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.getRefreshKey
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.uniqueSubtract
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import timber.log.Timber
import java.time.LocalDateTime.now
import javax.inject.Inject
import javax.inject.Singleton
@ -49,7 +49,7 @@ class MessageRepository @Inject constructor(
@Suppress("UNUSED_PARAMETER")
fun getMessages(
student: Student,
semester: Semester,
mailbox: Mailbox,
folder: MessageFolder,
forceRefresh: Boolean,
notify: Boolean = false,
@ -62,42 +62,20 @@ class MessageRepository @Inject constructor(
)
it.isEmpty() || forceRefresh || isExpired
},
query = { messagesDb.loadAll(student.id.toInt(), folder.id) },
query = { messagesDb.loadAll(mailbox.globalKey, folder.id) },
fetch = {
sdk.init(student).getMessages(Folder.valueOf(folder.name), now().minusMonths(3), now())
.mapToEntities(student)
sdk.init(student).getMessages(Folder.valueOf(folder.name)).mapToEntities(mailbox)
},
saveFetchResult = { old, new ->
messagesDb.deleteAll(old uniqueSubtract new)
messagesDb.insertAll((new uniqueSubtract old).onEach {
it.isNotified = !notify
})
messagesDb.updateAll(getMessagesWithReadByChange(old, new, !notify))
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(
student: Student,
message: Message,
@ -106,34 +84,34 @@ class MessageRepository @Inject constructor(
isResultEmpty = { it?.message?.content.isNullOrBlank() },
shouldFetch = {
checkNotNull(it) { "This message no longer exist!" }
Timber.d("Message content in db empty: ${it.message.content.isEmpty()}")
it.message.unread || it.message.content.isEmpty()
Timber.d("Message content in db empty: ${it.message.content.isBlank()}")
it.message.unread || it.message.content.isBlank()
},
query = { messagesDb.loadMessageWithAttachment(student.id.toInt(), message.messageId) },
query = { messagesDb.loadMessageWithAttachment(message.messageGlobalKey) },
fetch = {
sdk.init(student).getMessageDetails(
messageId = it!!.message.messageId,
folderId = message.folderId,
read = markAsRead,
id = message.realId
).let { details ->
details.content to details.attachments.mapToEntities()
}
sdk.init(student).getMessageDetails(it!!.message.messageGlobalKey)
},
saveFetchResult = { old, (downloadedMessage, attachments) ->
saveFetchResult = { old, new ->
checkNotNull(old) { "Fetched message no longer exist!" }
messagesDb.updateAll(listOf(old.message.apply {
id = old.message.id
unread = !markAsRead
content = content.ifBlank { downloadedMessage }
}))
messageAttachmentDao.insertAttachments(attachments)
messagesDb.updateAll(
listOf(old.message.apply {
id = message.id
unread = !markAsRead
sender = new.sender
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")
}
)
fun getMessagesFromDatabase(student: Student): Flow<List<Message>> {
return messagesDb.loadAll(student.id.toInt(), RECEIVED.id)
fun getMessagesFromDatabase(mailbox: Mailbox): Flow<List<Message>> {
return messagesDb.loadAll(mailbox.globalKey, RECEIVED.id)
}
suspend fun updateMessages(messages: List<Message>) {
@ -145,32 +123,48 @@ class MessageRepository @Inject constructor(
subject: String,
content: String,
recipients: List<Recipient>,
): SentMessage = sdk.init(student).sendMessage(
subject = subject,
content = content,
recipients = recipients.mapFromEntities()
)
mailboxId: String,
) {
sdk.init(student).sendMessage(
subject = subject,
content = content,
recipients = recipients.mapFromEntities(),
mailboxId = mailboxId,
)
}
suspend fun deleteMessages(student: Student, messages: List<Message>) {
val folderId = messages.first().folderId
val isDeleted = sdk.init(student)
.deleteMessages(messages = messages.map { it.messageId }, folderId = folderId)
suspend fun deleteMessages(student: Student, mailbox: Mailbox, messages: List<Message>) {
val firstMessage = messages.first()
sdk.init(student).deleteMessages(
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 {
it.copy(folderId = MessageFolder.TRASHED.id)
it.copy(folderId = TRASHED.id)
.apply {
id = it.id
content = it.content
sender = it.sender
recipients = it.recipients
}
}
messagesDb.updateAll(deletedMessages)
} else messagesDb.deleteAll(messages)
getMessages(
student = student,
mailbox = mailbox,
folder = TRASHED,
forceRefresh = true,
).first()
}
suspend fun deleteMessage(student: Student, message: Message) =
deleteMessages(student, listOf(message))
suspend fun deleteMessage(student: Student, mailbox: Mailbox, message: Message) {
deleteMessages(student, mailbox, listOf(message))
}
var draftMessage: MessageDraft?
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))
it.isEmpty() || forceRefresh || isExpired
},
query = { mobileDb.loadAll(student.userLoginId.takeIf { it != 0 } ?: student.studentId) },
query = { mobileDb.loadAll(student.userLoginId) },
fetch = {
sdk.init(student)
.switchDiary(semester.diaryId, semester.kindergartenDiaryId, semester.schoolYear)
.getRegisteredDevices()
.mapToEntities(semester)
.mapToEntities(student)
},
saveFetchResult = { old, new ->
mobileDb.deleteAll(old uniqueSubtract new)

View File

@ -222,19 +222,31 @@ class PreferencesRepository @Inject constructor(
get() = selectedDashboardTilesPreference.asFlow()
.map { set ->
set.map { DashboardItem.Tile.valueOf(it) }
.plus(DashboardItem.Tile.ACCOUNT)
.plus(DashboardItem.Tile.ADMIN_MESSAGE)
.plus(
listOfNotNull(
DashboardItem.Tile.ACCOUNT,
DashboardItem.Tile.ADMIN_MESSAGE,
DashboardItem.Tile.ADS.takeIf { isAdsEnabled }
)
)
.toSet()
}
var selectedDashboardTiles: Set<DashboardItem.Tile>
get() = selectedDashboardTilesPreference.get()
.map { DashboardItem.Tile.valueOf(it) }
.plus(DashboardItem.Tile.ACCOUNT)
.plus(DashboardItem.Tile.ADMIN_MESSAGE)
.plus(
listOfNotNull(
DashboardItem.Tile.ACCOUNT,
DashboardItem.Tile.ADMIN_MESSAGE,
DashboardItem.Tile.ADS.takeIf { isAdsEnabled }
)
)
.toSet()
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 }
.toSet()
@ -271,7 +283,38 @@ class PreferencesRepository @Inject constructor(
var isAppReviewDone: Boolean
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)
@ -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_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"
}
}

View File

@ -1,10 +1,7 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.db.dao.RecipientDao
import io.github.wulkanowy.data.db.entities.Message
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.db.entities.*
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AutoRefreshHelper
@ -23,9 +20,10 @@ class RecipientRepository @Inject constructor(
private val cacheKey = "recipient"
suspend fun refreshRecipients(student: Student, unit: ReportingUnit, role: Int) {
val new = sdk.init(student).getRecipients(unit.unitId, role).mapToEntities(unit.studentId)
val old = recipientDb.loadAll(unit.studentId, unit.unitId, role)
suspend fun refreshRecipients(student: Student, mailbox: Mailbox, type: MailboxType) {
val new = sdk.init(student).getRecipients(mailbox.globalKey)
.mapToEntities(mailbox.globalKey)
val old = recipientDb.loadAll(type, mailbox.globalKey)
recipientDb.deleteAll(old uniqueSubtract new)
recipientDb.insertAll(new uniqueSubtract old)
@ -33,18 +31,27 @@ class RecipientRepository @Inject constructor(
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, student))
}
suspend fun getRecipients(student: Student, unit: ReportingUnit, role: Int): List<Recipient> {
val cached = recipientDb.loadAll(unit.studentId, unit.unitId, role)
suspend fun getRecipients(
student: Student,
mailbox: Mailbox,
type: MailboxType
): List<Recipient> {
val cached = recipientDb.loadAll(type, mailbox.globalKey)
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
return if (cached.isEmpty() || isExpired) {
refreshRecipients(student, unit, role)
recipientDb.loadAll(unit.studentId, unit.unitId, role)
refreshRecipients(student, mailbox, type)
recipientDb.loadAll(type, mailbox.globalKey)
} else cached
}
suspend fun getMessageRecipients(student: Student, message: Message): List<Recipient> {
return sdk.init(student).getMessageRecipients(message.messageId, message.senderId)
.mapToEntities(student.studentId)
}
suspend fun getMessageSender(
student: Student,
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(
student: Student,
forceRefresh: Boolean, notify: Boolean = false
forceRefresh: Boolean,
notify: Boolean = false
) = networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = { it.isEmpty() },
@ -37,7 +38,7 @@ class SchoolAnnouncementRepository @Inject constructor(
it.isEmpty() || forceRefresh || isExpired
},
query = {
schoolAnnouncementDb.loadAll(student.studentId)
schoolAnnouncementDb.loadAll(student.userLoginId)
},
fetch = {
sdk.init(student)
@ -56,7 +57,7 @@ class SchoolAnnouncementRepository @Inject constructor(
)
fun getSchoolAnnouncementFromDatabase(student: Student): Flow<List<SchoolAnnouncement>> {
return schoolAnnouncementDb.loadAll(student.studentId)
return schoolAnnouncementDb.loadAll(student.userLoginId)
}
suspend fun updateSchoolAnnouncement(schoolAnnouncement: List<SchoolAnnouncement>) =

View File

@ -21,7 +21,7 @@ class NewMessageNotification @Inject constructor(
val notificationDataList = items.map {
NotificationData(
title = context.getPlural(R.plurals.message_new_items, 1),
content = "${it.sender}: ${it.subject}",
content = "${it.correspondents}: ${it.subject}",
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.Student
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.waitForResult
import io.github.wulkanowy.services.sync.notifications.NewMessageNotification
@ -11,19 +12,21 @@ import javax.inject.Inject
class MessageWork @Inject constructor(
private val messageRepository: MessageRepository,
private val mailboxRepository: MailboxRepository,
private val newMessageNotification: NewMessageNotification,
) : Work {
override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) {
val mailbox = mailboxRepository.getMailbox(student)
messageRepository.getMessages(
student = student,
semester = semester,
mailbox = mailbox,
folder = RECEIVED,
forceRefresh = true,
notify = notify
).waitForResult()
messageRepository.getMessagesFromDatabase(student).first()
messageRepository.getMessagesFromDatabase(mailbox).first()
.filter { !it.isNotified && it.unread }.let {
if (it.isNotEmpty()) newMessageNotification.notify(it, student)
messageRepository.updateMessages(it.onEach { message -> message.isNotified = true })

View File

@ -1,23 +1,22 @@
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.Student
import io.github.wulkanowy.data.repositories.MailboxRepository
import io.github.wulkanowy.data.repositories.RecipientRepository
import io.github.wulkanowy.data.repositories.ReportingUnitRepository
import javax.inject.Inject
class RecipientWork @Inject constructor(
private val reportingUnitRepository: ReportingUnitRepository,
private val mailboxRepository: MailboxRepository,
private val recipientRepository: RecipientRepository
) : Work {
override suspend fun doWork(student: Student, semester: Semester, notify: Boolean) {
reportingUnitRepository.refreshReportingUnits(student)
mailboxRepository.refreshMailboxes(student)
reportingUnitRepository.getReportingUnits(student).let { units ->
units.map {
recipientRepository.refreshRecipients(student, it, 2)
}
}
val mailbox = mailboxRepository.getMailbox(student)
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.services.sync.notifications.NewSchoolAnnouncementNotification
import kotlinx.coroutines.flow.first
import java.time.LocalDate
import javax.inject.Inject
class SchoolAnnouncementWork @Inject constructor(
@ -20,10 +21,13 @@ class SchoolAnnouncementWork @Inject constructor(
notify = notify,
).waitForResult()
schoolAnnouncementRepository.getSchoolAnnouncementFromDatabase(student).first()
.filter { !it.isNotified }.let {
if (it.isNotEmpty()) newSchoolAnnouncementNotification.notify(it, student)
schoolAnnouncementRepository.getSchoolAnnouncementFromDatabase(student)
.first()
.filter { !it.isNotified && it.date >= LocalDate.now() }
.let {
if (it.isNotEmpty()) {
newSchoolAnnouncementNotification.notify(it, student)
}
schoolAnnouncementRepository.updateSchoolAnnouncement(it.onEach { schoolAnnouncement ->
schoolAnnouncement.isNotified = true

View File

@ -91,15 +91,19 @@ class AttendancePresenter @Inject constructor(
fun onViewReselected() {
Timber.i("Attendance view is reselected")
view?.also { view ->
view?.let { view ->
if (view.currentStackSize == 1) {
baseDate.also {
if (currentDate != it) {
reloadView(it)
loadData()
} else if (!view.isViewEmpty) view.resetView()
baseDate = now().previousOrSameSchoolDay
if (currentDate != baseDate) {
reloadView(baseDate)
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.attendance.summary.AttendanceSummaryFragment
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.grade.GradeFragment
import io.github.wulkanowy.ui.modules.homework.HomeworkFragment
@ -47,6 +48,14 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
override var subtitleString =
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 {
fun newInstance() = DashboardFragment()

View File

@ -1,13 +1,9 @@
package io.github.wulkanowy.ui.modules.dashboard
import io.github.wulkanowy.data.db.entities.AdminMessage
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.db.entities.*
import io.github.wulkanowy.data.enums.GradeColorTheme
import io.github.wulkanowy.data.pojos.TimetableFull
import io.github.wulkanowy.utils.AdBanner
import io.github.wulkanowy.data.db.entities.Homework as EntitiesHomework
sealed class DashboardItem(val type: Type) {
@ -106,17 +102,26 @@ sealed class DashboardItem(val type: Type) {
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 {
ADMIN_MESSAGE,
ACCOUNT,
HORIZONTAL_GROUP,
LESSONS,
ADS,
GRADES,
HOMEWORK,
ANNOUNCEMENTS,
EXAMS,
CONFERENCES,
ADS
}
enum class Tile {
@ -126,12 +131,12 @@ sealed class DashboardItem(val type: Type) {
MESSAGES,
ATTENDANCE,
LESSONS,
ADS,
GRADES,
HOMEWORK,
ANNOUNCEMENTS,
EXAMS,
CONFERENCES,
ADS
}
}
@ -148,4 +153,4 @@ fun DashboardItem.Tile.toDashboardItemType() = when (this) {
DashboardItem.Tile.EXAMS -> DashboardItem.Type.EXAMS
DashboardItem.Tile.CONFERENCES -> DashboardItem.Type.CONFERENCES
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.RecyclerView
import java.util.Collections
import io.github.wulkanowy.ui.modules.dashboard.adapters.DashboardAdapter
import java.util.*
class DashboardItemMoveCallback(
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.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AdsHelper
import io.github.wulkanowy.utils.calculatePercentage
import io.github.wulkanowy.utils.nextOrSameSchoolDay
import kotlinx.coroutines.flow.*
@ -24,6 +25,7 @@ class DashboardPresenter @Inject constructor(
private val gradeRepository: GradeRepository,
private val semesterRepository: SemesterRepository,
private val messageRepository: MessageRepository,
private val mailboxRepository: MailboxRepository,
private val attendanceSummaryRepository: AttendanceSummaryRepository,
private val timetableRepository: TimetableRepository,
private val homeworkRepository: HomeworkRepository,
@ -31,7 +33,8 @@ class DashboardPresenter @Inject constructor(
private val conferenceRepository: ConferenceRepository,
private val preferencesRepository: PreferencesRepository,
private val schoolAnnouncementRepository: SchoolAnnouncementRepository,
private val adminMessageRepository: AdminMessageRepository
private val adminMessageRepository: AdminMessageRepository,
private val adsHelper: AdsHelper
) : BasePresenter<DashboardView>(errorHandler, studentRepository) {
private val dashboardItemLoadedList = mutableListOf<DashboardItem>()
@ -55,7 +58,11 @@ class DashboardPresenter @Inject constructor(
showContent(false)
}
preferencesRepository.selectedDashboardTilesFlow
merge(
preferencesRepository.selectedDashboardTilesFlow,
preferencesRepository.isAdsEnabledFlow
.map { preferencesRepository.selectedDashboardTiles }
)
.onEach { loadData(tilesToLoad = it) }
.launch("dashboard_pref")
}
@ -166,7 +173,7 @@ class DashboardPresenter @Inject constructor(
DashboardItem.Type.CONFERENCES -> {
loadConferences(student, forceRefresh)
}
DashboardItem.Type.ADS -> TODO()
DashboardItem.Type.ADS -> loadAds(forceRefresh)
DashboardItem.Type.ADMIN_MESSAGE -> loadAdminMessage(student, forceRefresh)
}
}
@ -221,6 +228,7 @@ class DashboardPresenter @Inject constructor(
private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) {
flow {
val semester = semesterRepository.getCurrentSemester(student)
val mailbox = mailboxRepository.getMailbox(student)
val selectedTiles = preferencesRepository.selectedDashboardTiles
val flowSuccess = flowOf(Resource.Success(null))
@ -232,7 +240,7 @@ class DashboardPresenter @Inject constructor(
val messageFLow = messageRepository.getMessages(
student = student,
semester = semester,
mailbox = mailbox,
folder = MessageFolder.RECEIVED,
forceRefresh = forceRefresh
).takeIf { DashboardItem.Tile.MESSAGES in selectedTiles } ?: flowSuccess
@ -595,6 +603,23 @@ class DashboardPresenter @Inject constructor(
.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) {
val isForceRefreshError = forceRefresh && dashboardItem.error != null
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) {
updateForceRefreshData(dashboardItem)
} else {

View File

@ -4,6 +4,8 @@ import io.github.wulkanowy.ui.base.BaseView
interface DashboardView : BaseView {
val tileWidth: Int
fun initView()
fun updateData(data: List<DashboardItem>)
@ -27,4 +29,4 @@ interface DashboardView : BaseView {
fun openNotificationsCenterView()
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.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.TimetableHeader
import io.github.wulkanowy.data.enums.GradeColorTheme
import io.github.wulkanowy.databinding.ItemDashboardAccountBinding
import io.github.wulkanowy.databinding.ItemDashboardAdminMessageBinding
import io.github.wulkanowy.databinding.ItemDashboardAnnouncementsBinding
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 io.github.wulkanowy.databinding.*
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.utils.*
import timber.log.Timber
import java.time.*
import java.util.Timer
import java.time.Duration
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.util.*
import javax.inject.Inject
import kotlin.concurrent.timer
@ -120,6 +111,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
DashboardItem.Type.ADMIN_MESSAGE.ordinal -> AdminMessageViewHolder(
ItemDashboardAdminMessageBinding.inflate(inflater, parent, false)
)
DashboardItem.Type.ADS.ordinal -> AdsViewHolder(
ItemDashboardAdsBinding.inflate(inflater, parent, false)
)
else -> throw IllegalArgumentException()
}
}
@ -135,6 +129,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
is ExamsViewHolder -> bindExamsViewHolder(holder, position)
is ConferencesViewHolder -> bindConferencesViewHolder(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) :
RecyclerView.ViewHolder(binding.root)
@ -788,6 +797,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
class AdminMessageViewHolder(val binding: ItemDashboardAdminMessageBinding) :
RecyclerView.ViewHolder(binding.root)
class AdsViewHolder(val binding: ItemDashboardAdsBinding) :
RecyclerView.ViewHolder(binding.root)
private class DiffCallback(
private val newList: 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.ViewGroup
@ -33,4 +33,4 @@ class DashboardAnnouncementsAdapter :
class ViewHolder(val binding: SubitemDashboardAnnouncementsBinding) :
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.ViewGroup
@ -33,4 +33,4 @@ class DashboardConferencesAdapter :
class ViewHolder(val binding: SubitemDashboardConferencesBinding) :
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.view.LayoutInflater
@ -56,4 +56,4 @@ class DashboardExamsAdapter :
class ViewHolder(val binding: SubitemDashboardExamsBinding) :
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.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.view.LayoutInflater
@ -53,4 +53,4 @@ class DashboardHomeworkAdapter : RecyclerView.Adapter<DashboardHomeworkAdapter.V
class ViewHolder(val binding: SubitemDashboardHomeworkBinding) :
RecyclerView.ViewHolder(binding.root)
}
}

View File

@ -17,16 +17,13 @@ val debugMessageItems = listOf(
)
private fun generateMessage(sender: String, subject: String) = Message(
sender = sender,
subject = subject,
studentId = 0,
realId = 0,
messageId = 0,
senderId = 0,
recipient = "",
messageId = 123,
date = Instant.now(),
folderId = 0,
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(
subject = subject,
content = content,
studentId = 0,
userLoginId = 0,
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.db.entities.Grade
import io.github.wulkanowy.data.enums.GradeExpandMode
import io.github.wulkanowy.data.enums.GradeSortingMode.ALPHABETIC
import io.github.wulkanowy.data.enums.GradeSortingMode.DATE
import io.github.wulkanowy.data.enums.GradeSortingMode
import io.github.wulkanowy.data.enums.GradeSortingMode.*
import io.github.wulkanowy.data.repositories.GradeRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
@ -204,6 +204,7 @@ class GradeDetailsPresenter @Inject constructor(
ALPHABETIC -> gradeSubjects.sortedBy { gradeDetailsWithAverage ->
gradeDetailsWithAverage.subject.lowercase()
}
AVERAGE -> gradeSubjects.sortedByDescending { it.average }
}
}
.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.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.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
@ -14,6 +17,7 @@ import javax.inject.Inject
class GradeSummaryPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val preferencesRepository: PreferencesRepository,
private val averageProvider: GradeAverageProvider,
private val analytics: AnalyticsHelper
) : BasePresenter<GradeSummaryView>(errorHandler, studentRepository) {
@ -127,7 +131,17 @@ class GradeSummaryPresenter @Inject constructor(
private fun createGradeSummaryItems(items: List<GradeSubject>): List<GradeSummary> {
return items
.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) }
}

View File

@ -93,6 +93,7 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
}
//https://developer.android.com/guide/playcore/in-app-updates#status_callback
@Deprecated("Deprecated in Java")
@Suppress("DEPRECATION")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
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) {
val emailHost = login.substringAfter("@")
val emailDomain = URL(host).host
if (emailHost != emailDomain) {
if (!emailHost.equals(emailDomain, true)) {
view?.setErrorEmailInvalid(domain = emailDomain)
isCorrect = false
}

View File

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

View File

@ -12,6 +12,7 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.elevation.ElevationOverlayProvider
import com.ncapdevi.fragnav.FragNavController
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.StudentWithSemesters
import io.github.wulkanowy.databinding.ActivityMainBinding
import io.github.wulkanowy.databinding.DialogAdsConsentBinding
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.Destination
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
@Deprecated("Deprecated in Java")
@Suppress("DEPRECATION")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
@ -287,6 +290,50 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
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) {
super.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.schoolandteachers.SchoolAndTeachersView
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.AppInfo
import kotlinx.coroutines.launch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import timber.log.Timber
@ -29,10 +32,12 @@ import javax.inject.Inject
class MainPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val prefRepository: PreferencesRepository,
private val preferencesRepository: PreferencesRepository,
private val syncManager: SyncManager,
private val analytics: AnalyticsHelper,
private val json: Json
private val json: Json,
private val adsHelper: AdsHelper,
private val appInfo: AppInfo
) : BasePresenter<MainView>(errorHandler, studentRepository) {
private var studentsWitSemesters: List<StudentWithSemesters>? = null
@ -47,7 +52,7 @@ class MainPresenter @Inject constructor(
private val Destination?.startMenuIndex
get() = when {
this == null -> prefRepository.startMenuIndex
this == null -> preferencesRepository.startMenuIndex
destinationType in rootDestinationTypeList -> {
rootDestinationTypeList.indexOf(destinationType)
}
@ -71,6 +76,8 @@ class MainPresenter @Inject constructor(
syncManager.startPeriodicSyncWorker()
checkAppSupport()
analytics.logEvent("app_open", "destination" to initDestination.toString())
Timber.i("Main view was initialized with $initDestination")
}
@ -155,18 +162,52 @@ class MainPresenter @Inject constructor(
} == true
}
private fun checkInAppReview() {
prefRepository.inAppReviewCount++
fun onEnableAdsSelected() {
view?.showPrivacyPolicyDialog()
}
if (prefRepository.inAppReviewDate == null) {
prefRepository.inAppReviewDate = Instant.now()
fun onPrivacyAgree(isPersonalizedAds: Boolean) {
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 &&
Instant.now().minus(Duration.ofDays(14)).isAfter(prefRepository.inAppReviewDate)
if (!preferencesRepository.isAppReviewDone && preferencesRepository.inAppReviewCount >= 50 &&
Instant.now().minus(Duration.ofDays(14)).isAfter(preferencesRepository.inAppReviewDate)
) {
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 showAppSupport()
fun showPrivacyPolicyDialog()
fun openPrivacyPolicy()
fun openMoreDestination(destination: Destination)
interface MainChildView {

View File

@ -4,6 +4,8 @@ import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.text.HtmlCompat.FROM_HTML_MODE_COMPACT
import androidx.core.text.parseAsHtml
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Message
@ -75,29 +77,25 @@ class MessagePreviewAdapter @Inject constructor() :
@SuppressLint("SetTextI18n")
private fun bindMessage(holder: MessageViewHolder, message: Message) {
val context = holder.binding.root.context
val recipientCount = message.unreadBy + message.readBy
val readText = when {
recipientCount > 1 -> {
context.getString(R.string.message_read_by, message.readBy, recipientCount)
}
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 readTextValue = when {
!message.unread -> R.string.all_yes
else -> R.string.all_no
}
val readText = context.getString(R.string.message_read, context.getString(readTextValue))
with(holder.binding) {
messagePreviewSubject.text =
message.subject.ifBlank { root.context.getString(R.string.message_no_subject) }
messagePreviewDate.text = root.context.getString(
messagePreviewSubject.text = message.subject.ifBlank {
context.getString(R.string.message_no_subject)
}
messagePreviewDate.text = context.getString(
R.string.message_date,
message.date.toFormattedString("yyyy-MM-dd HH:mm:ss")
)
messagePreviewRead.text = readText
messagePreviewContent.text = message.content
messagePreviewContent.text = message.content.parseAsHtml(FROM_HTML_MODE_COMPACT)
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)
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
get() = getString(R.string.message_not_exists)
@ -81,7 +82,10 @@ class MessagePreviewFragment :
super.onViewCreated(view, savedInstanceState)
binding = FragmentMessagePreviewBinding.bind(view)
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() {
@ -101,6 +105,8 @@ class MessagePreviewFragment :
menuShareButton = menu.findItem(R.id.messagePreviewMenuShare)
menuPrintButton = menu.findItem(R.id.messagePreviewMenuPrint)
presenter.onCreateOptionsMenu()
menu.findItem(R.id.mainMenuAccount).isVisible = false
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -129,8 +135,8 @@ class MessagePreviewFragment :
binding.messagePreviewRecycler.visibility = if (show) VISIBLE else GONE
}
override fun showOptions(show: Boolean) {
menuReplyButton?.isVisible = show
override fun showOptions(show: Boolean, isReplayable: Boolean) {
menuReplyButton?.isVisible = isReplayable
menuForwardButton?.isVisible = show
menuDeleteButton?.isVisible = show
menuShareButton?.isVisible = show
@ -173,7 +179,8 @@ class MessagePreviewFragment :
val webView = WebView(requireContext())
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) {
createWebPrintJob(view, jobName)

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
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
interface SendMessageView : BaseView {
@ -18,9 +18,17 @@ interface SendMessageView : BaseView {
val messageSuccess: String
val mailboxStudent: String
val mailboxParent: String
val mailboxGuardian: String
val mailboxEmployee: String
fun initView()
fun setReportingUnit(unit: ReportingUnit)
fun setMailbox(mailbox: String)
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.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.databinding.ItemMessageBinding
import io.github.wulkanowy.databinding.ItemMessageChipsBinding
import io.github.wulkanowy.utils.toFormattedString
@ -88,12 +87,8 @@ class MessageTabAdapter @Inject constructor() :
with(holder.binding) {
val style = if (message.unread) Typeface.BOLD else Typeface.NORMAL
messageItemAuthor.run {
text = if (message.folderId == MessageFolder.SENT.id) {
message.recipient
} else {
message.sender
}
with(messageItemAuthor) {
text = message.correspondents
setTypeface(null, style)
}
messageItemSubject.run {
@ -145,7 +140,7 @@ class MessageTabAdapter @Inject constructor() :
val newItem = new[newItemPosition]
return if (oldItem is MessageTabDataItem.MessageItem && newItem is MessageTabDataItem.MessageItem) {
oldItem.message.id == newItem.message.id
oldItem.message.messageGlobalKey == newItem.message.messageGlobalKey
} else {
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.db.entities.Message
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.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
@ -26,7 +26,7 @@ class MessageTabPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val messageRepository: MessageRepository,
private val semesterRepository: SemesterRepository,
private val mailboxRepository: MailboxRepository,
private val analytics: AnalyticsHelper
) : BasePresenter<MessageTabView>(errorHandler, studentRepository) {
@ -122,7 +122,8 @@ class MessageTabPresenter @Inject constructor(
runCatching {
val student = studentRepository.getCurrentStudent(true)
messageRepository.deleteMessages(student, messageList)
val mailbox = mailboxRepository.getMailbox(student)
messageRepository.deleteMessages(student, mailbox, messageList)
}
.onFailure(errorHandler::dispatch)
.onSuccess { view?.showMessagesDeleted() }
@ -159,7 +160,7 @@ class MessageTabPresenter @Inject constructor(
}
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) {
view?.run {
@ -206,8 +207,8 @@ class MessageTabPresenter @Inject constructor(
flatResourceFlow {
val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student)
messageRepository.getMessages(student, semester, folder, forceRefresh)
val mailbox = mailboxRepository.getMailbox(student)
messageRepository.getMessages(student, mailbox, folder, forceRefresh)
}
.logResourceStatus("load $folder message")
.onResourceData {
@ -333,7 +334,7 @@ class MessageTabPresenter @Inject constructor(
addAll(data.map { message ->
MessageTabDataItem.MessageItem(
message = message,
isSelected = messagesToDelete.any { it.id == message.id },
isSelected = messagesToDelete.any { it.messageGlobalKey == message.messageGlobalKey },
isActionMode = isActionMode
)
})
@ -345,10 +346,9 @@ class MessageTabPresenter @Inject constructor(
private fun calculateMatchRatio(message: Message, query: String): Int {
val subjectRatio = FuzzySearch.tokenSortPartialRatio(query.lowercase(), message.subject)
val senderOrRecipientRatio = FuzzySearch.tokenSortPartialRatio(
val correspondentsRatio = FuzzySearch.tokenSortPartialRatio(
query.lowercase(),
if (message.sender.isNotEmpty()) message.sender.lowercase()
else message.recipient.lowercase()
message.correspondents
)
val dateRatio = listOf(
@ -364,7 +364,7 @@ class MessageTabPresenter @Inject constructor(
return (subjectRatio.toDouble().pow(2)
+ senderOrRecipientRatio.toDouble().pow(2)
+ correspondentsRatio.toDouble().pow(2)
+ dateRatio.toDouble().pow(2) * 2
).toInt()
}

View File

@ -87,15 +87,19 @@ class TimetablePresenter @Inject constructor(
fun onViewReselected() {
Timber.i("Timetable view is reselected")
view?.also { view ->
view?.let { view ->
if (view.currentStackSize == 1) {
baseDate.also {
if (currentDate != it) {
reloadView(it)
loadData()
} else if (!view.isViewEmpty) view.resetView()
baseDate = now().nextOrSameSchoolDay
if (currentDate != baseDate) {
reloadView(baseDate)
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
import android.appwidget.AppWidgetManager.ACTION_APPWIDGET_UPDATE
import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID
import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_IDS
import android.appwidget.AppWidgetManager.*
import android.content.Intent
import android.os.Build
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.WidgetConfigureAdapter
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.utils.AppInfo
import javax.inject.Inject
@ -92,6 +91,7 @@ class TimetableWidgetConfigureActivity :
.apply {
action = ACTION_APPWIDGET_UPDATE
putExtra(EXTRA_APPWIDGET_IDS, intArrayOf(widgetId))
putExtra(EXTRA_FROM_CONFIGURE, true)
})
}

View File

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

View File

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

View File

@ -37,7 +37,7 @@
android:padding="10dp"
android:visibility="gone"
tools:ignore="UseCompoundDrawables"
tools:visibility="visible">
tools:visibility="gone">
<ImageView
android:layout_width="100dp"
@ -63,7 +63,7 @@
android:orientation="vertical"
android:visibility="invisible"
tools:ignore="UseCompoundDrawables"
tools:visibility="visible">
tools:visibility="gone">
<ImageView
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
android:id="@+id/noteItemPoints"
android:layout_width="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="16sp"

View File

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

View File

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

View File

@ -77,6 +77,9 @@
<string name="main_log_in">Přihlásit se</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_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-->
<string name="grade_header">Známka</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_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_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_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>
@ -294,6 +297,10 @@
<string name="message_move_to_trash">Přesunout do koše</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_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_print">Vytisknout</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_with_attachments">Pouze s přílohami</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">
<item quantity="one">%1$d zpráva</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_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_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_appearance_title">Vzhled a chování</string>
<string name="pref_settings_notifications_title">Oznámení</string>

View File

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

View File

@ -77,6 +77,9 @@
<string name="main_log_in">Anmelden</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_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-->
<string name="grade_header">Note</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_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_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_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>
@ -260,6 +263,10 @@
<string name="message_move_to_trash">In Papierkorb verschieben</string>
<string name="message_delete_forever">Dauerhaft löschen</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_print">Drucken</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_with_attachments">Nur mit Anhängen</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">
<item quantity="one">%1$d Nachricht</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_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_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_appearance_title">Aussehen &amp; Verhalten</string>
<string name="pref_settings_notifications_title">Benachrichtigungen</string>

View File

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

View File

@ -77,6 +77,9 @@
<string name="main_log_in">Zaloguj się</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_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-->
<string name="grade_header">Ocena</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_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_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_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>
@ -294,6 +297,10 @@
<string name="message_move_to_trash">Przenieś do kosza</string>
<string name="message_delete_forever">Usuń trwale</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_print">Drukuj</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_with_attachments">Tylko z załącznikami</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">
<item quantity="one">%1$d wiadomość</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_services_header">Synchronizacja</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_wifi">Tylko WiFi</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_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_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_appearance_title">Wygląd i zachowanie</string>
<string name="pref_settings_notifications_title">Powiadomienia</string>

View File

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

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