mirror of
https://github.com/wulkanowy/wulkanowy.git
synced 2025-01-19 15:46:46 -06:00
Merge branch 'release/1.4.0'
This commit is contained in:
commit
9066bce0d5
@ -1,5 +1,6 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'dagger.hilt.android.plugin'
|
||||
@ -14,23 +15,22 @@ apply from: 'sonarqube.gradle'
|
||||
apply from: 'hooks.gradle'
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
compileSdkVersion 31
|
||||
|
||||
defaultConfig {
|
||||
applicationId "io.github.wulkanowy"
|
||||
testApplicationId "io.github.tests.wulkanowy"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
versionCode 97
|
||||
versionName "1.3.0"
|
||||
targetSdkVersion 31
|
||||
versionCode 98
|
||||
versionName "1.4.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
|
||||
resValue "string", "app_name", "Wulkanowy"
|
||||
buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis())
|
||||
|
||||
manifestPlaceholders = [
|
||||
firebase_enabled: project.hasProperty("enableFirebase")
|
||||
firebase_enabled: project.hasProperty("enableFirebase"),
|
||||
admob_project_id: ""
|
||||
]
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
@ -40,6 +40,14 @@ android {
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
buildConfigField "String", "SINGLE_SUPPORT_AD_ID", "null"
|
||||
|
||||
if (System.env.SET_BUILD_TIMESTAMP) {
|
||||
buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis())
|
||||
} else {
|
||||
buildConfigField "long", "BUILD_TIMESTAMP", "1486235849000"
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
@ -62,12 +70,14 @@ android {
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
signingConfig signingConfigs.release
|
||||
buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\""
|
||||
}
|
||||
debug {
|
||||
resValue "string", "app_name", "Wulkanowy DEV " + defaultConfig.versionCode
|
||||
resValue "string", "app_name", "Wulkanowy DEV"
|
||||
applicationIdSuffix ".dev"
|
||||
versionNameSuffix "-dev"
|
||||
ext.enableCrashlytics = project.hasProperty("enableFirebase")
|
||||
buildConfigField "String", "MESSAGES_BASE_URL", "\"https://messages.wulkanowy.net.pl\""
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,23 +86,21 @@ android {
|
||||
productFlavors {
|
||||
hms {
|
||||
dimension "platform"
|
||||
manifestPlaceholders = [
|
||||
install_channel: "AppGallery"
|
||||
]
|
||||
manifestPlaceholders = [install_channel: "AppGallery"]
|
||||
}
|
||||
|
||||
play {
|
||||
dimension "platform"
|
||||
manifestPlaceholders = [
|
||||
install_channel: "Google Play"
|
||||
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"}\""
|
||||
}
|
||||
|
||||
fdroid {
|
||||
dimension "platform"
|
||||
manifestPlaceholders = [
|
||||
install_channel: "F-Droid"
|
||||
]
|
||||
manifestPlaceholders = [install_channel: "F-Droid"]
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,8 +149,8 @@ kapt {
|
||||
|
||||
play {
|
||||
defaultToAppBundles = false
|
||||
track = 'production'
|
||||
updatePriority = 3
|
||||
track = 'beta'
|
||||
updatePriority = 1
|
||||
enabled.set(false)
|
||||
}
|
||||
|
||||
@ -157,27 +165,28 @@ huaweiPublish {
|
||||
}
|
||||
|
||||
ext {
|
||||
work_manager = "2.6.0"
|
||||
work_manager = "2.7.0"
|
||||
android_hilt = "1.0.0"
|
||||
room = "2.3.0"
|
||||
chucker = "3.5.2"
|
||||
mockk = "1.12.0"
|
||||
moshi = "1.12.0"
|
||||
coroutines = "1.5.2"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "io.github.wulkanowy:sdk:1.3.0"
|
||||
implementation "io.github.wulkanowy:sdk:1.4.0"
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
|
||||
|
||||
implementation "androidx.core:core-ktx:1.6.0"
|
||||
implementation "androidx.activity:activity-ktx:1.3.1"
|
||||
implementation "androidx.appcompat:appcompat:1.3.1"
|
||||
implementation "androidx.appcompat:appcompat-resources:1.3.1"
|
||||
implementation "androidx.fragment:fragment-ktx:1.3.6"
|
||||
implementation "androidx.annotation:annotation:1.2.0"
|
||||
implementation "androidx.core:core-ktx:1.7.0"
|
||||
implementation 'androidx.core:core-splashscreen:1.0.0-alpha02'
|
||||
implementation "androidx.activity:activity-ktx:1.4.0"
|
||||
implementation "androidx.appcompat:appcompat:1.4.0-rc01"
|
||||
implementation "androidx.fragment:fragment-ktx:1.4.0-rc01"
|
||||
implementation "androidx.annotation:annotation:1.3.0"
|
||||
|
||||
implementation "androidx.preference:preference-ktx:1.1.1"
|
||||
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
||||
@ -193,7 +202,7 @@ dependencies {
|
||||
implementation "androidx.work:work-runtime-ktx:$work_manager"
|
||||
playImplementation "androidx.work:work-gcm:$work_manager"
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"
|
||||
|
||||
implementation "androidx.room:room-runtime:$room"
|
||||
implementation "androidx.room:room-ktx:$room"
|
||||
@ -207,40 +216,41 @@ dependencies {
|
||||
implementation 'com.github.ncapdevi:FragNav:3.3.0'
|
||||
implementation "com.github.YarikSOffice:lingver:1.3.0"
|
||||
|
||||
implementation "com.squareup.moshi:moshi:$moshi"
|
||||
implementation "com.squareup.moshi:moshi-adapters:$moshi"
|
||||
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi"
|
||||
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.2"
|
||||
|
||||
implementation "com.jakewharton.timber:timber:5.0.1"
|
||||
implementation "at.favre.lib:slf4j-timber:1.0.1"
|
||||
implementation 'com.github.bastienpaulfr:Treessence:1.0.4'
|
||||
implementation 'com.github.bastienpaulfr:Treessence:1.0.5'
|
||||
implementation "com.mikepenz:aboutlibraries-core:$about_libraries"
|
||||
implementation "io.coil-kt:coil:1.3.2"
|
||||
implementation "io.coil-kt:coil:1.4.0"
|
||||
implementation "io.github.wulkanowy:AppKillerManager:3.0.0"
|
||||
implementation 'me.xdrop:fuzzywuzzy:1.3.1'
|
||||
implementation 'com.fredporciuncula:flow-preferences:1.5.0'
|
||||
|
||||
playImplementation platform('com.google.firebase:firebase-bom:28.4.1')
|
||||
playImplementation platform('com.google.firebase:firebase-bom:29.0.0')
|
||||
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.2'
|
||||
playImplementation 'com.google.android.play:core-ktx:1.8.1'
|
||||
playImplementation 'com.google.android.gms:play-services-ads:20.4.0'
|
||||
|
||||
hmsImplementation 'com.huawei.hms:hianalytics:6.2.0.301'
|
||||
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.0.300'
|
||||
hmsImplementation 'com.huawei.hms:hianalytics:6.3.0.303'
|
||||
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.6.1.300'
|
||||
|
||||
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"
|
||||
|
||||
debugImplementation "com.github.ChuckerTeam.Chucker:library:$chucker"
|
||||
debugImplementation 'com.github.amitshekhariitbhu.Android-Debug-Database:debug-db:v1.0.6'
|
||||
debugImplementation 'com.github.amitshekhariitbhu.Android-Debug-Database:debug-db:1.0.6'
|
||||
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
testImplementation "io.mockk:mockk:$mockk"
|
||||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2'
|
||||
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines"
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
|
||||
testImplementation 'org.robolectric:robolectric:4.6.1'
|
||||
testImplementation 'org.robolectric:robolectric:4.7'
|
||||
testImplementation "androidx.test:runner:1.4.0"
|
||||
testImplementation "androidx.test.ext:junit:1.1.3"
|
||||
testImplementation "androidx.test:core:1.4.0"
|
||||
|
2322
app/schemas/io.github.wulkanowy.data.db.AppDatabase/41.json
Normal file
2322
app/schemas/io.github.wulkanowy.data.db.AppDatabase/41.json
Normal file
File diff suppressed because it is too large
Load Diff
2396
app/schemas/io.github.wulkanowy.data.db.AppDatabase/42.json
Normal file
2396
app/schemas/io.github.wulkanowy.data.db.AppDatabase/42.json
Normal file
File diff suppressed because it is too large
Load Diff
2408
app/schemas/io.github.wulkanowy.data.db.AppDatabase/43.json
Normal file
2408
app/schemas/io.github.wulkanowy.data.db.AppDatabase/43.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -38,10 +38,10 @@
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="false"
|
||||
android:theme="@style/WulkanowyTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="GoogleAppIndexingWarning,UnusedAttribute">
|
||||
<activity
|
||||
android:name=".ui.modules.splash.SplashActivity"
|
||||
@ -106,7 +106,8 @@
|
||||
</service>
|
||||
<service
|
||||
android:name=".services.messaging.AppMessagingService"
|
||||
android:exported="false">
|
||||
android:exported="false"
|
||||
tools:ignore="MissingClass">
|
||||
<intent-filter>
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
@ -152,44 +153,44 @@
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
|
||||
<meta-data
|
||||
android:name="install_channel"
|
||||
android:value="${install_channel}" />
|
||||
|
||||
<!-- workaround for https://github.com/firebase/firebase-android-sdk/issues/473 enabled:false -->
|
||||
<!-- https://firebase.googleblog.com/2017/03/take-control-of-your-firebase-init-on.html -->
|
||||
<provider
|
||||
android:name="com.google.firebase.provider.FirebaseInitProvider"
|
||||
android:authorities="${applicationId}.firebaseinitprovider"
|
||||
android:enabled="${firebase_enabled}"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
tools:ignore="MissingClass" />
|
||||
|
||||
<meta-data
|
||||
android:name="install_channel"
|
||||
android:value="${install_channel}" />
|
||||
<meta-data
|
||||
android:name="firebase_analytics_collection_enabled"
|
||||
android:value="${firebase_enabled}" />
|
||||
|
||||
<meta-data
|
||||
android:name="google_analytics_adid_collection_enabled"
|
||||
android:value="${firebase_enabled}" />
|
||||
|
||||
<meta-data
|
||||
android:name="firebase_crashlytics_collection_enabled"
|
||||
android:value="${firebase_enabled}" />
|
||||
|
||||
<meta-data
|
||||
android:name="firebase_messaging_auto_init_enabled"
|
||||
android:value="${firebase_enabled}" />
|
||||
|
||||
<meta-data
|
||||
android:name="firebase_inapp_messaging_auto_data_collection_enabled"
|
||||
android:value="${firebase_enabled}" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_icon"
|
||||
android:resource="@drawable/ic_stat_all" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||
android:value="push_channel" />
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.ads.APPLICATION_ID"
|
||||
android:value="${admob_project_id}" />
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.ads.DELAY_APP_MEASUREMENT_INIT"
|
||||
android:value="true" />
|
||||
</application>
|
||||
</manifest>
|
||||
|
@ -1,12 +1,10 @@
|
||||
package io.github.wulkanowy
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.util.Log.DEBUG
|
||||
import android.util.Log.INFO
|
||||
import android.util.Log.VERBOSE
|
||||
import android.webkit.WebView
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.hilt.work.HiltWorkerFactory
|
||||
import androidx.work.Configuration
|
||||
import com.yariksoffice.lingver.Lingver
|
||||
@ -41,10 +39,8 @@ class WulkanowyApp : Application(), Configuration.Provider {
|
||||
@Inject
|
||||
lateinit var analyticsHelper: AnalyticsHelper
|
||||
|
||||
@SuppressLint("UnsafeOptInUsageWarning")
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
FragmentManager.enableNewStateManager(false)
|
||||
initializeAppLanguage()
|
||||
themeManager.applyDefaultTheme()
|
||||
initLogging()
|
||||
|
@ -2,62 +2,100 @@ package io.github.wulkanowy.data
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.content.res.AssetManager
|
||||
import android.content.res.Resources
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.chuckerteam.chucker.api.ChuckerCollector
|
||||
import com.chuckerteam.chucker.api.ChuckerInterceptor
|
||||
import com.chuckerteam.chucker.api.RetentionManager
|
||||
import com.fredporciuncula.flow.preferences.FlowSharedPreferences
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import io.github.wulkanowy.data.api.AdminMessageService
|
||||
import io.github.wulkanowy.data.db.AppDatabase
|
||||
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
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal class RepositoryModule {
|
||||
internal class DataModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideSdk(chuckerCollector: ChuckerCollector, @ApplicationContext context: Context): Sdk {
|
||||
return Sdk().apply {
|
||||
fun provideSdk(chuckerInterceptor: ChuckerInterceptor) =
|
||||
Sdk().apply {
|
||||
androidVersion = android.os.Build.VERSION.RELEASE
|
||||
buildTag = android.os.Build.MODEL
|
||||
setSimpleHttpLogger { Timber.d(it) }
|
||||
|
||||
// for debug only
|
||||
addInterceptor(
|
||||
ChuckerInterceptor.Builder(context)
|
||||
.collector(chuckerCollector)
|
||||
.alwaysReadResponseBody(true)
|
||||
.build(), network = true
|
||||
)
|
||||
addInterceptor(chuckerInterceptor, network = true)
|
||||
}
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideChuckerCollector(
|
||||
@ApplicationContext context: Context,
|
||||
prefRepository: PreferencesRepository
|
||||
): ChuckerCollector {
|
||||
return ChuckerCollector(
|
||||
context = context,
|
||||
showNotification = prefRepository.isDebugNotificationEnable,
|
||||
retentionPeriod = RetentionManager.Period.ONE_HOUR
|
||||
)
|
||||
}
|
||||
) = ChuckerCollector(
|
||||
context = context,
|
||||
showNotification = prefRepository.isDebugNotificationEnable,
|
||||
retentionPeriod = RetentionManager.Period.ONE_HOUR
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideChuckerInterceptor(
|
||||
@ApplicationContext context: Context,
|
||||
chuckerCollector: ChuckerCollector
|
||||
) = ChuckerInterceptor.Builder(context)
|
||||
.collector(chuckerCollector)
|
||||
.alwaysReadResponseBody(true)
|
||||
.build()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideOkHttpClient(chuckerInterceptor: ChuckerInterceptor): OkHttpClient =
|
||||
OkHttpClient.Builder()
|
||||
.addNetworkInterceptor(chuckerInterceptor)
|
||||
.addInterceptor(HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BASIC
|
||||
})
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideRetrofit(
|
||||
okHttpClient: OkHttpClient,
|
||||
json: Json,
|
||||
appInfo: AppInfo
|
||||
): Retrofit = Retrofit.Builder()
|
||||
.baseUrl(appInfo.messagesBaseUrl)
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
||||
.build()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideAdminMessageService(retrofit: Retrofit): AdminMessageService = retrofit.create()
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
@ -67,14 +105,6 @@ internal class RepositoryModule {
|
||||
appInfo: AppInfo
|
||||
) = AppDatabase.newInstance(context, sharedPrefProvider, appInfo)
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideResources(@ApplicationContext context: Context): Resources = context.resources
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideAssets(@ApplicationContext context: Context): AssetManager = context.assets
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideSharedPref(@ApplicationContext context: Context): SharedPreferences =
|
||||
@ -88,7 +118,9 @@ internal class RepositoryModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideMoshi() = Moshi.Builder().build()
|
||||
fun provideJson() = Json {
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
@ -206,4 +238,8 @@ internal class RepositoryModule {
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideNotificationDao(database: AppDatabase) = database.notificationDao
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideAdminMessageDao(database: AppDatabase) = database.adminMessagesDao
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package io.github.wulkanowy.data.api
|
||||
|
||||
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||
import retrofit2.http.GET
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
interface AdminMessageService {
|
||||
|
||||
@GET("/v1.json")
|
||||
suspend fun getAdminMessages(): List<AdminMessage>
|
||||
}
|
@ -6,6 +6,7 @@ import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.RoomDatabase.JournalMode.TRUNCATE
|
||||
import androidx.room.TypeConverters
|
||||
import io.github.wulkanowy.data.db.dao.AdminMessageDao
|
||||
import io.github.wulkanowy.data.db.dao.AttendanceDao
|
||||
import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao
|
||||
import io.github.wulkanowy.data.db.dao.CompletedLessonsDao
|
||||
@ -35,6 +36,7 @@ import io.github.wulkanowy.data.db.dao.TeacherDao
|
||||
import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao
|
||||
import io.github.wulkanowy.data.db.dao.TimetableDao
|
||||
import io.github.wulkanowy.data.db.dao.TimetableHeaderDao
|
||||
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||
import io.github.wulkanowy.data.db.entities.Attendance
|
||||
import io.github.wulkanowy.data.db.entities.AttendanceSummary
|
||||
import io.github.wulkanowy.data.db.entities.CompletedLesson
|
||||
@ -98,6 +100,9 @@ import io.github.wulkanowy.data.db.migrations.Migration38
|
||||
import io.github.wulkanowy.data.db.migrations.Migration39
|
||||
import io.github.wulkanowy.data.db.migrations.Migration4
|
||||
import io.github.wulkanowy.data.db.migrations.Migration40
|
||||
import io.github.wulkanowy.data.db.migrations.Migration41
|
||||
import io.github.wulkanowy.data.db.migrations.Migration42
|
||||
import io.github.wulkanowy.data.db.migrations.Migration43
|
||||
import io.github.wulkanowy.data.db.migrations.Migration5
|
||||
import io.github.wulkanowy.data.db.migrations.Migration6
|
||||
import io.github.wulkanowy.data.db.migrations.Migration7
|
||||
@ -137,7 +142,8 @@ import javax.inject.Singleton
|
||||
StudentInfo::class,
|
||||
TimetableHeader::class,
|
||||
SchoolAnnouncement::class,
|
||||
Notification::class
|
||||
Notification::class,
|
||||
AdminMessage::class
|
||||
],
|
||||
version = AppDatabase.VERSION_SCHEMA,
|
||||
exportSchema = true
|
||||
@ -146,7 +152,7 @@ import javax.inject.Singleton
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
companion object {
|
||||
const val VERSION_SCHEMA = 40
|
||||
const val VERSION_SCHEMA = 43
|
||||
|
||||
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
|
||||
Migration2(),
|
||||
@ -187,7 +193,10 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
Migration37(),
|
||||
Migration38(),
|
||||
Migration39(),
|
||||
Migration40()
|
||||
Migration40(),
|
||||
Migration41(sharedPrefProvider),
|
||||
Migration42(),
|
||||
Migration43()
|
||||
)
|
||||
|
||||
fun newInstance(
|
||||
@ -259,4 +268,6 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
abstract val schoolAnnouncementDao: SchoolAnnouncementDao
|
||||
|
||||
abstract val notificationDao: NotificationDao
|
||||
|
||||
abstract val adminMessagesDao: AdminMessageDao
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
package io.github.wulkanowy.data.db
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import io.github.wulkanowy.data.db.adapters.PairAdapterFactory
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
@ -13,15 +14,7 @@ import java.util.Date
|
||||
|
||||
class Converters {
|
||||
|
||||
private val moshi by lazy { Moshi.Builder().add(PairAdapterFactory).build() }
|
||||
|
||||
private val integerListAdapter by lazy {
|
||||
moshi.adapter<List<Int>>(Types.newParameterizedType(List::class.java, Integer::class.java))
|
||||
}
|
||||
|
||||
private val stringListPairAdapter by lazy {
|
||||
moshi.adapter<List<Pair<String, String>>>(Types.newParameterizedType(List::class.java, Pair::class.java, String::class.java, String::class.java))
|
||||
}
|
||||
private val json = Json
|
||||
|
||||
@TypeConverter
|
||||
fun timestampToDate(value: Long?): LocalDate? = value?.run {
|
||||
@ -51,21 +44,25 @@ class Converters {
|
||||
|
||||
@TypeConverter
|
||||
fun intListToJson(list: List<Int>): String {
|
||||
return integerListAdapter.toJson(list)
|
||||
return json.encodeToString(list)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToIntList(value: String): List<Int> {
|
||||
return integerListAdapter.fromJson(value).orEmpty()
|
||||
return json.decodeFromString(value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun stringPairListToJson(list: List<Pair<String, String>>): String {
|
||||
return stringListPairAdapter.toJson(list)
|
||||
return json.encodeToString(list)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToStringPairList(value: String): List<Pair<String, String>> {
|
||||
return stringListPairAdapter.fromJson(value).orEmpty()
|
||||
return try {
|
||||
json.decodeFromString(value)
|
||||
} catch (e: SerializationException) {
|
||||
emptyList() // handle errors from old gson Pair serialized data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,11 +22,14 @@ class SharedPrefProvider @Inject constructor(
|
||||
|
||||
fun getString(key: String) = sharedPref.getString(key, null)
|
||||
|
||||
fun getString(key: String, defaultValue: String): String = sharedPref.getString(key, defaultValue) ?: defaultValue
|
||||
fun getString(key: String, defaultValue: String): String =
|
||||
sharedPref.getString(key, defaultValue) ?: defaultValue
|
||||
|
||||
fun getBoolean(key: String, defaultValue: Boolean): Boolean = sharedPref.getBoolean(key, defaultValue)
|
||||
fun getBoolean(key: String, defaultValue: Boolean): Boolean =
|
||||
sharedPref.getBoolean(key, defaultValue)
|
||||
|
||||
fun putBoolean(key: String, value: Boolean, sync: Boolean = false) = sharedPref.edit(sync) { putBoolean(key, value) }
|
||||
fun putBoolean(key: String, value: Boolean, sync: Boolean = false) =
|
||||
sharedPref.edit(sync) { putBoolean(key, value) }
|
||||
|
||||
fun putString(key: String, value: String?, sync: Boolean = false) {
|
||||
sharedPref.edit(sync) { putString(key, value) }
|
||||
|
@ -1,68 +0,0 @@
|
||||
package io.github.wulkanowy.data.db.adapters
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.JsonReader
|
||||
import com.squareup.moshi.JsonWriter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import java.lang.reflect.Type
|
||||
|
||||
object PairAdapterFactory : JsonAdapter.Factory {
|
||||
|
||||
override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi): JsonAdapter<*>? {
|
||||
if (type !is ParameterizedType || List::class.java != type.rawType) return null
|
||||
if (type.actualTypeArguments[0] != Pair::class.java) return null
|
||||
|
||||
val listType = Types.newParameterizedType(List::class.java, Map::class.java, String::class.java)
|
||||
val listAdapter = moshi.adapter<List<Map<String, String>>>(listType)
|
||||
|
||||
val mapType = Types.newParameterizedType(MutableMap::class.java, String::class.java, String::class.java)
|
||||
val mapAdapter = moshi.adapter<Map<String, String>>(mapType)
|
||||
|
||||
return PairAdapter(listAdapter, mapAdapter)
|
||||
}
|
||||
|
||||
private class PairAdapter(
|
||||
private val listAdapter: JsonAdapter<List<Map<String, String>>>,
|
||||
private val mapAdapter: JsonAdapter<Map<String, String>>,
|
||||
) : JsonAdapter<List<Pair<String, String>>>() {
|
||||
|
||||
override fun toJson(writer: JsonWriter, value: List<Pair<String, String>>?) {
|
||||
writer.beginArray()
|
||||
value?.forEach {
|
||||
writer.beginObject()
|
||||
writer.name("first").value(it.first)
|
||||
writer.name("second").value(it.second)
|
||||
writer.endObject()
|
||||
}
|
||||
writer.endArray()
|
||||
}
|
||||
|
||||
override fun fromJson(reader: JsonReader): List<Pair<String, String>>? {
|
||||
return if (reader.peek() == JsonReader.Token.BEGIN_OBJECT) deserializeMoshiMap(reader)
|
||||
else deserializeGsonPair(reader)
|
||||
}
|
||||
|
||||
// for compatibility with 0.21.0
|
||||
private fun deserializeMoshiMap(reader: JsonReader): List<Pair<String, String>>? {
|
||||
val map = mapAdapter.fromJson(reader) ?: return null
|
||||
|
||||
return map.entries.map {
|
||||
it.key to it.value
|
||||
}
|
||||
}
|
||||
|
||||
private fun deserializeGsonPair(reader: JsonReader): List<Pair<String, String>>? {
|
||||
val list = listAdapter.fromJson(reader) ?: return null
|
||||
|
||||
return list.map {
|
||||
require(it.size == 2) {
|
||||
"pair with more or less than two elements: $list"
|
||||
}
|
||||
|
||||
it["first"].orEmpty() to it["second"].orEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package io.github.wulkanowy.data.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import io.github.wulkanowy.data.db.entities.AdminMessage
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
@Dao
|
||||
abstract class AdminMessageDao : BaseDao<AdminMessage> {
|
||||
|
||||
@Query("SELECT * FROM AdminMessages")
|
||||
abstract fun loadAll(): Flow<List<AdminMessage>>
|
||||
|
||||
@Transaction
|
||||
open suspend fun removeOldAndSaveNew(
|
||||
oldMessages: List<AdminMessage>,
|
||||
newMessages: List<AdminMessage>
|
||||
) {
|
||||
deleteAll(oldMessages)
|
||||
insertAll(newMessages)
|
||||
}
|
||||
}
|
@ -11,6 +11,11 @@ import javax.inject.Singleton
|
||||
@Dao
|
||||
interface AttendanceDao : BaseDao<Attendance> {
|
||||
|
||||
@Query("SELECT * FROM Attendance WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :from AND date <= :end")
|
||||
fun loadAll(diaryId: Int, studentId: Int, from: LocalDate, end: LocalDate): Flow<List<Attendance>>
|
||||
@Query("SELECT * FROM Attendance WHERE diary_id = :diaryId AND student_id = :studentId AND date >= :start AND date <= :end")
|
||||
fun loadAll(
|
||||
diaryId: Int,
|
||||
studentId: Int,
|
||||
start: LocalDate,
|
||||
end: LocalDate
|
||||
): Flow<List<Attendance>>
|
||||
}
|
||||
|
@ -0,0 +1,37 @@
|
||||
package io.github.wulkanowy.data.db.entities
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@Entity(tableName = "AdminMessages")
|
||||
data class AdminMessage(
|
||||
|
||||
@PrimaryKey
|
||||
val id: Int,
|
||||
|
||||
val title: String,
|
||||
|
||||
val content: String,
|
||||
|
||||
@ColumnInfo(name = "version_name")
|
||||
val versionMin: Int? = null,
|
||||
|
||||
@ColumnInfo(name = "version_max")
|
||||
val versionMax: Int? = null,
|
||||
|
||||
@ColumnInfo(name = "target_register_host")
|
||||
val targetRegisterHost: String? = null,
|
||||
|
||||
@ColumnInfo(name = "target_flavor")
|
||||
val targetFlavor: String? = null,
|
||||
|
||||
@ColumnInfo(name = "destination_url")
|
||||
val destinationUrl: String? = null,
|
||||
|
||||
val priority: String,
|
||||
|
||||
val type: String
|
||||
)
|
@ -47,4 +47,7 @@ data class Attendance(
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = 0
|
||||
|
||||
@ColumnInfo(name = "is_notified")
|
||||
var isNotified: Boolean = true
|
||||
}
|
||||
|
@ -40,4 +40,7 @@ data class Homework(
|
||||
|
||||
@ColumnInfo(name = "is_notified")
|
||||
var isNotified: Boolean = true
|
||||
|
||||
@ColumnInfo(name = "is_added_by_user")
|
||||
var isAddedByUser: Boolean = false
|
||||
}
|
||||
|
@ -3,10 +3,9 @@ package io.github.wulkanowy.data.db.entities
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.squareup.moshi.JsonClass
|
||||
import java.io.Serializable
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@kotlinx.serialization.Serializable
|
||||
@Entity(tableName = "Recipients")
|
||||
data class Recipient(
|
||||
|
||||
|
@ -50,4 +50,7 @@ data class Timetable(
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = 0
|
||||
|
||||
@ColumnInfo(name = "is_notified")
|
||||
var isNotified: Boolean = true
|
||||
}
|
||||
|
@ -0,0 +1,21 @@
|
||||
package io.github.wulkanowy.data.db.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import io.github.wulkanowy.data.db.SharedPrefProvider
|
||||
import io.github.wulkanowy.ui.modules.grade.GradeExpandMode
|
||||
|
||||
class Migration41(private val sharedPrefProvider: SharedPrefProvider) : Migration(40, 41) {
|
||||
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
migrateSharedPreferences()
|
||||
database.execSQL("ALTER TABLE Homework ADD COLUMN is_added_by_user INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
|
||||
private fun migrateSharedPreferences() {
|
||||
if (sharedPrefProvider.getBoolean("pref_key_expand_grade", false)) {
|
||||
sharedPrefProvider.putString("pref_key_expand_grade_mode", GradeExpandMode.ALWAYS_EXPANDED.value)
|
||||
}
|
||||
sharedPrefProvider.delete("pref_key_expand_grade")
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package io.github.wulkanowy.data.db.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
class Migration42 : Migration(41, 42) {
|
||||
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL(
|
||||
"""CREATE TABLE IF NOT EXISTS `AdminMessages` (
|
||||
`id` INTEGER NOT NULL,
|
||||
`title` TEXT NOT NULL,
|
||||
`content` TEXT NOT NULL,
|
||||
`version_name` INTEGER,
|
||||
`version_max` INTEGER,
|
||||
`target_register_host` TEXT,
|
||||
`target_flavor` TEXT,
|
||||
`destination_url` TEXT,
|
||||
`priority` TEXT NOT NULL,
|
||||
`type` TEXT NOT NULL,
|
||||
PRIMARY KEY(`id`))"""
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package io.github.wulkanowy.data.db.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
class Migration43 : Migration(42, 43) {
|
||||
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE Timetable ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1")
|
||||
database.execSQL("ALTER TABLE Attendance ADD COLUMN is_notified INTEGER NOT NULL DEFAULT 1")
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
package io.github.wulkanowy.data.pojos
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Serializable
|
||||
class Contributor(
|
||||
val displayName: String,
|
||||
val githubUsername: String
|
||||
|
@ -1,9 +1,9 @@
|
||||
package io.github.wulkanowy.data.pojos
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import io.github.wulkanowy.ui.modules.message.send.RecipientChipItem
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Serializable
|
||||
data class MessageDraft(
|
||||
val recipients: List<RecipientChipItem>,
|
||||
val subject: String,
|
||||
|
@ -1,36 +1,19 @@
|
||||
package io.github.wulkanowy.data.pojos
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.PluralsRes
|
||||
import androidx.annotation.StringRes
|
||||
import android.content.Intent
|
||||
import io.github.wulkanowy.services.sync.notifications.NotificationType
|
||||
import io.github.wulkanowy.ui.modules.main.MainView
|
||||
|
||||
sealed interface NotificationData {
|
||||
data class NotificationData(
|
||||
val intentToStart: Intent,
|
||||
val title: String,
|
||||
val content: String
|
||||
)
|
||||
|
||||
data class GroupNotificationData(
|
||||
val notificationDataList: List<NotificationData>,
|
||||
val title: String,
|
||||
val content: String,
|
||||
val intentToStart: Intent,
|
||||
val type: NotificationType
|
||||
val startMenu: MainView.Section
|
||||
val icon: Int
|
||||
val titleStringRes: Int
|
||||
val contentStringRes: Int
|
||||
}
|
||||
)
|
||||
|
||||
data class MultipleNotificationsData(
|
||||
override val type: NotificationType,
|
||||
override val startMenu: MainView.Section,
|
||||
@DrawableRes override val icon: Int,
|
||||
@PluralsRes override val titleStringRes: Int,
|
||||
@PluralsRes override val contentStringRes: Int,
|
||||
|
||||
@PluralsRes val summaryStringRes: Int,
|
||||
val lines: List<String>,
|
||||
) : NotificationData
|
||||
|
||||
data class OneNotificationData(
|
||||
override val type: NotificationType,
|
||||
override val startMenu: MainView.Section,
|
||||
@DrawableRes override val icon: Int,
|
||||
@StringRes override val titleStringRes: Int,
|
||||
@StringRes override val contentStringRes: Int,
|
||||
|
||||
val contentValues: List<String>,
|
||||
) : NotificationData
|
||||
|
@ -0,0 +1,52 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import io.github.wulkanowy.data.api.AdminMessageService
|
||||
import io.github.wulkanowy.data.db.dao.AdminMessageDao
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.utils.AppInfo
|
||||
import io.github.wulkanowy.utils.AutoRefreshHelper
|
||||
import io.github.wulkanowy.utils.networkBoundResource
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class AdminMessageRepository @Inject constructor(
|
||||
private val adminMessageService: AdminMessageService,
|
||||
private val adminMessageDao: AdminMessageDao,
|
||||
private val appInfo: AppInfo,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
) {
|
||||
private val saveFetchResultMutex = Mutex()
|
||||
|
||||
private val cacheKey = "admin_messages"
|
||||
|
||||
suspend fun getAdminMessages(student: Student, forceRefresh: Boolean) = networkBoundResource(
|
||||
mutex = saveFetchResultMutex,
|
||||
query = { adminMessageDao.loadAll() },
|
||||
fetch = { adminMessageService.getAdminMessages() },
|
||||
shouldFetch = {
|
||||
refreshHelper.shouldBeRefreshed(cacheKey) || forceRefresh
|
||||
},
|
||||
saveFetchResult = { oldItems, newItems ->
|
||||
adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
|
||||
refreshHelper.updateLastRefreshTimestamp(cacheKey)
|
||||
},
|
||||
showSavedOnLoading = false,
|
||||
mapResult = { adminMessages ->
|
||||
adminMessages.filter { adminMessage ->
|
||||
val isCorrectRegister = adminMessage.targetRegisterHost?.let {
|
||||
student.scrapperBaseUrl.contains(it, true)
|
||||
} ?: true
|
||||
val isCorrectFlavor =
|
||||
adminMessage.targetFlavor?.equals(appInfo.buildFlavor, true) ?: true
|
||||
val isCorrectMaxVersion =
|
||||
adminMessage.versionMax?.let { it >= appInfo.versionCode } ?: true
|
||||
val isCorrectMinVersion =
|
||||
adminMessage.versionMin?.let { it <= appInfo.versionCode } ?: true
|
||||
|
||||
isCorrectRegister && isCorrectFlavor && isCorrectMaxVersion && isCorrectMinVersion
|
||||
}.maxByOrNull { it.id }
|
||||
}
|
||||
)
|
||||
}
|
@ -1,25 +1,27 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import android.content.res.AssetManager
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.data.pojos.Contributor
|
||||
import io.github.wulkanowy.utils.DispatchersProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class AppCreatorRepository @Inject constructor(
|
||||
private val assets: AssetManager,
|
||||
private val dispatchers: DispatchersProvider
|
||||
@ApplicationContext private val context: Context,
|
||||
private val dispatchers: DispatchersProvider,
|
||||
private val json: Json,
|
||||
) {
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun getAppCreators() = withContext(dispatchers.backgroundThread) {
|
||||
val moshi = Moshi.Builder().build()
|
||||
val type = Types.newParameterizedType(List::class.java, Contributor::class.java)
|
||||
val adapter = moshi.adapter<List<Contributor>>(type)
|
||||
adapter.fromJson(assets.open("contributors.json").bufferedReader().use { it.readText() })
|
||||
suspend fun getAppCreators() = withContext(dispatchers.io) {
|
||||
val inputStream = context.assets.open("contributors.json").buffered()
|
||||
json.decodeFromStream<List<Contributor>>(inputStream)
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import io.github.wulkanowy.utils.monday
|
||||
import io.github.wulkanowy.utils.networkBoundResource
|
||||
import io.github.wulkanowy.utils.sunday
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
@ -38,6 +39,7 @@ class AttendanceRepository @Inject constructor(
|
||||
start: LocalDate,
|
||||
end: LocalDate,
|
||||
forceRefresh: Boolean,
|
||||
notify: Boolean = false,
|
||||
) = networkBoundResource(
|
||||
mutex = saveFetchResultMutex,
|
||||
shouldFetch = {
|
||||
@ -56,13 +58,28 @@ class AttendanceRepository @Inject constructor(
|
||||
},
|
||||
saveFetchResult = { old, new ->
|
||||
attendanceDb.deleteAll(old uniqueSubtract new)
|
||||
attendanceDb.insertAll(new uniqueSubtract old)
|
||||
val attendanceToAdd = (new uniqueSubtract old).map { newAttendance ->
|
||||
newAttendance.apply { if (notify) isNotified = false }
|
||||
}
|
||||
attendanceDb.insertAll(attendanceToAdd)
|
||||
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
|
||||
},
|
||||
filterResult = { it.filter { item -> item.date in start..end } }
|
||||
)
|
||||
|
||||
fun getAttendanceFromDatabase(
|
||||
semester: Semester,
|
||||
start: LocalDate,
|
||||
end: LocalDate
|
||||
): Flow<List<Attendance>> {
|
||||
return attendanceDb.loadAll(semester.diaryId, semester.studentId, start, end)
|
||||
}
|
||||
|
||||
suspend fun updateTimetable(timetable: List<Attendance>) {
|
||||
return attendanceDb.updateAll(timetable)
|
||||
}
|
||||
|
||||
suspend fun excuseForAbsence(
|
||||
student: Student, semester: Semester,
|
||||
absenceList: List<Attendance>, reason: String? = null
|
||||
|
@ -61,8 +61,9 @@ class HomeworkRepository @Inject constructor(
|
||||
val homeWorkToSave = (new uniqueSubtract old).onEach {
|
||||
if (notify) it.isNotified = false
|
||||
}
|
||||
val filteredOld = old.filterNot { it.isAddedByUser }
|
||||
|
||||
homeworkDb.deleteAll(old uniqueSubtract new)
|
||||
homeworkDb.deleteAll(filteredOld uniqueSubtract new)
|
||||
homeworkDb.insertAll(homeWorkToSave)
|
||||
|
||||
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
|
||||
@ -79,4 +80,8 @@ class HomeworkRepository @Inject constructor(
|
||||
homeworkDb.loadAll(semester.semesterId, semester.studentId, start.monday, end.sunday)
|
||||
|
||||
suspend fun updateHomework(homework: List<Homework>) = homeworkDb.updateAll(homework)
|
||||
|
||||
suspend fun saveHomework(homework: Homework) = homeworkDb.insertAll(listOf(homework))
|
||||
|
||||
suspend fun deleteHomework(homework: Homework) = homeworkDb.deleteAll(listOf(homework))
|
||||
}
|
||||
|
@ -15,24 +15,23 @@ class LoggerRepository @Inject constructor(
|
||||
|
||||
suspend fun getLastLogLines() = getLastModified().readText().split("\n")
|
||||
|
||||
suspend fun getLogFiles() = withContext(dispatchers.backgroundThread) {
|
||||
File(context.filesDir.absolutePath).listFiles(File::isFile)?.filter {
|
||||
it.name.endsWith(".log")
|
||||
}!!
|
||||
suspend fun getLogFiles() = withContext(dispatchers.io) {
|
||||
File(context.filesDir.absolutePath).listFiles(File::isFile)
|
||||
?.filter { it.name.endsWith(".log") }!!
|
||||
}
|
||||
|
||||
private suspend fun getLastModified(): File {
|
||||
return withContext(dispatchers.backgroundThread) {
|
||||
var lastModifiedTime = Long.MIN_VALUE
|
||||
var chosenFile: File? = null
|
||||
File(context.filesDir.absolutePath).listFiles(File::isFile)?.forEach { file ->
|
||||
private suspend fun getLastModified() = withContext(dispatchers.io) {
|
||||
var lastModifiedTime = Long.MIN_VALUE
|
||||
var chosenFile: File? = null
|
||||
|
||||
File(context.filesDir.absolutePath).listFiles(File::isFile)
|
||||
?.forEach { file ->
|
||||
if (file.lastModified() > lastModifiedTime) {
|
||||
lastModifiedTime = file.lastModified()
|
||||
chosenFile = file
|
||||
}
|
||||
}
|
||||
if (chosenFile == null) throw FileNotFoundException("Log file not found")
|
||||
chosenFile!!
|
||||
}
|
||||
|
||||
chosenFile ?: throw FileNotFoundException("Log file not found")
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package io.github.wulkanowy.data.repositories
|
||||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.Moshi
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.Resource
|
||||
@ -18,7 +17,6 @@ import io.github.wulkanowy.data.enums.MessageFolder.RECEIVED
|
||||
import io.github.wulkanowy.data.mappers.mapFromEntities
|
||||
import io.github.wulkanowy.data.mappers.mapToEntities
|
||||
import io.github.wulkanowy.data.pojos.MessageDraft
|
||||
import io.github.wulkanowy.data.pojos.MessageDraftJsonAdapter
|
||||
import io.github.wulkanowy.sdk.Sdk
|
||||
import io.github.wulkanowy.sdk.pojo.Folder
|
||||
import io.github.wulkanowy.sdk.pojo.SentMessage
|
||||
@ -29,6 +27,9 @@ import io.github.wulkanowy.utils.networkBoundResource
|
||||
import io.github.wulkanowy.utils.uniqueSubtract
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
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
|
||||
@ -42,7 +43,7 @@ class MessageRepository @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val refreshHelper: AutoRefreshHelper,
|
||||
private val sharedPrefProvider: SharedPrefProvider,
|
||||
private val moshi: Moshi,
|
||||
private val json: Json,
|
||||
) {
|
||||
|
||||
private val saveFetchResultMutex = Mutex()
|
||||
@ -168,9 +169,9 @@ class MessageRepository @Inject constructor(
|
||||
|
||||
var draftMessage: MessageDraft?
|
||||
get() = sharedPrefProvider.getString(context.getString(R.string.pref_key_message_send_draft))
|
||||
?.let { MessageDraftJsonAdapter(moshi).fromJson(it) }
|
||||
?.let { json.decodeFromString(it) }
|
||||
set(value) = sharedPrefProvider.putString(
|
||||
context.getString(R.string.pref_key_message_send_draft),
|
||||
value?.let { MessageDraftJsonAdapter(moshi).toJson(it) }
|
||||
value?.let { json.encodeToString(it) }
|
||||
)
|
||||
}
|
||||
|
@ -5,20 +5,23 @@ import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import com.fredporciuncula.flow.preferences.FlowSharedPreferences
|
||||
import com.fredporciuncula.flow.preferences.Preference
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.adapter
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.sdk.toLocalDate
|
||||
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
|
||||
import io.github.wulkanowy.ui.modules.grade.GradeAverageMode
|
||||
import io.github.wulkanowy.ui.modules.grade.GradeExpandMode
|
||||
import io.github.wulkanowy.ui.modules.grade.GradeSortingMode
|
||||
import io.github.wulkanowy.utils.toLocalDateTime
|
||||
import io.github.wulkanowy.utils.toTimestamp
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.lang.ClassCastException
|
||||
import java.lang.IllegalStateException
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import javax.inject.Inject
|
||||
@ -27,16 +30,12 @@ import javax.inject.Singleton
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Singleton
|
||||
class PreferencesRepository @Inject constructor(
|
||||
@ApplicationContext val context: Context,
|
||||
private val sharedPref: SharedPreferences,
|
||||
private val flowSharedPref: FlowSharedPreferences,
|
||||
@ApplicationContext val context: Context,
|
||||
moshi: Moshi
|
||||
private val json: Json,
|
||||
) {
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
private val dashboardItemsPositionAdapter: JsonAdapter<Map<DashboardItem.Type, Int>> =
|
||||
moshi.adapter()
|
||||
|
||||
val startMenuIndex: Int
|
||||
get() = getString(R.string.pref_key_start_menu, R.string.pref_default_startup).toInt()
|
||||
|
||||
@ -60,8 +59,13 @@ class PreferencesRepository @Inject constructor(
|
||||
R.bool.pref_default_grade_average_force_calc
|
||||
)
|
||||
|
||||
val isGradeExpandable: Boolean
|
||||
get() = !getBoolean(R.string.pref_key_expand_grade, R.bool.pref_default_expand_grade)
|
||||
val gradeExpandMode: GradeExpandMode
|
||||
get() = GradeExpandMode.getByValue(
|
||||
getString(
|
||||
R.string.pref_key_expand_grade_mode,
|
||||
R.string.pref_default_expand_grade_mode
|
||||
)
|
||||
)
|
||||
|
||||
val showAllSubjectsOnStatisticsList: Boolean
|
||||
get() = getBoolean(
|
||||
@ -197,14 +201,14 @@ class PreferencesRepository @Inject constructor(
|
||||
|
||||
var dashboardItemsPosition: Map<DashboardItem.Type, Int>?
|
||||
get() {
|
||||
val json = sharedPref.getString(PREF_KEY_DASHBOARD_ITEMS_POSITION, null) ?: return null
|
||||
val value = sharedPref.getString(PREF_KEY_DASHBOARD_ITEMS_POSITION, null) ?: return null
|
||||
|
||||
return dashboardItemsPositionAdapter.fromJson(json)
|
||||
return json.decodeFromString(value)
|
||||
}
|
||||
set(value) = sharedPref.edit {
|
||||
putString(
|
||||
PREF_KEY_DASHBOARD_ITEMS_POSITION,
|
||||
dashboardItemsPositionAdapter.toJson(value)
|
||||
json.encodeToString(value)
|
||||
)
|
||||
}
|
||||
|
||||
@ -213,6 +217,7 @@ class PreferencesRepository @Inject constructor(
|
||||
.map { set ->
|
||||
set.map { DashboardItem.Tile.valueOf(it) }
|
||||
.plus(DashboardItem.Tile.ACCOUNT)
|
||||
.plus(DashboardItem.Tile.ADMIN_MESSAGE)
|
||||
.toSet()
|
||||
}
|
||||
|
||||
@ -220,6 +225,7 @@ class PreferencesRepository @Inject constructor(
|
||||
get() = selectedDashboardTilesPreference.get()
|
||||
.map { DashboardItem.Tile.valueOf(it) }
|
||||
.plus(DashboardItem.Tile.ACCOUNT)
|
||||
.plus(DashboardItem.Tile.ADMIN_MESSAGE)
|
||||
.toSet()
|
||||
set(value) {
|
||||
val filteredValue = value.filterNot { it == DashboardItem.Tile.ACCOUNT }
|
||||
@ -267,6 +273,9 @@ class PreferencesRepository @Inject constructor(
|
||||
private fun getBoolean(id: String, default: Int) =
|
||||
sharedPref.getBoolean(id, context.resources.getBoolean(default))
|
||||
|
||||
private fun getBoolean(id: Int, default: Boolean) =
|
||||
sharedPref.getBoolean(context.getString(id), default)
|
||||
|
||||
private companion object {
|
||||
|
||||
private const val PREF_KEY_DASHBOARD_ITEMS_POSITION = "dashboard_items_position"
|
||||
|
@ -26,7 +26,7 @@ class SemesterRepository @Inject constructor(
|
||||
student: Student,
|
||||
forceRefresh: Boolean = false,
|
||||
refreshOnNoCurrent: Boolean = false
|
||||
) = withContext(dispatchers.backgroundThread) {
|
||||
) = withContext(dispatchers.io) {
|
||||
val semesters = semesterDb.loadAll(student.studentId, student.classId)
|
||||
|
||||
if (isShouldFetch(student, semesters, forceRefresh, refreshOnNoCurrent)) {
|
||||
@ -64,7 +64,7 @@ class SemesterRepository @Inject constructor(
|
||||
}
|
||||
|
||||
suspend fun getCurrentSemester(student: Student, forceRefresh: Boolean = false) =
|
||||
withContext(dispatchers.backgroundThread) {
|
||||
withContext(dispatchers.io) {
|
||||
getSemesters(student, forceRefresh).getCurrentOrLast()
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ class StudentRepository @Inject constructor(
|
||||
.map {
|
||||
it.apply {
|
||||
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
|
||||
student.password = withContext(dispatchers.backgroundThread) {
|
||||
student.password = withContext(dispatchers.io) {
|
||||
decrypt(student.password)
|
||||
}
|
||||
}
|
||||
@ -77,7 +77,7 @@ class StudentRepository @Inject constructor(
|
||||
val student = studentDb.loadById(id) ?: throw NoCurrentStudentException()
|
||||
|
||||
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
|
||||
student.password = withContext(dispatchers.backgroundThread) {
|
||||
student.password = withContext(dispatchers.io) {
|
||||
decrypt(student.password)
|
||||
}
|
||||
}
|
||||
@ -88,7 +88,7 @@ class StudentRepository @Inject constructor(
|
||||
val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException()
|
||||
|
||||
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
|
||||
student.password = withContext(dispatchers.backgroundThread) {
|
||||
student.password = withContext(dispatchers.io) {
|
||||
decrypt(student.password)
|
||||
}
|
||||
}
|
||||
@ -101,7 +101,7 @@ class StudentRepository @Inject constructor(
|
||||
.map {
|
||||
it.apply {
|
||||
if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) {
|
||||
password = withContext(dispatchers.backgroundThread) {
|
||||
password = withContext(dispatchers.io) {
|
||||
encrypt(password, context)
|
||||
}
|
||||
}
|
||||
|
@ -47,6 +47,7 @@ class TimetableRepository @Inject constructor(
|
||||
end: LocalDate,
|
||||
forceRefresh: Boolean,
|
||||
refreshAdditional: Boolean = false,
|
||||
notify: Boolean = false
|
||||
) = networkBoundResource(
|
||||
mutex = saveFetchResultMutex,
|
||||
shouldFetch = { (timetable, additional, headers) ->
|
||||
@ -67,7 +68,7 @@ class TimetableRepository @Inject constructor(
|
||||
timetableFull.mapToEntities(semester)
|
||||
},
|
||||
saveFetchResult = { timetableOld, timetableNew ->
|
||||
refreshTimetable(student, timetableOld.lessons, timetableNew.lessons)
|
||||
refreshTimetable(student, timetableOld.lessons, timetableNew.lessons, notify)
|
||||
refreshAdditional(timetableOld.additional, timetableNew.additional)
|
||||
refreshDayHeaders(timetableOld.headers, timetableNew.headers)
|
||||
|
||||
@ -117,13 +118,28 @@ class TimetableRepository @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun getTimetableFromDatabase(
|
||||
semester: Semester,
|
||||
from: LocalDate,
|
||||
end: LocalDate
|
||||
): Flow<List<Timetable>> {
|
||||
return timetableDb.loadAll(semester.diaryId, semester.studentId, from, end)
|
||||
}
|
||||
|
||||
suspend fun updateTimetable(timetable: List<Timetable>) {
|
||||
return timetableDb.updateAll(timetable)
|
||||
}
|
||||
|
||||
private suspend fun refreshTimetable(
|
||||
student: Student,
|
||||
lessonsOld: List<Timetable>,
|
||||
lessonsNew: List<Timetable>,
|
||||
notify: Boolean
|
||||
) {
|
||||
val lessonsToRemove = lessonsOld uniqueSubtract lessonsNew
|
||||
val lessonsToAdd = lessonsNew uniqueSubtract lessonsOld
|
||||
val lessonsToAdd = (lessonsNew uniqueSubtract lessonsOld).map { new ->
|
||||
new.apply { if (notify) isNotified = false }
|
||||
}
|
||||
|
||||
timetableDb.deleteAll(lessonsToRemove)
|
||||
timetableDb.insertAll(lessonsToAdd)
|
||||
|
@ -15,6 +15,7 @@ import dagger.multibindings.IntoSet
|
||||
import io.github.wulkanowy.services.sync.channels.Channel
|
||||
import io.github.wulkanowy.services.sync.channels.DebugChannel
|
||||
import io.github.wulkanowy.services.sync.channels.LuckyNumberChannel
|
||||
import io.github.wulkanowy.services.sync.channels.NewAttendanceChannel
|
||||
import io.github.wulkanowy.services.sync.channels.NewConferencesChannel
|
||||
import io.github.wulkanowy.services.sync.channels.NewExamChannel
|
||||
import io.github.wulkanowy.services.sync.channels.NewGradesChannel
|
||||
@ -23,6 +24,7 @@ import io.github.wulkanowy.services.sync.channels.NewMessagesChannel
|
||||
import io.github.wulkanowy.services.sync.channels.NewNotesChannel
|
||||
import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel
|
||||
import io.github.wulkanowy.services.sync.channels.PushChannel
|
||||
import io.github.wulkanowy.services.sync.channels.TimetableChangeChannel
|
||||
import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel
|
||||
import io.github.wulkanowy.services.sync.works.AttendanceSummaryWork
|
||||
import io.github.wulkanowy.services.sync.works.AttendanceWork
|
||||
@ -167,4 +169,12 @@ abstract class ServicesModule {
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun provideUpcomingLessonsChannel(channel: UpcomingLessonsChannel): Channel
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun provideChangeTimetableChannel(channel: TimetableChangeChannel): Channel
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun provideNewAttendanceChannel(channel: NewAttendanceChannel): Channel
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package io.github.wulkanowy.services.alarm
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
@ -15,8 +14,9 @@ import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.services.HiltBroadcastReceiver
|
||||
import io.github.wulkanowy.services.sync.channels.UpcomingLessonsChannel.Companion.CHANNEL_ID
|
||||
import io.github.wulkanowy.ui.modules.main.MainActivity
|
||||
import io.github.wulkanowy.ui.modules.main.MainView
|
||||
import io.github.wulkanowy.ui.modules.Destination
|
||||
import io.github.wulkanowy.ui.modules.splash.SplashActivity
|
||||
import io.github.wulkanowy.utils.PendingIntentCompat
|
||||
import io.github.wulkanowy.utils.flowWithResource
|
||||
import io.github.wulkanowy.utils.getCompatColor
|
||||
import io.github.wulkanowy.utils.toLocalDateTime
|
||||
@ -41,7 +41,7 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
|
||||
const val NOTIFICATION_TYPE_UPCOMING = 2
|
||||
const val NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION = 3
|
||||
|
||||
const val NOTIFICATION_ID = "id"
|
||||
const val NOTIFICATION_ID = 2137
|
||||
|
||||
const val STUDENT_NAME = "student_name"
|
||||
const val STUDENT_ID = "student_id"
|
||||
@ -71,11 +71,10 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
|
||||
|
||||
private fun prepareNotification(context: Context, intent: Intent) {
|
||||
val type = intent.getIntExtra(LESSON_TYPE, 0)
|
||||
val notificationId = intent.getIntExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id)
|
||||
val isPersistent = preferencesRepository.isUpcomingLessonsNotificationsPersistent
|
||||
|
||||
if (type == NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION) {
|
||||
return NotificationManagerCompat.from(context).cancel(notificationId)
|
||||
return NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID)
|
||||
}
|
||||
|
||||
val studentId = intent.getIntExtra(STUDENT_ID, 0)
|
||||
@ -92,7 +91,8 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
|
||||
|
||||
Timber.d("TimetableNotification receive: type: $type, subject: $subject, start: ${start.toLocalDateTime()}, student: $studentId")
|
||||
|
||||
showNotification(context, notificationId, isPersistent, studentName,
|
||||
showNotification(
|
||||
context, isPersistent, studentName,
|
||||
if (type == NOTIFICATION_TYPE_CURRENT) end else start, end - start,
|
||||
context.getString(
|
||||
if (type == NOTIFICATION_TYPE_CURRENT) R.string.timetable_now else R.string.timetable_next,
|
||||
@ -109,7 +109,6 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
|
||||
|
||||
private fun showNotification(
|
||||
context: Context,
|
||||
notificationId: Int,
|
||||
isPersistent: Boolean,
|
||||
studentName: String?,
|
||||
countDown: Long,
|
||||
@ -118,12 +117,13 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
|
||||
next: String?
|
||||
) {
|
||||
NotificationManagerCompat.from(context)
|
||||
.notify(notificationId, NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.notify(NOTIFICATION_ID, NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setContentTitle(title)
|
||||
.setContentText(next)
|
||||
.setAutoCancel(false)
|
||||
.setWhen(countDown)
|
||||
.setOngoing(isPersistent)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.apply {
|
||||
if (Build.VERSION.SDK_INT >= N) setUsesChronometer(true)
|
||||
}
|
||||
@ -137,9 +137,9 @@ class TimetableNotificationReceiver : HiltBroadcastReceiver() {
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
MainView.Section.TIMETABLE.id,
|
||||
MainActivity.getStartIntent(context, MainView.Section.TIMETABLE, true),
|
||||
FLAG_UPDATE_CURRENT
|
||||
NOTIFICATION_ID,
|
||||
SplashActivity.getStartIntent(context, Destination.Timetable()),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||
)
|
||||
)
|
||||
.build()
|
||||
|
@ -3,7 +3,6 @@ package io.github.wulkanowy.services.alarm
|
||||
import android.app.AlarmManager
|
||||
import android.app.AlarmManager.RTC_WAKEUP
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.AlarmManagerCompat
|
||||
@ -25,8 +24,8 @@ import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companio
|
||||
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.NOTIFICATION_TYPE_UPCOMING
|
||||
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_ID
|
||||
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_NAME
|
||||
import io.github.wulkanowy.ui.modules.main.MainView
|
||||
import io.github.wulkanowy.utils.DispatchersProvider
|
||||
import io.github.wulkanowy.utils.PendingIntentCompat
|
||||
import io.github.wulkanowy.utils.nickOrName
|
||||
import io.github.wulkanowy.utils.toTimestamp
|
||||
import kotlinx.coroutines.withContext
|
||||
@ -54,7 +53,7 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
|
||||
|
||||
suspend fun cancelScheduled(lessons: List<Timetable>, student: Student) {
|
||||
val studentId = student.studentId
|
||||
withContext(dispatchersProvider.backgroundThread) {
|
||||
withContext(dispatchersProvider.io) {
|
||||
lessons.sortedBy { it.start }.forEachIndexed { index, lesson ->
|
||||
val upcomingTime = getUpcomingLessonTime(index, lessons, lesson)
|
||||
cancelScheduledTo(
|
||||
@ -73,13 +72,19 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
|
||||
|
||||
private fun cancelScheduledTo(range: ClosedRange<LocalDateTime>, requestCode: Int) {
|
||||
if (now() in range) cancelNotification()
|
||||
|
||||
alarmManager.cancel(
|
||||
PendingIntent.getBroadcast(context, requestCode, Intent(), FLAG_UPDATE_CURRENT)
|
||||
PendingIntent.getBroadcast(
|
||||
context,
|
||||
requestCode,
|
||||
Intent(),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun cancelNotification() =
|
||||
NotificationManagerCompat.from(context).cancel(MainView.Section.TIMETABLE.id)
|
||||
NotificationManagerCompat.from(context).cancel(NOTIFICATION_ID)
|
||||
|
||||
suspend fun scheduleNotifications(lessons: List<Timetable>, student: Student) {
|
||||
if (!preferencesRepository.isUpcomingLessonsNotificationsEnable) {
|
||||
@ -91,7 +96,7 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
|
||||
return
|
||||
}
|
||||
|
||||
withContext(dispatchersProvider.backgroundThread) {
|
||||
withContext(dispatchersProvider.io) {
|
||||
lessons.groupBy { it.date }
|
||||
.map { it.value.sortedBy { lesson -> lesson.start } }
|
||||
.map { it.filter { lesson -> lesson.isStudentPlan } }
|
||||
@ -156,9 +161,8 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
|
||||
AlarmManagerCompat.setExactAndAllowWhileIdle(
|
||||
alarmManager, RTC_WAKEUP, time.toTimestamp(),
|
||||
PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also {
|
||||
it.putExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id)
|
||||
it.putExtra(LESSON_TYPE, notificationType)
|
||||
}, FLAG_UPDATE_CURRENT)
|
||||
}, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE)
|
||||
)
|
||||
Timber.d(
|
||||
"TimetableNotification scheduled: type: $notificationType, subject: ${
|
||||
|
@ -0,0 +1,90 @@
|
||||
package io.github.wulkanowy.services.shortcuts
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.ui.modules.Destination
|
||||
import io.github.wulkanowy.ui.modules.splash.SplashActivity
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ShortcutsHelper @Inject constructor(@ApplicationContext private val context: Context) {
|
||||
|
||||
private val destinations = mapOf(
|
||||
"grade" to Destination.Grade,
|
||||
"attendance" to Destination.Attendance,
|
||||
"exam" to Destination.Exam,
|
||||
"timetable" to Destination.Timetable()
|
||||
)
|
||||
|
||||
init {
|
||||
initializeShortcuts()
|
||||
}
|
||||
|
||||
fun getDestination(intent: Intent) =
|
||||
destinations[intent.getStringExtra(EXTRA_SHORTCUT_DESTINATION_ID)]
|
||||
|
||||
private fun initializeShortcuts() {
|
||||
val shortcutsInfo = listOf(
|
||||
ShortcutInfoCompat.Builder(context, "grade_shortcut")
|
||||
.setShortLabel(context.getString(R.string.grade_title))
|
||||
.setLongLabel(context.getString(R.string.grade_title))
|
||||
.setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_grade))
|
||||
.setIntent(SplashActivity.getStartIntent(context)
|
||||
.apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "grade")
|
||||
}
|
||||
)
|
||||
.build(),
|
||||
|
||||
ShortcutInfoCompat.Builder(context, "attendance_shortcut")
|
||||
.setShortLabel(context.getString(R.string.attendance_title))
|
||||
.setLongLabel(context.getString(R.string.attendance_title))
|
||||
.setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_attendance))
|
||||
.setIntent(SplashActivity.getStartIntent(context)
|
||||
.apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "attendance")
|
||||
}
|
||||
)
|
||||
.build(),
|
||||
|
||||
ShortcutInfoCompat.Builder(context, "exam_shortcut")
|
||||
.setShortLabel(context.getString(R.string.exam_title))
|
||||
.setLongLabel(context.getString(R.string.exam_title))
|
||||
.setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_exam))
|
||||
.setIntent(SplashActivity.getStartIntent(context)
|
||||
.apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "exam")
|
||||
}
|
||||
)
|
||||
.build(),
|
||||
|
||||
ShortcutInfoCompat.Builder(context, "timetable_shortcut")
|
||||
.setShortLabel(context.getString(R.string.timetable_title))
|
||||
.setLongLabel(context.getString(R.string.timetable_title))
|
||||
.setIcon(IconCompat.createWithResource(context, R.drawable.ic_shortcut_timetable))
|
||||
.setIntent(SplashActivity.getStartIntent(context)
|
||||
.apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
putExtra(EXTRA_SHORTCUT_DESTINATION_ID, "timetable")
|
||||
}
|
||||
)
|
||||
.build()
|
||||
)
|
||||
|
||||
shortcutsInfo.forEach { ShortcutManagerCompat.pushDynamicShortcut(context, it) }
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
private const val EXTRA_SHORTCUT_DESTINATION_ID = "shortcut_destination_id"
|
||||
}
|
||||
}
|
@ -74,7 +74,7 @@ class SyncManager @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun startOneTimeSyncWorker(): Flow<WorkInfo> {
|
||||
fun startOneTimeSyncWorker(): Flow<WorkInfo?> {
|
||||
val work = OneTimeWorkRequestBuilder<SyncWorker>()
|
||||
.setInputData(
|
||||
Data.Builder()
|
||||
|
@ -19,11 +19,11 @@ import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException
|
||||
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
|
||||
import io.github.wulkanowy.services.sync.channels.DebugChannel
|
||||
import io.github.wulkanowy.services.sync.works.Work
|
||||
import io.github.wulkanowy.utils.DispatchersProvider
|
||||
import io.github.wulkanowy.utils.getCompatColor
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import kotlin.random.Random
|
||||
|
||||
@HiltWorker
|
||||
@ -34,13 +34,14 @@ class SyncWorker @AssistedInject constructor(
|
||||
private val semesterRepository: SemesterRepository,
|
||||
private val works: Set<@JvmSuppressWildcards Work>,
|
||||
private val preferencesRepository: PreferencesRepository,
|
||||
private val notificationManager: NotificationManagerCompat
|
||||
private val notificationManager: NotificationManagerCompat,
|
||||
private val dispatchersProvider: DispatchersProvider
|
||||
) : CoroutineWorker(appContext, workerParameters) {
|
||||
|
||||
override suspend fun doWork() = coroutineScope {
|
||||
override suspend fun doWork() = withContext(dispatchersProvider.io) {
|
||||
Timber.i("SyncWorker is starting")
|
||||
|
||||
if (!studentRepository.isCurrentStudentSet()) return@coroutineScope Result.failure()
|
||||
if (!studentRepository.isCurrentStudentSet()) return@withContext Result.failure()
|
||||
|
||||
val student = studentRepository.getCurrentStudent()
|
||||
val semester = semesterRepository.getCurrentSemester(student, true)
|
||||
@ -50,12 +51,12 @@ class SyncWorker @AssistedInject constructor(
|
||||
Timber.i("${work::class.java.simpleName} is starting")
|
||||
work.doWork(student, semester)
|
||||
Timber.i("${work::class.java.simpleName} result: Success")
|
||||
preferencesRepository.lasSyncDate = LocalDateTime.now(ZoneId.systemDefault())
|
||||
null
|
||||
} catch (e: Throwable) {
|
||||
Timber.w("${work::class.java.simpleName} result: An exception ${e.message} occurred")
|
||||
if (e is FeatureDisabledException || e is FeatureNotAvailableException) null
|
||||
else {
|
||||
if (e is FeatureDisabledException || e is FeatureNotAvailableException) {
|
||||
null
|
||||
} else {
|
||||
Timber.e(e)
|
||||
e
|
||||
}
|
||||
@ -70,13 +71,16 @@ class SyncWorker @AssistedInject constructor(
|
||||
)
|
||||
}
|
||||
exceptions.isNotEmpty() -> Result.retry()
|
||||
else -> Result.success()
|
||||
else -> {
|
||||
preferencesRepository.lasSyncDate = LocalDateTime.now()
|
||||
Result.success()
|
||||
}
|
||||
}
|
||||
|
||||
if (preferencesRepository.isDebugNotificationEnable) notify(result)
|
||||
Timber.i("SyncWorker result: $result")
|
||||
|
||||
result
|
||||
return@withContext result
|
||||
}
|
||||
|
||||
private fun notify(result: Result) {
|
||||
|
@ -0,0 +1,36 @@
|
||||
package io.github.wulkanowy.services.sync.channels
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.R
|
||||
import javax.inject.Inject
|
||||
|
||||
@TargetApi(26)
|
||||
class NewAttendanceChannel @Inject constructor(
|
||||
private val notificationManager: NotificationManagerCompat,
|
||||
@ApplicationContext private val context: Context
|
||||
) : Channel {
|
||||
|
||||
companion object {
|
||||
const val CHANNEL_ID = "new_attendance_channel"
|
||||
}
|
||||
|
||||
override fun create() {
|
||||
notificationManager.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
context.getString(R.string.channel_new_attendance),
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
)
|
||||
.apply {
|
||||
enableLights(true)
|
||||
enableVibration(true)
|
||||
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package io.github.wulkanowy.services.sync.channels
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.R
|
||||
import javax.inject.Inject
|
||||
|
||||
@TargetApi(26)
|
||||
class TimetableChangeChannel @Inject constructor(
|
||||
private val notificationManager: NotificationManagerCompat,
|
||||
@ApplicationContext private val context: Context
|
||||
) : Channel {
|
||||
|
||||
companion object {
|
||||
const val CHANNEL_ID = "change_timetable_channel"
|
||||
}
|
||||
|
||||
override fun create() {
|
||||
notificationManager.createNotificationChannel(
|
||||
NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
context.getString(R.string.channel_change_timetable),
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
)
|
||||
.apply {
|
||||
enableLights(true)
|
||||
enableVibration(true)
|
||||
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
|
||||
})
|
||||
}
|
||||
}
|
@ -10,12 +10,11 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.Notification
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
|
||||
import io.github.wulkanowy.data.pojos.GroupNotificationData
|
||||
import io.github.wulkanowy.data.pojos.NotificationData
|
||||
import io.github.wulkanowy.data.pojos.OneNotificationData
|
||||
import io.github.wulkanowy.data.repositories.NotificationRepository
|
||||
import io.github.wulkanowy.ui.modules.main.MainActivity
|
||||
import io.github.wulkanowy.utils.AppInfo
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.utils.PendingIntentCompat
|
||||
import io.github.wulkanowy.utils.getCompatBitmap
|
||||
import io.github.wulkanowy.utils.getCompatColor
|
||||
import io.github.wulkanowy.utils.nickOrName
|
||||
@ -26,120 +25,156 @@ import kotlin.random.Random
|
||||
class AppNotificationManager @Inject constructor(
|
||||
private val notificationManager: NotificationManagerCompat,
|
||||
@ApplicationContext private val context: Context,
|
||||
private val appInfo: AppInfo,
|
||||
private val studentRepository: StudentRepository,
|
||||
private val notificationRepository: NotificationRepository
|
||||
) {
|
||||
|
||||
suspend fun sendNotification(notificationData: NotificationData, student: Student) =
|
||||
when (notificationData) {
|
||||
is OneNotificationData -> sendOneNotification(notificationData, student)
|
||||
is MultipleNotificationsData -> sendMultipleNotifications(notificationData, student)
|
||||
}
|
||||
|
||||
private suspend fun sendOneNotification(
|
||||
notificationData: OneNotificationData,
|
||||
student: Student
|
||||
) {
|
||||
val content = context.getString(
|
||||
notificationData.contentStringRes,
|
||||
*notificationData.contentValues.toTypedArray()
|
||||
)
|
||||
|
||||
val title = context.getString(notificationData.titleStringRes)
|
||||
|
||||
val notification = getDefaultNotificationBuilder(notificationData)
|
||||
.setContentTitle(title)
|
||||
.setContentText(content)
|
||||
.setStyle(
|
||||
NotificationCompat.BigTextStyle()
|
||||
.setSummaryText(student.nickOrName)
|
||||
.bigText(content)
|
||||
)
|
||||
.build()
|
||||
|
||||
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), notification)
|
||||
|
||||
saveNotification(title, content, notificationData, student)
|
||||
}
|
||||
|
||||
private suspend fun sendMultipleNotifications(
|
||||
notificationData: MultipleNotificationsData,
|
||||
student: Student
|
||||
) {
|
||||
val groupType = notificationData.type.group ?: return
|
||||
val group = "${groupType}_${student.id}"
|
||||
val groupId = student.id * 100 + notificationData.type.ordinal
|
||||
|
||||
notificationData.lines.forEach { item ->
|
||||
val title = context.resources.getQuantityString(notificationData.titleStringRes, 1)
|
||||
|
||||
val notification = getDefaultNotificationBuilder(notificationData)
|
||||
.setContentTitle(title)
|
||||
.setContentText(item)
|
||||
.setStyle(
|
||||
NotificationCompat.BigTextStyle()
|
||||
.setSummaryText(student.nickOrName)
|
||||
.bigText(item)
|
||||
)
|
||||
.setGroup(group)
|
||||
.build()
|
||||
|
||||
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), notification)
|
||||
|
||||
saveNotification(title, item, notificationData, student)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
|
||||
|
||||
val summaryNotification = getDefaultNotificationBuilder(notificationData)
|
||||
.setSmallIcon(notificationData.icon)
|
||||
.setGroup(group)
|
||||
.setStyle(NotificationCompat.InboxStyle().setSummaryText(student.nickOrName))
|
||||
.setGroupSummary(true)
|
||||
.build()
|
||||
|
||||
notificationManager.notify(groupId.toInt(), summaryNotification)
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun getDefaultNotificationBuilder(notificationData: NotificationData): NotificationCompat.Builder {
|
||||
val pendingIntentsFlags = if (appInfo.systemVersion >= Build.VERSION_CODES.M) {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
} else {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
}
|
||||
|
||||
return NotificationCompat.Builder(context, notificationData.type.channel)
|
||||
.setLargeIcon(context.getCompatBitmap(notificationData.icon, R.color.colorPrimary))
|
||||
suspend fun sendSingleNotification(
|
||||
notificationData: NotificationData,
|
||||
notificationType: NotificationType,
|
||||
student: Student
|
||||
) {
|
||||
val notification = NotificationCompat.Builder(context, notificationType.channel)
|
||||
.setLargeIcon(context.getCompatBitmap(notificationType.icon, R.color.colorPrimary))
|
||||
.setSmallIcon(R.drawable.ic_stat_all)
|
||||
.setAutoCancel(true)
|
||||
.setDefaults(NotificationCompat.DEFAULT_ALL)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setColor(context.getCompatColor(R.color.colorPrimary))
|
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
notificationData.startMenu.id,
|
||||
MainActivity.getStartIntent(context, notificationData.startMenu, true),
|
||||
pendingIntentsFlags
|
||||
Random.nextInt(),
|
||||
notificationData.intentToStart,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||
)
|
||||
)
|
||||
.setContentTitle(notificationData.title)
|
||||
.setContentText(notificationData.content)
|
||||
.setStyle(
|
||||
NotificationCompat.BigTextStyle()
|
||||
.bigText(notificationData.content)
|
||||
.also { builder ->
|
||||
if (shouldShowStudentName()) {
|
||||
builder.setSummaryText(student.nickOrName)
|
||||
}
|
||||
}
|
||||
)
|
||||
.build()
|
||||
|
||||
notificationManager.notify(Random.nextInt(), notification)
|
||||
saveNotification(notificationData, notificationType, student)
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
suspend fun sendMultipleNotifications(
|
||||
groupNotificationData: GroupNotificationData,
|
||||
student: Student
|
||||
) {
|
||||
val notificationType = groupNotificationData.type
|
||||
val groupType = notificationType.group ?: return
|
||||
val group = "${groupType}_${student.id}"
|
||||
|
||||
sendSummaryNotification(groupNotificationData, group, student)
|
||||
|
||||
groupNotificationData.notificationDataList.forEach { notificationData ->
|
||||
val notification = NotificationCompat.Builder(context, notificationType.channel)
|
||||
.setLargeIcon(context.getCompatBitmap(notificationType.icon, R.color.colorPrimary))
|
||||
.setSmallIcon(R.drawable.ic_stat_all)
|
||||
.setAutoCancel(true)
|
||||
.setDefaults(NotificationCompat.DEFAULT_ALL)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setColor(context.getCompatColor(R.color.colorPrimary))
|
||||
.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
Random.nextInt(),
|
||||
notificationData.intentToStart,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||
)
|
||||
)
|
||||
.setContentTitle(notificationData.title)
|
||||
.setContentText(notificationData.content)
|
||||
.setStyle(
|
||||
NotificationCompat.BigTextStyle()
|
||||
.bigText(notificationData.content)
|
||||
.also { builder ->
|
||||
if (shouldShowStudentName()) {
|
||||
builder.setSummaryText(student.nickOrName)
|
||||
}
|
||||
}
|
||||
)
|
||||
.setGroup(group)
|
||||
.build()
|
||||
|
||||
notificationManager.notify(Random.nextInt(), notification)
|
||||
saveNotification(notificationData, groupNotificationData.type, student)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendSummaryNotification(
|
||||
groupNotificationData: GroupNotificationData,
|
||||
group: String,
|
||||
student: Student
|
||||
) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
|
||||
|
||||
val summaryNotification =
|
||||
NotificationCompat.Builder(context, groupNotificationData.type.channel)
|
||||
.setContentTitle(groupNotificationData.title)
|
||||
.setContentText(groupNotificationData.content)
|
||||
.setSmallIcon(groupNotificationData.type.icon)
|
||||
.setAutoCancel(true)
|
||||
.setDefaults(NotificationCompat.DEFAULT_ALL)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setColor(context.getCompatColor(R.color.colorPrimary))
|
||||
.setStyle(
|
||||
NotificationCompat.InboxStyle()
|
||||
.also { builder ->
|
||||
if (shouldShowStudentName()) {
|
||||
builder.setSummaryText(student.nickOrName)
|
||||
}
|
||||
groupNotificationData.notificationDataList.forEach {
|
||||
builder.addLine(it.content)
|
||||
}
|
||||
}
|
||||
)
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
context,
|
||||
Random.nextInt(),
|
||||
groupNotificationData.intentToStart,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
|
||||
)
|
||||
)
|
||||
.setLocalOnly(true)
|
||||
.setGroup(group)
|
||||
.setGroupSummary(true)
|
||||
.build()
|
||||
|
||||
val groupId = student.id * 100 + groupNotificationData.type.ordinal
|
||||
notificationManager.notify(groupId.toInt(), summaryNotification)
|
||||
}
|
||||
|
||||
private suspend fun saveNotification(
|
||||
title: String,
|
||||
content: String,
|
||||
notificationData: NotificationData,
|
||||
notificationType: NotificationType,
|
||||
student: Student
|
||||
) {
|
||||
val notificationEntity = Notification(
|
||||
studentId = student.id,
|
||||
title = title,
|
||||
content = content,
|
||||
type = notificationData.type,
|
||||
title = notificationData.title,
|
||||
content = notificationData.content,
|
||||
type = notificationType,
|
||||
date = LocalDateTime.now()
|
||||
)
|
||||
|
||||
notificationRepository.saveNotification(notificationEntity)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun shouldShowStudentName(): Boolean =
|
||||
studentRepository.getSavedStudents(decryptPass = false).size > 1
|
||||
}
|
||||
|
@ -0,0 +1,124 @@
|
||||
package io.github.wulkanowy.services.sync.notifications
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.db.entities.Timetable
|
||||
import io.github.wulkanowy.data.pojos.GroupNotificationData
|
||||
import io.github.wulkanowy.data.pojos.NotificationData
|
||||
import io.github.wulkanowy.ui.modules.Destination
|
||||
import io.github.wulkanowy.ui.modules.splash.SplashActivity
|
||||
import io.github.wulkanowy.utils.getPlural
|
||||
import io.github.wulkanowy.utils.toFormattedString
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import javax.inject.Inject
|
||||
|
||||
class ChangeTimetableNotification @Inject constructor(
|
||||
private val appNotificationManager: AppNotificationManager,
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
|
||||
suspend fun notify(items: List<Timetable>, student: Student) {
|
||||
val currentTime = LocalDateTime.now()
|
||||
val changedLessons = items.filter { (it.canceled || it.changes) && it.start > currentTime }
|
||||
val notificationDataList = changedLessons.groupBy { it.date }
|
||||
.map { (date, lessons) ->
|
||||
getNotificationContents(date, lessons).map {
|
||||
NotificationData(
|
||||
title = context.getPlural(
|
||||
R.plurals.timetable_notify_new_items_title,
|
||||
1
|
||||
),
|
||||
content = it,
|
||||
intentToStart = SplashActivity.getStartIntent(
|
||||
context = context,
|
||||
destination = Destination.Timetable(date)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.flatten()
|
||||
.ifEmpty { return }
|
||||
|
||||
val groupNotificationData = GroupNotificationData(
|
||||
notificationDataList = notificationDataList,
|
||||
title = context.getPlural(
|
||||
R.plurals.timetable_notify_new_items_title,
|
||||
changedLessons.size
|
||||
),
|
||||
content = context.getPlural(
|
||||
R.plurals.timetable_notify_new_items_group,
|
||||
changedLessons.size,
|
||||
changedLessons.size
|
||||
),
|
||||
intentToStart = SplashActivity.getStartIntent(context, Destination.Timetable()),
|
||||
type = NotificationType.CHANGE_TIMETABLE
|
||||
)
|
||||
|
||||
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
|
||||
}
|
||||
|
||||
private fun getNotificationContents(date: LocalDate, lessons: List<Timetable>): List<String> {
|
||||
val formattedDate = date.toFormattedString("EEE dd.MM")
|
||||
|
||||
return if (lessons.size > 2) {
|
||||
listOf(
|
||||
context.getPlural(
|
||||
R.plurals.timetable_notify_new_items,
|
||||
lessons.size,
|
||||
formattedDate,
|
||||
lessons.size,
|
||||
)
|
||||
)
|
||||
} else {
|
||||
lessons.map {
|
||||
buildString {
|
||||
append(
|
||||
context.getString(
|
||||
R.string.timetable_notify_lesson,
|
||||
formattedDate,
|
||||
it.number,
|
||||
it.subject
|
||||
)
|
||||
)
|
||||
if (it.roomOld.isNotBlank()) {
|
||||
appendLine()
|
||||
append(
|
||||
context.getString(
|
||||
R.string.timetable_notify_change_room,
|
||||
it.roomOld,
|
||||
it.room
|
||||
)
|
||||
)
|
||||
}
|
||||
if (it.teacherOld.isNotBlank() && it.teacher != it.teacherOld) {
|
||||
appendLine()
|
||||
append(
|
||||
context.getString(
|
||||
R.string.timetable_notify_change_teacher,
|
||||
it.teacherOld,
|
||||
it.teacher
|
||||
)
|
||||
)
|
||||
}
|
||||
if (it.subjectOld.isNotBlank()) {
|
||||
appendLine()
|
||||
append(
|
||||
context.getString(
|
||||
R.string.timetable_notify_change_subject,
|
||||
it.subjectOld,
|
||||
it.subject
|
||||
)
|
||||
)
|
||||
}
|
||||
if (it.info.isNotBlank()) {
|
||||
appendLine()
|
||||
append(it.info)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
package io.github.wulkanowy.services.sync.notifications
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.Attendance
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.pojos.GroupNotificationData
|
||||
import io.github.wulkanowy.data.pojos.NotificationData
|
||||
import io.github.wulkanowy.ui.modules.Destination
|
||||
import io.github.wulkanowy.ui.modules.splash.SplashActivity
|
||||
import io.github.wulkanowy.utils.descriptionRes
|
||||
import io.github.wulkanowy.utils.getPlural
|
||||
import io.github.wulkanowy.utils.toFormattedString
|
||||
import javax.inject.Inject
|
||||
|
||||
class NewAttendanceNotification @Inject constructor(
|
||||
private val appNotificationManager: AppNotificationManager,
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
|
||||
suspend fun notify(items: List<Attendance>, student: Student) {
|
||||
val lines = items.filterNot { it.presence || it.name == "UNKNOWN" }
|
||||
.map {
|
||||
val description = context.getString(it.descriptionRes)
|
||||
"${it.date.toFormattedString("dd.MM")} - ${it.subject}: $description"
|
||||
}
|
||||
.ifEmpty { return }
|
||||
|
||||
val notificationDataList = lines.map {
|
||||
NotificationData(
|
||||
title = context.getPlural(R.plurals.attendance_notify_new_items_title, 1),
|
||||
content = it,
|
||||
intentToStart = SplashActivity.getStartIntent(context, Destination.Attendance)
|
||||
)
|
||||
}
|
||||
|
||||
val groupNotificationData = GroupNotificationData(
|
||||
notificationDataList = notificationDataList,
|
||||
title = context.getPlural(
|
||||
R.plurals.attendance_notify_new_items_title,
|
||||
notificationDataList.size
|
||||
),
|
||||
content = context.getPlural(
|
||||
R.plurals.attendance_notify_new_items,
|
||||
notificationDataList.size,
|
||||
notificationDataList.size
|
||||
),
|
||||
intentToStart = SplashActivity.getStartIntent(context, Destination.Attendance),
|
||||
type = NotificationType.NEW_ATTENDANCE
|
||||
)
|
||||
|
||||
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
|
||||
}
|
||||
}
|
@ -1,34 +1,52 @@
|
||||
package io.github.wulkanowy.services.sync.notifications
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.Conference
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
|
||||
import io.github.wulkanowy.ui.modules.main.MainView
|
||||
import io.github.wulkanowy.data.pojos.GroupNotificationData
|
||||
import io.github.wulkanowy.data.pojos.NotificationData
|
||||
import io.github.wulkanowy.ui.modules.Destination
|
||||
import io.github.wulkanowy.ui.modules.splash.SplashActivity
|
||||
import io.github.wulkanowy.utils.getPlural
|
||||
import io.github.wulkanowy.utils.toFormattedString
|
||||
import java.time.LocalDateTime
|
||||
import javax.inject.Inject
|
||||
|
||||
class NewConferenceNotification @Inject constructor(
|
||||
private val appNotificationManager: AppNotificationManager
|
||||
private val appNotificationManager: AppNotificationManager,
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
|
||||
suspend fun notify(items: List<Conference>, student: Student) {
|
||||
val today = LocalDateTime.now()
|
||||
val lines = items.filter { !it.date.isBefore(today) }.map {
|
||||
"${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}"
|
||||
}.ifEmpty { return }
|
||||
val lines = items.filter { !it.date.isBefore(today) }
|
||||
.map {
|
||||
"${it.date.toFormattedString("dd.MM")} - ${it.title}: ${it.subject}"
|
||||
}
|
||||
.ifEmpty { return }
|
||||
|
||||
val notification = MultipleNotificationsData(
|
||||
type = NotificationType.NEW_CONFERENCE,
|
||||
icon = R.drawable.ic_more_conferences,
|
||||
titleStringRes = R.plurals.conference_notify_new_item_title,
|
||||
contentStringRes = R.plurals.conference_notify_new_items,
|
||||
summaryStringRes = R.plurals.conference_number_item,
|
||||
startMenu = MainView.Section.CONFERENCE,
|
||||
lines = lines
|
||||
val notificationDataList = lines.map {
|
||||
NotificationData(
|
||||
title = context.getPlural(R.plurals.conference_notify_new_item_title, 1),
|
||||
content = it,
|
||||
intentToStart = SplashActivity.getStartIntent(context, Destination.Conference)
|
||||
)
|
||||
}
|
||||
|
||||
val groupNotificationData = GroupNotificationData(
|
||||
notificationDataList = notificationDataList,
|
||||
title = context.getPlural(R.plurals.conference_notify_new_item_title, lines.size),
|
||||
content = context.getPlural(
|
||||
R.plurals.conference_notify_new_items,
|
||||
lines.size,
|
||||
lines.size
|
||||
),
|
||||
intentToStart = SplashActivity.getStartIntent(context, Destination.Conference),
|
||||
type = NotificationType.NEW_CONFERENCE
|
||||
)
|
||||
|
||||
appNotificationManager.sendNotification(notification, student)
|
||||
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
|
||||
}
|
||||
}
|
||||
|
@ -1,34 +1,52 @@
|
||||
package io.github.wulkanowy.services.sync.notifications
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.Exam
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
|
||||
import io.github.wulkanowy.ui.modules.main.MainView
|
||||
import io.github.wulkanowy.data.pojos.GroupNotificationData
|
||||
import io.github.wulkanowy.data.pojos.NotificationData
|
||||
import io.github.wulkanowy.ui.modules.Destination
|
||||
import io.github.wulkanowy.ui.modules.splash.SplashActivity
|
||||
import io.github.wulkanowy.utils.getPlural
|
||||
import io.github.wulkanowy.utils.toFormattedString
|
||||
import java.time.LocalDate
|
||||
import javax.inject.Inject
|
||||
|
||||
class NewExamNotification @Inject constructor(
|
||||
private val appNotificationManager: AppNotificationManager
|
||||
private val appNotificationManager: AppNotificationManager,
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
|
||||
suspend fun notify(items: List<Exam>, student: Student) {
|
||||
val today = LocalDate.now()
|
||||
val lines = items.filter { !it.date.isBefore(today) }.map {
|
||||
"${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.description}"
|
||||
}.ifEmpty { return }
|
||||
val lines = items.filter { !it.date.isBefore(today) }
|
||||
.map {
|
||||
"${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.description}"
|
||||
}
|
||||
.ifEmpty { return }
|
||||
|
||||
val notification = MultipleNotificationsData(
|
||||
type = NotificationType.NEW_EXAM,
|
||||
icon = R.drawable.ic_main_exam,
|
||||
titleStringRes = R.plurals.exam_notify_new_item_title,
|
||||
contentStringRes = R.plurals.exam_notify_new_item_content,
|
||||
summaryStringRes = R.plurals.exam_number_item,
|
||||
startMenu = MainView.Section.EXAM,
|
||||
lines = lines
|
||||
val notificationDataList = lines.map {
|
||||
NotificationData(
|
||||
title = context.getPlural(R.plurals.exam_notify_new_item_title, 1),
|
||||
content = it,
|
||||
intentToStart = SplashActivity.getStartIntent(context, Destination.Exam),
|
||||
)
|
||||
}
|
||||
|
||||
val groupNotificationData = GroupNotificationData(
|
||||
notificationDataList = notificationDataList,
|
||||
title = context.getPlural(R.plurals.exam_notify_new_item_title, lines.size),
|
||||
content = context.getPlural(
|
||||
R.plurals.exam_notify_new_item_content,
|
||||
lines.size,
|
||||
lines.size
|
||||
),
|
||||
intentToStart = SplashActivity.getStartIntent(context, Destination.Exam),
|
||||
type = NotificationType.NEW_EXAM
|
||||
)
|
||||
|
||||
appNotificationManager.sendNotification(notification, student)
|
||||
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
|
||||
}
|
||||
}
|
||||
|
@ -1,62 +1,88 @@
|
||||
package io.github.wulkanowy.services.sync.notifications
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.Grade
|
||||
import io.github.wulkanowy.data.db.entities.GradeSummary
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
|
||||
import io.github.wulkanowy.ui.modules.main.MainView
|
||||
import io.github.wulkanowy.data.pojos.GroupNotificationData
|
||||
import io.github.wulkanowy.data.pojos.NotificationData
|
||||
import io.github.wulkanowy.ui.modules.Destination
|
||||
import io.github.wulkanowy.ui.modules.splash.SplashActivity
|
||||
import io.github.wulkanowy.utils.getPlural
|
||||
import javax.inject.Inject
|
||||
|
||||
class NewGradeNotification @Inject constructor(
|
||||
private val appNotificationManager: AppNotificationManager
|
||||
private val appNotificationManager: AppNotificationManager,
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
|
||||
suspend fun notifyDetails(items: List<Grade>, student: Student) {
|
||||
val notification = MultipleNotificationsData(
|
||||
type = NotificationType.NEW_GRADE_DETAILS,
|
||||
icon = R.drawable.ic_stat_grade,
|
||||
titleStringRes = R.plurals.grade_new_items,
|
||||
contentStringRes = R.plurals.grade_notify_new_items,
|
||||
summaryStringRes = R.plurals.grade_number_item,
|
||||
startMenu = MainView.Section.GRADE,
|
||||
lines = items.map {
|
||||
"${it.subject}: ${it.entry}"
|
||||
}
|
||||
val notificationDataList = items.map {
|
||||
NotificationData(
|
||||
title = context.getPlural(R.plurals.grade_new_items, 1),
|
||||
content = "${it.subject}: ${it.entry}",
|
||||
intentToStart = SplashActivity.getStartIntent(context, Destination.Grade),
|
||||
)
|
||||
}
|
||||
|
||||
val groupNotificationData = GroupNotificationData(
|
||||
notificationDataList = notificationDataList,
|
||||
title = context.getPlural(R.plurals.grade_new_items, items.size),
|
||||
content = context.getPlural(R.plurals.grade_notify_new_items, items.size, items.size),
|
||||
intentToStart = SplashActivity.getStartIntent(context, Destination.Grade),
|
||||
type = NotificationType.NEW_GRADE_DETAILS
|
||||
)
|
||||
|
||||
appNotificationManager.sendNotification(notification, student)
|
||||
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
|
||||
}
|
||||
|
||||
suspend fun notifyPredicted(items: List<GradeSummary>, student: Student) {
|
||||
val notification = MultipleNotificationsData(
|
||||
type = NotificationType.NEW_GRADE_PREDICTED,
|
||||
icon = R.drawable.ic_stat_grade,
|
||||
titleStringRes = R.plurals.grade_new_items_predicted,
|
||||
contentStringRes = R.plurals.grade_notify_new_items_predicted,
|
||||
summaryStringRes = R.plurals.grade_number_item,
|
||||
startMenu = MainView.Section.GRADE,
|
||||
lines = items.map {
|
||||
"${it.subject}: ${it.predictedGrade}"
|
||||
}
|
||||
val notificationDataList = items.map {
|
||||
NotificationData(
|
||||
title = context.getPlural(R.plurals.grade_new_items_predicted, 1),
|
||||
content = "${it.subject}: ${it.predictedGrade}",
|
||||
intentToStart = SplashActivity.getStartIntent(context, Destination.Grade),
|
||||
)
|
||||
}
|
||||
|
||||
val groupNotificationData = GroupNotificationData(
|
||||
notificationDataList = notificationDataList,
|
||||
title = context.getPlural(R.plurals.grade_new_items_predicted, items.size),
|
||||
content = context.getPlural(
|
||||
R.plurals.grade_notify_new_items_predicted,
|
||||
items.size,
|
||||
items.size
|
||||
),
|
||||
intentToStart = SplashActivity.getStartIntent(context, Destination.Grade),
|
||||
type = NotificationType.NEW_GRADE_PREDICTED
|
||||
)
|
||||
|
||||
appNotificationManager.sendNotification(notification, student)
|
||||
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
|
||||
}
|
||||
|
||||
suspend fun notifyFinal(items: List<GradeSummary>, student: Student) {
|
||||
val notification = MultipleNotificationsData(
|
||||
type = NotificationType.NEW_GRADE_FINAL,
|
||||
icon = R.drawable.ic_stat_grade,
|
||||
titleStringRes = R.plurals.grade_new_items_final,
|
||||
contentStringRes = R.plurals.grade_notify_new_items_final,
|
||||
summaryStringRes = R.plurals.grade_number_item,
|
||||
startMenu = MainView.Section.GRADE,
|
||||
lines = items.map {
|
||||
"${it.subject}: ${it.finalGrade}"
|
||||
}
|
||||
val notificationDataList = items.map {
|
||||
NotificationData(
|
||||
title = context.getPlural(R.plurals.grade_new_items_final, 1),
|
||||
content = "${it.subject}: ${it.finalGrade}",
|
||||
intentToStart = SplashActivity.getStartIntent(context, Destination.Grade),
|
||||
)
|
||||
}
|
||||
|
||||
val groupNotificationData = GroupNotificationData(
|
||||
notificationDataList = notificationDataList,
|
||||
title = context.getPlural(R.plurals.grade_new_items_final, items.size),
|
||||
content = context.getPlural(
|
||||
R.plurals.grade_notify_new_items_final,
|
||||
items.size,
|
||||
items.size
|
||||
),
|
||||
intentToStart = SplashActivity.getStartIntent(context, Destination.Grade),
|
||||
type = NotificationType.NEW_GRADE_FINAL
|
||||
)
|
||||
|
||||
appNotificationManager.sendNotification(notification, student)
|
||||
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
|
||||
}
|
||||
}
|
||||
|
@ -1,34 +1,52 @@
|
||||
package io.github.wulkanowy.services.sync.notifications
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.Homework
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
|
||||
import io.github.wulkanowy.ui.modules.main.MainView
|
||||
import io.github.wulkanowy.data.pojos.GroupNotificationData
|
||||
import io.github.wulkanowy.data.pojos.NotificationData
|
||||
import io.github.wulkanowy.ui.modules.Destination
|
||||
import io.github.wulkanowy.ui.modules.splash.SplashActivity
|
||||
import io.github.wulkanowy.utils.getPlural
|
||||
import io.github.wulkanowy.utils.toFormattedString
|
||||
import java.time.LocalDate
|
||||
import javax.inject.Inject
|
||||
|
||||
class NewHomeworkNotification @Inject constructor(
|
||||
private val appNotificationManager: AppNotificationManager
|
||||
private val appNotificationManager: AppNotificationManager,
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
|
||||
suspend fun notify(items: List<Homework>, student: Student) {
|
||||
val today = LocalDate.now()
|
||||
val lines = items.filter { !it.date.isBefore(today) }.map {
|
||||
"${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}"
|
||||
}.ifEmpty { return }
|
||||
val lines = items.filter { !it.date.isBefore(today) }
|
||||
.map {
|
||||
"${it.date.toFormattedString("dd.MM")} - ${it.subject}: ${it.content}"
|
||||
}
|
||||
.ifEmpty { return }
|
||||
|
||||
val notification = MultipleNotificationsData(
|
||||
val notificationDataList = lines.map {
|
||||
NotificationData(
|
||||
title = context.getPlural(R.plurals.homework_notify_new_item_title, 1),
|
||||
content = it,
|
||||
intentToStart = SplashActivity.getStartIntent(context, Destination.Homework),
|
||||
)
|
||||
}
|
||||
|
||||
val groupNotificationData = GroupNotificationData(
|
||||
title = context.getPlural(R.plurals.homework_notify_new_item_title, lines.size),
|
||||
content = context.getPlural(
|
||||
R.plurals.homework_notify_new_item_content,
|
||||
lines.size,
|
||||
lines.size
|
||||
),
|
||||
intentToStart = SplashActivity.getStartIntent(context, Destination.Homework),
|
||||
type = NotificationType.NEW_HOMEWORK,
|
||||
icon = R.drawable.ic_more_homework,
|
||||
titleStringRes = R.plurals.homework_notify_new_item_title,
|
||||
contentStringRes = R.plurals.homework_notify_new_item_content,
|
||||
summaryStringRes = R.plurals.homework_number_item,
|
||||
startMenu = MainView.Section.HOMEWORK,
|
||||
lines = lines
|
||||
notificationDataList = notificationDataList
|
||||
)
|
||||
|
||||
appNotificationManager.sendNotification(notification, student)
|
||||
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +1,34 @@
|
||||
package io.github.wulkanowy.services.sync.notifications
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.LuckyNumber
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.pojos.OneNotificationData
|
||||
import io.github.wulkanowy.ui.modules.main.MainView
|
||||
import io.github.wulkanowy.data.pojos.NotificationData
|
||||
import io.github.wulkanowy.ui.modules.Destination
|
||||
import io.github.wulkanowy.ui.modules.splash.SplashActivity
|
||||
import javax.inject.Inject
|
||||
|
||||
class NewLuckyNumberNotification @Inject constructor(
|
||||
private val appNotificationManager: AppNotificationManager
|
||||
private val appNotificationManager: AppNotificationManager,
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
|
||||
suspend fun notify(item: LuckyNumber, student: Student) {
|
||||
val notification = OneNotificationData(
|
||||
type = NotificationType.NEW_LUCKY_NUMBER,
|
||||
icon = R.drawable.ic_stat_luckynumber,
|
||||
titleStringRes = R.string.lucky_number_notify_new_item_title,
|
||||
contentStringRes = R.string.lucky_number_notify_new_item,
|
||||
startMenu = MainView.Section.LUCKY_NUMBER,
|
||||
contentValues = listOf(item.luckyNumber.toString())
|
||||
)
|
||||
suspend fun notify(item: LuckyNumber, student: Student) {
|
||||
val notificationData = NotificationData(
|
||||
title = context.getString(R.string.lucky_number_notify_new_item_title),
|
||||
content = context.getString(
|
||||
R.string.lucky_number_notify_new_item,
|
||||
item.luckyNumber.toString()
|
||||
),
|
||||
intentToStart = SplashActivity.getStartIntent(context, Destination.LuckyNumber)
|
||||
)
|
||||
|
||||
appNotificationManager.sendNotification(notification, student)
|
||||
appNotificationManager.sendSingleNotification(
|
||||
notificationData = notificationData,
|
||||
notificationType = NotificationType.NEW_LUCKY_NUMBER,
|
||||
student = student
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,29 +1,39 @@
|
||||
package io.github.wulkanowy.services.sync.notifications
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.Message
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
|
||||
import io.github.wulkanowy.ui.modules.main.MainView
|
||||
import io.github.wulkanowy.data.pojos.GroupNotificationData
|
||||
import io.github.wulkanowy.data.pojos.NotificationData
|
||||
import io.github.wulkanowy.ui.modules.Destination
|
||||
import io.github.wulkanowy.ui.modules.splash.SplashActivity
|
||||
import io.github.wulkanowy.utils.getPlural
|
||||
import javax.inject.Inject
|
||||
|
||||
class NewMessageNotification @Inject constructor(
|
||||
private val appNotificationManager: AppNotificationManager
|
||||
private val appNotificationManager: AppNotificationManager,
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
|
||||
suspend fun notify(items: List<Message>, student: Student) {
|
||||
val notification = MultipleNotificationsData(
|
||||
type = NotificationType.NEW_MESSAGE,
|
||||
icon = R.drawable.ic_stat_message,
|
||||
titleStringRes = R.plurals.message_new_items,
|
||||
contentStringRes = R.plurals.message_notify_new_items,
|
||||
summaryStringRes = R.plurals.message_number_item,
|
||||
startMenu = MainView.Section.MESSAGE,
|
||||
lines = items.map {
|
||||
"${it.sender}: ${it.subject}"
|
||||
}
|
||||
val notificationDataList = items.map {
|
||||
NotificationData(
|
||||
title = context.getPlural(R.plurals.message_new_items, 1),
|
||||
content = "${it.sender}: ${it.subject}",
|
||||
intentToStart = SplashActivity.getStartIntent(context, Destination.Message),
|
||||
)
|
||||
}
|
||||
|
||||
val groupNotificationData = GroupNotificationData(
|
||||
notificationDataList = notificationDataList,
|
||||
title = context.getPlural(R.plurals.message_new_items, items.size),
|
||||
content = context.getPlural(R.plurals.message_notify_new_items, items.size, items.size),
|
||||
intentToStart = SplashActivity.getStartIntent(context, Destination.Message),
|
||||
type = NotificationType.NEW_MESSAGE
|
||||
)
|
||||
|
||||
appNotificationManager.sendNotification(notification, student)
|
||||
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
|
||||
}
|
||||
}
|
||||
|
@ -1,42 +1,46 @@
|
||||
package io.github.wulkanowy.services.sync.notifications
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.Note
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
|
||||
import io.github.wulkanowy.data.pojos.GroupNotificationData
|
||||
import io.github.wulkanowy.data.pojos.NotificationData
|
||||
import io.github.wulkanowy.sdk.scrapper.notes.NoteCategory
|
||||
import io.github.wulkanowy.ui.modules.main.MainView
|
||||
import io.github.wulkanowy.ui.modules.Destination
|
||||
import io.github.wulkanowy.ui.modules.splash.SplashActivity
|
||||
import io.github.wulkanowy.utils.getPlural
|
||||
import javax.inject.Inject
|
||||
|
||||
class NewNoteNotification @Inject constructor(
|
||||
private val appNotificationManager: AppNotificationManager
|
||||
private val appNotificationManager: AppNotificationManager,
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
|
||||
suspend fun notify(items: List<Note>, student: Student) {
|
||||
val notification = MultipleNotificationsData(
|
||||
type = NotificationType.NEW_NOTE,
|
||||
icon = R.drawable.ic_stat_note,
|
||||
titleStringRes = when (NoteCategory.getByValue(items.first().categoryType)) {
|
||||
val notificationDataList = items.map {
|
||||
val titleRes = when (NoteCategory.getByValue(it.categoryType)) {
|
||||
NoteCategory.POSITIVE -> R.plurals.praise_new_items
|
||||
NoteCategory.NEUTRAL -> R.plurals.neutral_note_new_items
|
||||
else -> R.plurals.note_new_items
|
||||
},
|
||||
contentStringRes = when (NoteCategory.getByValue(items.first().categoryType)) {
|
||||
NoteCategory.POSITIVE -> R.plurals.praise_notify_new_items
|
||||
NoteCategory.NEUTRAL -> R.plurals.neutral_note_notify_new_items
|
||||
else -> R.plurals.note_notify_new_items
|
||||
},
|
||||
summaryStringRes = when (NoteCategory.getByValue(items.first().categoryType)) {
|
||||
NoteCategory.POSITIVE -> R.plurals.praise_number_item
|
||||
NoteCategory.NEUTRAL -> R.plurals.neutral_note_number_item
|
||||
else -> R.plurals.note_number_item
|
||||
},
|
||||
startMenu = MainView.Section.NOTE,
|
||||
lines = items.map {
|
||||
"${it.teacher}: ${it.category}"
|
||||
}
|
||||
|
||||
NotificationData(
|
||||
title = context.getPlural(titleRes, 1),
|
||||
content = "${it.teacher}: ${it.category}",
|
||||
intentToStart = SplashActivity.getStartIntent(context, Destination.Note),
|
||||
)
|
||||
}
|
||||
|
||||
val groupNotificationData = GroupNotificationData(
|
||||
notificationDataList = notificationDataList,
|
||||
intentToStart = SplashActivity.getStartIntent(context, Destination.Note),
|
||||
title = context.getPlural(R.plurals.note_new_items, items.size),
|
||||
content = context.getPlural(R.plurals.note_notify_new_items, items.size, items.size),
|
||||
type = NotificationType.NEW_NOTE
|
||||
)
|
||||
|
||||
appNotificationManager.sendNotification(notification, student)
|
||||
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
|
||||
}
|
||||
}
|
||||
|
@ -1,29 +1,54 @@
|
||||
package io.github.wulkanowy.services.sync.notifications
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.SchoolAnnouncement
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.pojos.MultipleNotificationsData
|
||||
import io.github.wulkanowy.ui.modules.main.MainView
|
||||
import io.github.wulkanowy.data.pojos.GroupNotificationData
|
||||
import io.github.wulkanowy.data.pojos.NotificationData
|
||||
import io.github.wulkanowy.ui.modules.Destination
|
||||
import io.github.wulkanowy.ui.modules.splash.SplashActivity
|
||||
import io.github.wulkanowy.utils.getPlural
|
||||
import javax.inject.Inject
|
||||
|
||||
class NewSchoolAnnouncementNotification @Inject constructor(
|
||||
private val appNotificationManager: AppNotificationManager
|
||||
private val appNotificationManager: AppNotificationManager,
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
|
||||
suspend fun notify(items: List<SchoolAnnouncement>, student: Student) {
|
||||
val notification = MultipleNotificationsData(
|
||||
type = NotificationType.NEW_ANNOUNCEMENT,
|
||||
icon = R.drawable.ic_all_about,
|
||||
titleStringRes = R.plurals.school_announcement_notify_new_item_title,
|
||||
contentStringRes = R.plurals.school_announcement_notify_new_items,
|
||||
summaryStringRes = R.plurals.school_announcement_number_item,
|
||||
startMenu = MainView.Section.SCHOOL_ANNOUNCEMENT,
|
||||
lines = items.map {
|
||||
"${it.subject}: ${it.content}"
|
||||
}
|
||||
suspend fun notify(items: List<SchoolAnnouncement>, student: Student) {
|
||||
val notificationDataList = items.map {
|
||||
NotificationData(
|
||||
intentToStart = SplashActivity.getStartIntent(
|
||||
context = context,
|
||||
destination = Destination.SchoolAnnouncement
|
||||
),
|
||||
title = context.getPlural(
|
||||
R.plurals.school_announcement_notify_new_item_title,
|
||||
1
|
||||
),
|
||||
content = "${it.subject}: ${it.content}"
|
||||
)
|
||||
}
|
||||
val groupNotificationData = GroupNotificationData(
|
||||
type = NotificationType.NEW_ANNOUNCEMENT,
|
||||
intentToStart = SplashActivity.getStartIntent(
|
||||
context = context,
|
||||
destination = Destination.SchoolAnnouncement
|
||||
),
|
||||
title = context.getPlural(
|
||||
R.plurals.school_announcement_notify_new_item_title,
|
||||
items.size
|
||||
),
|
||||
content = context.getPlural(
|
||||
R.plurals.school_announcement_notify_new_items,
|
||||
items.size,
|
||||
items.size
|
||||
),
|
||||
notificationDataList = notificationDataList
|
||||
)
|
||||
|
||||
appNotificationManager.sendNotification(notification, student)
|
||||
appNotificationManager.sendMultipleNotifications(groupNotificationData, student)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package io.github.wulkanowy.services.sync.notifications
|
||||
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.services.sync.channels.LuckyNumberChannel
|
||||
import io.github.wulkanowy.services.sync.channels.NewAttendanceChannel
|
||||
import io.github.wulkanowy.services.sync.channels.NewConferencesChannel
|
||||
import io.github.wulkanowy.services.sync.channels.NewExamChannel
|
||||
import io.github.wulkanowy.services.sync.channels.NewGradesChannel
|
||||
@ -9,17 +11,76 @@ import io.github.wulkanowy.services.sync.channels.NewMessagesChannel
|
||||
import io.github.wulkanowy.services.sync.channels.NewNotesChannel
|
||||
import io.github.wulkanowy.services.sync.channels.NewSchoolAnnouncementsChannel
|
||||
import io.github.wulkanowy.services.sync.channels.PushChannel
|
||||
import io.github.wulkanowy.services.sync.channels.TimetableChangeChannel
|
||||
|
||||
enum class NotificationType(val group: String?, val channel: String) {
|
||||
NEW_CONFERENCE("new_conferences_group", NewConferencesChannel.CHANNEL_ID),
|
||||
NEW_EXAM("new_exam_group", NewExamChannel.CHANNEL_ID),
|
||||
NEW_GRADE_DETAILS("new_grade_details_group", NewGradesChannel.CHANNEL_ID),
|
||||
NEW_GRADE_PREDICTED("new_grade_predicted_group", NewGradesChannel.CHANNEL_ID),
|
||||
NEW_GRADE_FINAL("new_grade_final_group", NewGradesChannel.CHANNEL_ID),
|
||||
NEW_HOMEWORK("new_homework_group", NewHomeworkChannel.CHANNEL_ID),
|
||||
NEW_LUCKY_NUMBER("lucky_number_group", LuckyNumberChannel.CHANNEL_ID),
|
||||
NEW_MESSAGE("new_message_group", NewMessagesChannel.CHANNEL_ID),
|
||||
NEW_NOTE("new_notes_group", NewNotesChannel.CHANNEL_ID),
|
||||
NEW_ANNOUNCEMENT("new_school_announcements_group", NewSchoolAnnouncementsChannel.CHANNEL_ID),
|
||||
PUSH(null, PushChannel.CHANNEL_ID)
|
||||
enum class NotificationType(
|
||||
val group: String?,
|
||||
val channel: String,
|
||||
val icon: Int
|
||||
) {
|
||||
NEW_CONFERENCE(
|
||||
group = "new_conferences_group",
|
||||
channel = NewConferencesChannel.CHANNEL_ID,
|
||||
icon = R.drawable.ic_more_conferences,
|
||||
),
|
||||
NEW_EXAM(
|
||||
group = "new_exam_group",
|
||||
channel = NewExamChannel.CHANNEL_ID,
|
||||
icon = R.drawable.ic_main_exam
|
||||
),
|
||||
NEW_GRADE_DETAILS(
|
||||
group = "new_grade_details_group",
|
||||
channel = NewGradesChannel.CHANNEL_ID,
|
||||
icon = R.drawable.ic_stat_grade,
|
||||
),
|
||||
NEW_GRADE_PREDICTED(
|
||||
group = "new_grade_predicted_group",
|
||||
channel = NewGradesChannel.CHANNEL_ID,
|
||||
icon = R.drawable.ic_stat_grade,
|
||||
),
|
||||
NEW_GRADE_FINAL(
|
||||
group = "new_grade_final_group",
|
||||
channel = NewGradesChannel.CHANNEL_ID,
|
||||
icon = R.drawable.ic_stat_grade,
|
||||
),
|
||||
NEW_HOMEWORK(
|
||||
group = "new_homework_group",
|
||||
channel = NewHomeworkChannel.CHANNEL_ID,
|
||||
icon = R.drawable.ic_more_homework,
|
||||
),
|
||||
NEW_LUCKY_NUMBER(
|
||||
group = null,
|
||||
channel = LuckyNumberChannel.CHANNEL_ID,
|
||||
icon = R.drawable.ic_stat_luckynumber,
|
||||
),
|
||||
NEW_MESSAGE(
|
||||
group = "new_message_group",
|
||||
channel = NewMessagesChannel.CHANNEL_ID,
|
||||
icon = R.drawable.ic_stat_message,
|
||||
),
|
||||
NEW_NOTE(
|
||||
group = "new_notes_group",
|
||||
channel = NewNotesChannel.CHANNEL_ID,
|
||||
icon = R.drawable.ic_stat_note
|
||||
),
|
||||
NEW_ANNOUNCEMENT(
|
||||
group = "new_school_announcements_group",
|
||||
channel = NewSchoolAnnouncementsChannel.CHANNEL_ID,
|
||||
icon = R.drawable.ic_all_about
|
||||
),
|
||||
CHANGE_TIMETABLE(
|
||||
group = "change_timetable_group",
|
||||
channel = TimetableChangeChannel.CHANNEL_ID,
|
||||
icon = R.drawable.ic_main_timetable
|
||||
),
|
||||
NEW_ATTENDANCE(
|
||||
group = "new_attendance_group",
|
||||
channel = NewAttendanceChannel.CHANNEL_ID,
|
||||
icon = R.drawable.ic_main_attendance
|
||||
),
|
||||
PUSH(
|
||||
group = null,
|
||||
channel = PushChannel.CHANNEL_ID,
|
||||
icon = R.drawable.ic_stat_all
|
||||
)
|
||||
}
|
||||
|
@ -3,18 +3,40 @@ 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.repositories.AttendanceRepository
|
||||
import io.github.wulkanowy.utils.monday
|
||||
import io.github.wulkanowy.utils.sunday
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.services.sync.notifications.NewAttendanceNotification
|
||||
import io.github.wulkanowy.utils.previousOrSameSchoolDay
|
||||
import io.github.wulkanowy.utils.waitForResult
|
||||
import kotlinx.coroutines.flow.first
|
||||
import java.time.LocalDate.now
|
||||
import javax.inject.Inject
|
||||
|
||||
class AttendanceWork @Inject constructor(
|
||||
private val attendanceRepository: AttendanceRepository
|
||||
private val attendanceRepository: AttendanceRepository,
|
||||
private val newAttendanceNotification: NewAttendanceNotification,
|
||||
private val preferencesRepository: PreferencesRepository
|
||||
) : Work {
|
||||
|
||||
override suspend fun doWork(student: Student, semester: Semester) {
|
||||
attendanceRepository.getAttendance(student, semester, now().monday, now().sunday, true)
|
||||
attendanceRepository.getAttendance(
|
||||
student = student,
|
||||
semester = semester,
|
||||
start = now().previousOrSameSchoolDay,
|
||||
end = now().previousOrSameSchoolDay,
|
||||
forceRefresh = true,
|
||||
notify = preferencesRepository.isNotificationsEnable
|
||||
)
|
||||
.waitForResult()
|
||||
|
||||
attendanceRepository.getAttendanceFromDatabase(semester, now().minusDays(7), now())
|
||||
.first()
|
||||
.filterNot { it.isNotified }
|
||||
.let {
|
||||
if (it.isNotEmpty()) newAttendanceNotification.notify(it, student)
|
||||
|
||||
attendanceRepository.updateTimetable(it.onEach { attendance ->
|
||||
attendance.isNotified = true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,7 @@ import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.repositories.HomeworkRepository
|
||||
import io.github.wulkanowy.data.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.services.sync.notifications.NewHomeworkNotification
|
||||
import io.github.wulkanowy.utils.monday
|
||||
import io.github.wulkanowy.utils.sunday
|
||||
import io.github.wulkanowy.utils.nextOrSameSchoolDay
|
||||
import io.github.wulkanowy.utils.waitForResult
|
||||
import kotlinx.coroutines.flow.first
|
||||
import java.time.LocalDate.now
|
||||
@ -22,13 +21,13 @@ class HomeworkWork @Inject constructor(
|
||||
homeworkRepository.getHomework(
|
||||
student = student,
|
||||
semester = semester,
|
||||
start = now().monday,
|
||||
end = now().sunday,
|
||||
start = now().nextOrSameSchoolDay,
|
||||
end = now().nextOrSameSchoolDay,
|
||||
forceRefresh = true,
|
||||
notify = preferencesRepository.isNotificationsEnable
|
||||
).waitForResult()
|
||||
|
||||
homeworkRepository.getHomeworkFromDatabase(semester, now().monday, now().sunday).first()
|
||||
homeworkRepository.getHomeworkFromDatabase(semester, now(), now().plusDays(7)).first()
|
||||
.filter { !it.isNotified }.let {
|
||||
if (it.isNotEmpty()) newHomeworkNotification.notify(it, student)
|
||||
|
||||
|
@ -2,18 +2,41 @@ 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.repositories.PreferencesRepository
|
||||
import io.github.wulkanowy.data.repositories.TimetableRepository
|
||||
import io.github.wulkanowy.utils.monday
|
||||
import io.github.wulkanowy.utils.sunday
|
||||
import io.github.wulkanowy.services.sync.notifications.ChangeTimetableNotification
|
||||
import io.github.wulkanowy.utils.nextOrSameSchoolDay
|
||||
import io.github.wulkanowy.utils.waitForResult
|
||||
import kotlinx.coroutines.flow.first
|
||||
import java.time.LocalDate.now
|
||||
import javax.inject.Inject
|
||||
|
||||
class TimetableWork @Inject constructor(
|
||||
private val timetableRepository: TimetableRepository
|
||||
private val timetableRepository: TimetableRepository,
|
||||
private val changeTimetableNotification: ChangeTimetableNotification,
|
||||
private val preferencesRepository: PreferencesRepository
|
||||
) : Work {
|
||||
|
||||
override suspend fun doWork(student: Student, semester: Semester) {
|
||||
timetableRepository.getTimetable(student, semester, now().monday, now().sunday, true).waitForResult()
|
||||
timetableRepository.getTimetable(
|
||||
student = student,
|
||||
semester = semester,
|
||||
start = now().nextOrSameSchoolDay,
|
||||
end = now().nextOrSameSchoolDay,
|
||||
forceRefresh = true,
|
||||
notify = preferencesRepository.isNotificationsEnable
|
||||
)
|
||||
.waitForResult()
|
||||
|
||||
timetableRepository.getTimetableFromDatabase(semester, now(), now().plusDays(7))
|
||||
.first()
|
||||
.filterNot { it.isNotified }
|
||||
.let {
|
||||
if (it.isNotEmpty()) changeTimetableNotification.notify(it, student)
|
||||
|
||||
timetableRepository.updateTimetable(it.onEach { timetable ->
|
||||
timetable.isNotified = true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,11 @@
|
||||
package io.github.wulkanowy.ui.base
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.snackbar.Snackbar.LENGTH_LONG
|
||||
@ -40,7 +37,6 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
|
||||
themeManager.applyActivityTheme(this)
|
||||
super.onCreate(savedInstanceState)
|
||||
supportFragmentManager.registerFragmentLifecycleCallbacks(fragmentLifecycleLogger, true)
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
setTaskDescription(
|
||||
@ -83,8 +79,8 @@ abstract class BaseActivity<T : BasePresenter<out BaseView>, VB : ViewBinding> :
|
||||
}
|
||||
|
||||
override fun openClearLoginView() {
|
||||
startActivity(LoginActivity.getStartIntent(this)
|
||||
.apply { addFlags(FLAG_ACTIVITY_CLEAR_TASK or FLAG_ACTIVITY_NEW_TASK) })
|
||||
startActivity(LoginActivity.getStartIntent(this))
|
||||
finishAffinity()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
@ -2,32 +2,33 @@ package io.github.wulkanowy.ui.base
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentPagerAdapter
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
|
||||
//TODO Use ViewPager2
|
||||
class BaseFragmentPagerAdapter(private val fragmentManager: FragmentManager) :
|
||||
FragmentPagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
|
||||
class BaseFragmentPagerAdapter(
|
||||
private val fragmentManager: FragmentManager,
|
||||
private val pagesCount: Int,
|
||||
lifecycle: Lifecycle,
|
||||
) : FragmentStateAdapter(fragmentManager, lifecycle), TabLayoutMediator.TabConfigurationStrategy {
|
||||
|
||||
private val pages = mutableMapOf<Fragment, String?>()
|
||||
lateinit var itemFactory: (position: Int) -> Fragment
|
||||
|
||||
var titleFactory: (position: Int) -> String? = { "" }
|
||||
|
||||
var containerId = 0
|
||||
|
||||
fun getFragmentInstance(position: Int): Fragment? {
|
||||
require(containerId != 0) { "Container id is 0" }
|
||||
return fragmentManager.findFragmentByTag("android:switcher:$containerId:$position")
|
||||
return fragmentManager.findFragmentByTag("f$position")
|
||||
}
|
||||
|
||||
fun addFragments(fragments: List<Fragment>) {
|
||||
fragments.forEach { pages[it] = null }
|
||||
override fun createFragment(position: Int): Fragment = itemFactory(position)
|
||||
|
||||
override fun getItemCount() = pagesCount
|
||||
|
||||
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
|
||||
tab.text = titleFactory(position)
|
||||
}
|
||||
|
||||
fun addFragmentsWithTitle(pages: Map<Fragment, String>) {
|
||||
this.pages.putAll(pages)
|
||||
}
|
||||
|
||||
override fun getItem(position: Int) = pages.keys.elementAt(position)
|
||||
|
||||
override fun getCount() = pages.size
|
||||
|
||||
override fun getPageTitle(position: Int) = pages.values.elementAt(position)
|
||||
}
|
||||
|
@ -6,29 +6,27 @@ import io.github.wulkanowy.utils.flowWithResource
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import timber.log.Timber
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
open class BasePresenter<T : BaseView>(
|
||||
protected val errorHandler: ErrorHandler,
|
||||
protected val studentRepository: StudentRepository
|
||||
) : CoroutineScope {
|
||||
) {
|
||||
private val job = SupervisorJob()
|
||||
|
||||
private var job = Job()
|
||||
protected val presenterScope = CoroutineScope(job + Dispatchers.Main)
|
||||
|
||||
private val jobs = mutableMapOf<String, Job>()
|
||||
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = Dispatchers.Main + job
|
||||
private val childrenJobs = mutableMapOf<String, Job>()
|
||||
|
||||
var view: T? = null
|
||||
|
||||
open fun onAttachView(view: T) {
|
||||
job = Job()
|
||||
this.view = view
|
||||
errorHandler.apply {
|
||||
showErrorMessage = view::showError
|
||||
@ -64,22 +62,22 @@ open class BasePresenter<T : BaseView>(
|
||||
}
|
||||
|
||||
fun <T> Flow<T>.launch(individualJobTag: String = "load"): Job {
|
||||
jobs[individualJobTag]?.cancel()
|
||||
val job = catch { errorHandler.dispatch(it) }.launchIn(this@BasePresenter)
|
||||
jobs[individualJobTag] = job
|
||||
childrenJobs[individualJobTag]?.cancel()
|
||||
val job = catch { errorHandler.dispatch(it) }.launchIn(presenterScope)
|
||||
childrenJobs[individualJobTag] = job
|
||||
Timber.d("Job $individualJobTag launched in ${this@BasePresenter.javaClass.simpleName}: $job")
|
||||
return job
|
||||
}
|
||||
|
||||
fun cancelJobs(vararg names: String) {
|
||||
names.forEach {
|
||||
jobs[it]?.cancel()
|
||||
childrenJobs[it]?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
open fun onDetachView() {
|
||||
view = null
|
||||
job.cancel()
|
||||
job.cancelChildren()
|
||||
errorHandler.clear()
|
||||
view = null
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import android.widget.Toast
|
||||
import android.widget.Toast.LENGTH_LONG
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.view.isGone
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.databinding.DialogErrorBinding
|
||||
@ -24,8 +25,6 @@ import io.github.wulkanowy.utils.openEmailClient
|
||||
import io.github.wulkanowy.utils.openInternetBrowser
|
||||
import okhttp3.internal.http2.StreamResetException
|
||||
import java.io.InterruptedIOException
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import java.net.ConnectException
|
||||
import java.net.SocketTimeoutException
|
||||
import java.net.UnknownHostException
|
||||
@ -64,26 +63,26 @@ class ErrorDialog : BaseDialogFragment<DialogErrorBinding>() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val stringWriter = StringWriter().apply {
|
||||
error.printStackTrace(PrintWriter(this))
|
||||
}
|
||||
val errorStacktrace = error.stackTraceToString()
|
||||
|
||||
with(binding) {
|
||||
errorDialogContent.text = stringWriter.toString()
|
||||
errorDialogContent.text = errorStacktrace.replace(": ${error.localizedMessage}", "")
|
||||
with(errorDialogHorizontalScroll) {
|
||||
post { fullScroll(HorizontalScrollView.FOCUS_LEFT) }
|
||||
}
|
||||
errorDialogCopy.setOnClickListener {
|
||||
val clip = ClipData.newPlainText("wulkanowy", stringWriter.toString())
|
||||
val clip = ClipData.newPlainText("Error details", errorStacktrace)
|
||||
activity?.getSystemService<ClipboardManager>()?.setPrimaryClip(clip)
|
||||
|
||||
Toast.makeText(context, R.string.all_copied, LENGTH_LONG).show()
|
||||
}
|
||||
errorDialogCancel.setOnClickListener { dismiss() }
|
||||
errorDialogReport.setOnClickListener {
|
||||
openConfirmDialog { openEmailClient(stringWriter.toString()) }
|
||||
openConfirmDialog { openEmailClient(errorStacktrace) }
|
||||
}
|
||||
errorDialogMessage.text = resources.getString(error)
|
||||
errorDialogHumanizedMessage.text = resources.getString(error)
|
||||
errorDialogErrorMessage.text = error.localizedMessage
|
||||
errorDialogErrorMessage.isGone = error.localizedMessage.isNullOrBlank()
|
||||
errorDialogReport.isEnabled = when (error) {
|
||||
is UnknownHostException,
|
||||
is InterruptedIOException,
|
||||
|
@ -1,6 +1,7 @@
|
||||
package io.github.wulkanowy.ui.base
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
|
||||
import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException
|
||||
import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException
|
||||
@ -9,7 +10,7 @@ import io.github.wulkanowy.utils.security.ScramblerException
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
open class ErrorHandler @Inject constructor(protected val resources: Resources) {
|
||||
open class ErrorHandler @Inject constructor(@ApplicationContext protected val context: Context) {
|
||||
|
||||
var showErrorMessage: (String, Throwable) -> Unit = { _, _ -> }
|
||||
|
||||
@ -25,7 +26,7 @@ open class ErrorHandler @Inject constructor(protected val resources: Resources)
|
||||
}
|
||||
|
||||
protected open fun proceed(error: Throwable) {
|
||||
showErrorMessage(resources.getString(error), error)
|
||||
showErrorMessage(context.resources.getString(error), error)
|
||||
when (error) {
|
||||
is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl)
|
||||
is ScramblerException, is BadCredentialsException -> onSessionExpired()
|
||||
|
@ -41,14 +41,15 @@ class ThemeManager @Inject constructor(private val preferencesRepository: Prefer
|
||||
)
|
||||
}
|
||||
|
||||
private fun isThemeApplicable(activity: AppCompatActivity): Boolean {
|
||||
return activity.packageManager
|
||||
private fun isThemeApplicable(activity: AppCompatActivity) =
|
||||
activity.packageManager
|
||||
.getPackageInfo(activity.packageName, GET_ACTIVITIES)
|
||||
.activities.singleOrNull { it.name == activity::class.java.canonicalName }
|
||||
?.theme.let {
|
||||
.activities
|
||||
.singleOrNull { it.name == activity::class.java.canonicalName }
|
||||
?.theme
|
||||
.let {
|
||||
it == R.style.WulkanowyTheme_Black || it == R.style.WulkanowyTheme_NoActionBar
|
||||
|| it == R.style.WulkanowyTheme_Login || it == R.style.WulkanowyTheme_Login_Black
|
||||
|| it == R.style.WulkanowyTheme_MessageSend || it == R.style.WulkanowyTheme_MessageSend_Black
|
||||
}
|
||||
}
|
||||
}
|
||||
|
136
app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt
Normal file
136
app/src/main/java/io/github/wulkanowy/ui/modules/Destination.kt
Normal file
@ -0,0 +1,136 @@
|
||||
package io.github.wulkanowy.ui.modules
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment
|
||||
import io.github.wulkanowy.ui.modules.conference.ConferenceFragment
|
||||
import io.github.wulkanowy.ui.modules.dashboard.DashboardFragment
|
||||
import io.github.wulkanowy.ui.modules.exam.ExamFragment
|
||||
import io.github.wulkanowy.ui.modules.grade.GradeFragment
|
||||
import io.github.wulkanowy.ui.modules.homework.HomeworkFragment
|
||||
import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment
|
||||
import io.github.wulkanowy.ui.modules.message.MessageFragment
|
||||
import io.github.wulkanowy.ui.modules.more.MoreFragment
|
||||
import io.github.wulkanowy.ui.modules.note.NoteFragment
|
||||
import io.github.wulkanowy.ui.modules.schoolandteachers.school.SchoolFragment
|
||||
import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment
|
||||
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
|
||||
import java.io.Serializable
|
||||
import java.time.LocalDate
|
||||
|
||||
sealed interface Destination : Serializable {
|
||||
|
||||
/*
|
||||
Type in children classes have to be as getter to avoid null in enums
|
||||
https://stackoverflow.com/questions/68866453/kotlin-enum-val-is-returning-null-despite-being-set-at-compile-time
|
||||
*/
|
||||
val type: Type
|
||||
|
||||
val fragment: Fragment
|
||||
|
||||
enum class Type(val defaultDestination: Destination) {
|
||||
DASHBOARD(Dashboard),
|
||||
GRADE(Grade),
|
||||
ATTENDANCE(Attendance),
|
||||
EXAM(Exam),
|
||||
TIMETABLE(Timetable()),
|
||||
HOMEWORK(Homework),
|
||||
NOTE(Note),
|
||||
CONFERENCE(Conference),
|
||||
SCHOOL_ANNOUNCEMENT(SchoolAnnouncement),
|
||||
SCHOOL(School),
|
||||
LUCKY_NUMBER(More),
|
||||
MORE(More),
|
||||
MESSAGE(Message);
|
||||
}
|
||||
|
||||
object Dashboard : Destination {
|
||||
|
||||
override val type get() = Type.DASHBOARD
|
||||
|
||||
override val fragment get() = DashboardFragment.newInstance()
|
||||
}
|
||||
|
||||
object Grade : Destination {
|
||||
|
||||
override val type get() = Type.GRADE
|
||||
|
||||
override val fragment get() = GradeFragment.newInstance()
|
||||
}
|
||||
|
||||
object Attendance : Destination {
|
||||
|
||||
override val type get() = Type.ATTENDANCE
|
||||
|
||||
override val fragment get() = AttendanceFragment.newInstance()
|
||||
}
|
||||
|
||||
object Exam : Destination {
|
||||
|
||||
override val type get() = Type.EXAM
|
||||
|
||||
override val fragment get() = ExamFragment.newInstance()
|
||||
}
|
||||
|
||||
data class Timetable(val date: LocalDate? = null) : Destination {
|
||||
|
||||
override val type get() = Type.TIMETABLE
|
||||
|
||||
override val fragment get() = TimetableFragment.newInstance(date)
|
||||
}
|
||||
|
||||
object Homework : Destination {
|
||||
|
||||
override val type get() = Type.HOMEWORK
|
||||
|
||||
override val fragment get() = HomeworkFragment.newInstance()
|
||||
}
|
||||
|
||||
object Note : Destination {
|
||||
|
||||
override val type get() = Type.NOTE
|
||||
|
||||
override val fragment get() = NoteFragment.newInstance()
|
||||
}
|
||||
|
||||
object Conference : Destination {
|
||||
|
||||
override val type get() = Type.CONFERENCE
|
||||
|
||||
override val fragment get() = ConferenceFragment.newInstance()
|
||||
}
|
||||
|
||||
object SchoolAnnouncement : Destination {
|
||||
|
||||
override val type get() = Type.SCHOOL_ANNOUNCEMENT
|
||||
|
||||
override val fragment get() = SchoolAnnouncementFragment.newInstance()
|
||||
}
|
||||
|
||||
object School : Destination {
|
||||
|
||||
override val type get() = Type.SCHOOL
|
||||
|
||||
override val fragment get() = SchoolFragment.newInstance()
|
||||
}
|
||||
|
||||
object LuckyNumber : Destination {
|
||||
|
||||
override val type get() = Type.LUCKY_NUMBER
|
||||
|
||||
override val fragment get() = LuckyNumberFragment.newInstance()
|
||||
}
|
||||
|
||||
object More : Destination {
|
||||
|
||||
override val type get() = Type.MORE
|
||||
|
||||
override val fragment get() = MoreFragment.newInstance()
|
||||
}
|
||||
|
||||
object Message : Destination {
|
||||
|
||||
override val type get() = Type.MESSAGE
|
||||
|
||||
override val fragment get() = MessageFragment.newInstance()
|
||||
}
|
||||
}
|
@ -82,18 +82,20 @@ class AboutPresenter @Inject constructor(
|
||||
|
||||
private fun loadData() {
|
||||
view?.run {
|
||||
updateData(listOfNotNull(
|
||||
versionRes,
|
||||
creatorsRes,
|
||||
feedbackRes,
|
||||
faqRes,
|
||||
discordRes,
|
||||
facebookRes,
|
||||
twitterRes,
|
||||
homepageRes,
|
||||
licensesRes,
|
||||
privacyRes
|
||||
))
|
||||
updateData(
|
||||
listOfNotNull(
|
||||
versionRes,
|
||||
creatorsRes,
|
||||
feedbackRes,
|
||||
faqRes,
|
||||
discordRes,
|
||||
facebookRes,
|
||||
twitterRes,
|
||||
homepageRes,
|
||||
licensesRes,
|
||||
privacyRes
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ class LicensePresenter @Inject constructor(
|
||||
|
||||
private fun loadData() {
|
||||
flowWithResource {
|
||||
withContext(dispatchers.backgroundThread) {
|
||||
withContext(dispatchers.io) {
|
||||
view?.appLibraries.orEmpty()
|
||||
}
|
||||
}.onEach {
|
||||
|
@ -3,12 +3,9 @@ package io.github.wulkanowy.ui.modules.account.accountedit
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.graphics.drawable.RippleDrawable
|
||||
import android.graphics.drawable.StateListDrawable
|
||||
import android.os.Build
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
@ -52,30 +49,13 @@ class AccountEditColorAdapter @Inject constructor() :
|
||||
}
|
||||
}
|
||||
|
||||
private fun Int.createForegroundDrawable(): Drawable =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
val mask = GradientDrawable().apply {
|
||||
shape = GradientDrawable.OVAL
|
||||
setColor(Color.BLACK)
|
||||
}
|
||||
RippleDrawable(ColorStateList.valueOf(this.rippleColor), null, mask)
|
||||
} else {
|
||||
val foreground = StateListDrawable().apply {
|
||||
alpha = 80
|
||||
setEnterFadeDuration(250)
|
||||
setExitFadeDuration(250)
|
||||
}
|
||||
|
||||
val mask = GradientDrawable().apply {
|
||||
shape = GradientDrawable.OVAL
|
||||
setColor(this@createForegroundDrawable.rippleColor)
|
||||
}
|
||||
|
||||
foreground.apply {
|
||||
addState(intArrayOf(android.R.attr.state_pressed), mask)
|
||||
addState(intArrayOf(), ColorDrawable(Color.TRANSPARENT))
|
||||
}
|
||||
private fun Int.createForegroundDrawable(): Drawable {
|
||||
val mask = GradientDrawable().apply {
|
||||
shape = GradientDrawable.OVAL
|
||||
setColor(Color.BLACK)
|
||||
}
|
||||
return RippleDrawable(ColorStateList.valueOf(this.rippleColor), null, mask)
|
||||
}
|
||||
|
||||
private inline val Int.rippleColor: Int
|
||||
get() {
|
||||
|
@ -9,7 +9,7 @@ import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.Attendance
|
||||
import io.github.wulkanowy.data.enums.SentExcuseStatus
|
||||
import io.github.wulkanowy.databinding.ItemAttendanceBinding
|
||||
import io.github.wulkanowy.utils.description
|
||||
import io.github.wulkanowy.utils.descriptionRes
|
||||
import io.github.wulkanowy.utils.isExcusableOrNotExcused
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -36,7 +36,7 @@ class AttendanceAdapter @Inject constructor() :
|
||||
with(holder.binding) {
|
||||
attendanceItemNumber.text = item.number.toString()
|
||||
attendanceItemSubject.text = item.subject
|
||||
attendanceItemDescription.setText(item.description)
|
||||
attendanceItemDescription.setText(item.descriptionRes)
|
||||
attendanceItemAlert.visibility = item.run { if (absence && !excused) View.VISIBLE else View.INVISIBLE }
|
||||
attendanceItemNumber.visibility = View.GONE
|
||||
attendanceItemExcuseInfo.visibility = View.GONE
|
||||
@ -46,7 +46,7 @@ class AttendanceAdapter @Inject constructor() :
|
||||
onExcuseCheckboxSelect(item, checked)
|
||||
}
|
||||
|
||||
when (if (item.excuseStatus != null) SentExcuseStatus.valueOf(item.excuseStatus) else null) {
|
||||
when (item.excuseStatus?.let { SentExcuseStatus.valueOf(it)}) {
|
||||
SentExcuseStatus.WAITING -> {
|
||||
attendanceItemExcuseInfo.setImageResource(R.drawable.ic_excuse_waiting)
|
||||
attendanceItemExcuseInfo.visibility = View.VISIBLE
|
||||
|
@ -7,7 +7,7 @@ import android.view.ViewGroup
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import io.github.wulkanowy.data.db.entities.Attendance
|
||||
import io.github.wulkanowy.databinding.DialogAttendanceBinding
|
||||
import io.github.wulkanowy.utils.description
|
||||
import io.github.wulkanowy.utils.descriptionRes
|
||||
import io.github.wulkanowy.utils.lifecycleAwareVariable
|
||||
import io.github.wulkanowy.utils.toFormattedString
|
||||
|
||||
@ -45,7 +45,7 @@ class AttendanceDialog : DialogFragment() {
|
||||
|
||||
with(binding) {
|
||||
attendanceDialogSubjectValue.text = attendance.subject
|
||||
attendanceDialogDescriptionValue.setText(attendance.description)
|
||||
attendanceDialogDescriptionValue.setText(attendance.descriptionRes)
|
||||
attendanceDialogDateValue.text = attendance.date.toFormattedString()
|
||||
attendanceDialogNumberValue.text = attendance.number.toString()
|
||||
attendanceDialogClose.setOnClickListener { dismiss() }
|
||||
|
@ -12,6 +12,7 @@ import android.view.View.INVISIBLE
|
||||
import android.view.View.VISIBLE
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.datepicker.CalendarConstraints
|
||||
import com.google.android.material.datepicker.MaterialDatePicker
|
||||
@ -121,9 +122,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
|
||||
attendanceSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
|
||||
attendanceSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
|
||||
attendanceSwipe.setProgressBackgroundColorSchemeColor(
|
||||
requireContext().getThemeAttrColor(
|
||||
R.attr.colorSwipeRefresh
|
||||
)
|
||||
requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh)
|
||||
)
|
||||
attendanceErrorRetry.setOnClickListener { presenter.onRetry() }
|
||||
attendanceErrorDetails.setOnClickListener { presenter.onDetailsClick() }
|
||||
@ -134,7 +133,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
|
||||
|
||||
attendanceExcuseButton.setOnClickListener { presenter.onExcuseButtonClick() }
|
||||
|
||||
attendanceNavContainer.setElevationCompat(requireContext().dpToPx(8f))
|
||||
attendanceNavContainer.elevation = requireContext().dpToPx(8f)
|
||||
}
|
||||
}
|
||||
|
||||
@ -218,7 +217,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
|
||||
}
|
||||
|
||||
override fun showExcuseButton(show: Boolean) {
|
||||
binding.attendanceExcuseButton.visibility = if (show) VISIBLE else GONE
|
||||
binding.attendanceExcuseButton.isVisible = show
|
||||
}
|
||||
|
||||
override fun showAttendanceDialog(lesson: Attendance) {
|
||||
@ -289,12 +288,16 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
|
||||
}
|
||||
|
||||
override fun showExcuseCheckboxes(show: Boolean) {
|
||||
attendanceAdapter.apply {
|
||||
with(attendanceAdapter) {
|
||||
excuseActionMode = show
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun showDayNavigation(show: Boolean) {
|
||||
binding.attendanceNavContainer.isVisible = show
|
||||
}
|
||||
|
||||
override fun finishActionMode() {
|
||||
actionMode?.finish()
|
||||
}
|
||||
|
@ -174,6 +174,8 @@ class AttendancePresenter @Inject constructor(
|
||||
view?.apply {
|
||||
showExcuseCheckboxes(true)
|
||||
showExcuseButton(false)
|
||||
enableSwipe(false)
|
||||
showDayNavigation(false)
|
||||
}
|
||||
attendanceToExcuseList.clear()
|
||||
return true
|
||||
@ -183,6 +185,8 @@ class AttendancePresenter @Inject constructor(
|
||||
view?.apply {
|
||||
showExcuseCheckboxes(false)
|
||||
showExcuseButton(true)
|
||||
enableSwipe(true)
|
||||
showDayNavigation(true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -259,9 +263,8 @@ class AttendancePresenter @Inject constructor(
|
||||
showEmpty(filteredAttendance.isEmpty())
|
||||
showErrorView(false)
|
||||
showContent(filteredAttendance.isNotEmpty())
|
||||
showExcuseButton(filteredAttendance.any { item ->
|
||||
(!isParent && isVulcanExcusedFunctionEnabled) || (isParent && item.isExcusableOrNotExcused)
|
||||
})
|
||||
val anyExcusables = filteredAttendance.any { it.isExcusableOrNotExcused }
|
||||
showExcuseButton(anyExcusables && (isParent || isVulcanExcusedFunctionEnabled))
|
||||
}
|
||||
analytics.logEvent(
|
||||
"load_data",
|
||||
|
@ -60,6 +60,8 @@ interface AttendanceView : BaseView {
|
||||
|
||||
fun showExcuseCheckboxes(show: Boolean)
|
||||
|
||||
fun showDayNavigation(show: Boolean)
|
||||
|
||||
fun finishActionMode()
|
||||
|
||||
fun popView()
|
||||
|
@ -71,7 +71,7 @@ class AttendanceSummaryFragment :
|
||||
setOnItemSelectedListener<TextView> { presenter.onSubjectSelected(it?.text?.toString()) }
|
||||
}
|
||||
|
||||
binding.attendanceSummarySubjectsContainer.setElevationCompat(requireContext().dpToPx(1f))
|
||||
binding.attendanceSummarySubjectsContainer.elevation = requireContext().dpToPx(1f)
|
||||
}
|
||||
|
||||
override fun updateSubjects(data: ArrayList<String>) {
|
||||
|
@ -1,6 +1,8 @@
|
||||
package io.github.wulkanowy.ui.modules.dashboard
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
@ -18,6 +20,7 @@ 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.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
|
||||
@ -63,6 +66,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
|
||||
|
||||
var onConferencesTileClickListener: () -> Unit = {}
|
||||
|
||||
var onAdminMessageClickListener: (String?) -> Unit = {}
|
||||
|
||||
val items = mutableListOf<DashboardItem>()
|
||||
|
||||
fun submitList(newItems: List<DashboardItem>) {
|
||||
@ -109,6 +114,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
|
||||
DashboardItem.Type.CONFERENCES.ordinal -> ConferencesViewHolder(
|
||||
ItemDashboardConferencesBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
DashboardItem.Type.ADMIN_MESSAGE.ordinal -> AdminMessageViewHolder(
|
||||
ItemDashboardAdminMessageBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
@ -123,6 +131,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
|
||||
is AnnouncementsViewHolder -> bindAnnouncementsViewHolder(holder, position)
|
||||
is ExamsViewHolder -> bindExamsViewHolder(holder, position)
|
||||
is ConferencesViewHolder -> bindConferencesViewHolder(holder, position)
|
||||
is AdminMessageViewHolder -> bindAdminMessage(holder, position)
|
||||
}
|
||||
}
|
||||
|
||||
@ -290,7 +299,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
|
||||
val currentDayHeader =
|
||||
timetableFull?.headers.orEmpty().singleOrNull { it.date == currentDate }
|
||||
|
||||
val tomorrowTimetable = timetableFull?.lessons.orEmpty()
|
||||
val tomorrowTimetable = timetableFull?.lessons
|
||||
.orEmpty()
|
||||
.filter { it.date == currentDate.plusDays(1) }
|
||||
.filterNot { it.canceled }
|
||||
val tomorrowDayHeader =
|
||||
@ -301,26 +311,31 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
|
||||
dateToNavigate = currentDate
|
||||
updateLessonView(item, currentTimetable, binding)
|
||||
binding.dashboardLessonsItemTitleTomorrow.isVisible = false
|
||||
binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false
|
||||
}
|
||||
tomorrowTimetable.isNotEmpty() -> {
|
||||
dateToNavigate = tomorrowDate
|
||||
updateLessonView(item, tomorrowTimetable, binding)
|
||||
binding.dashboardLessonsItemTitleTomorrow.isVisible = true
|
||||
binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false
|
||||
}
|
||||
currentDayHeader != null && currentDayHeader.content.isNotBlank() -> {
|
||||
dateToNavigate = currentDate
|
||||
updateLessonView(item, emptyList(), binding, currentDayHeader)
|
||||
binding.dashboardLessonsItemTitleTomorrow.isVisible = false
|
||||
binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false
|
||||
}
|
||||
tomorrowDayHeader != null && tomorrowDayHeader.content.isNotBlank() -> {
|
||||
dateToNavigate = tomorrowDate
|
||||
updateLessonView(item, emptyList(), binding, tomorrowDayHeader)
|
||||
binding.dashboardLessonsItemTitleTomorrow.isVisible = true
|
||||
binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible = false
|
||||
}
|
||||
else -> {
|
||||
dateToNavigate = tomorrowDate
|
||||
dateToNavigate = currentDate
|
||||
updateLessonView(item, emptyList(), binding)
|
||||
binding.dashboardLessonsItemTitleTomorrow.isVisible =
|
||||
binding.dashboardLessonsItemTitleTomorrow.isVisible = false
|
||||
binding.dashboardLessonsItemTitleTodayAndTomorrow.isVisible =
|
||||
!(item.isLoading && item.error == null)
|
||||
}
|
||||
}
|
||||
@ -692,6 +707,34 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindAdminMessage(adminMessageViewHolder: AdminMessageViewHolder, position: Int) {
|
||||
val item = (items[position] as DashboardItem.AdminMessages).adminMessage ?: return
|
||||
val context = adminMessageViewHolder.binding.root.context
|
||||
val (backgroundColor, textColor) = when (item.priority) {
|
||||
"HIGH" -> {
|
||||
context.getThemeAttrColor(R.attr.colorPrimary) to
|
||||
context.getThemeAttrColor(R.attr.colorOnPrimary)
|
||||
}
|
||||
"MEDIUM" -> {
|
||||
context.getThemeAttrColor(R.attr.colorMessageMedium) to Color.BLACK
|
||||
}
|
||||
else -> null to context.getThemeAttrColor(R.attr.colorOnSurface)
|
||||
}
|
||||
|
||||
with(adminMessageViewHolder.binding) {
|
||||
dashboardAdminMessageItemTitle.text = item.title
|
||||
dashboardAdminMessageItemTitle.setTextColor(textColor)
|
||||
dashboardAdminMessageItemDescription.text = item.content
|
||||
dashboardAdminMessageItemDescription.setTextColor(textColor)
|
||||
dashboardAdminMessageItemIcon.setColorFilter(textColor)
|
||||
|
||||
root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) })
|
||||
item.destinationUrl?.let { url ->
|
||||
root.setOnClickListener { onAdminMessageClickListener(url) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AccountViewHolder(val binding: ItemDashboardAccountBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
@ -731,6 +774,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
|
||||
val adapter by lazy { DashboardConferencesAdapter() }
|
||||
}
|
||||
|
||||
class AdminMessageViewHolder(val binding: ItemDashboardAdminMessageBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
private class DiffCallback(
|
||||
private val newList: List<DashboardItem>,
|
||||
private val oldList: List<DashboardItem>
|
||||
|
@ -10,6 +10,7 @@ import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.databinding.FragmentDashboardBinding
|
||||
@ -29,6 +30,7 @@ import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragm
|
||||
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
|
||||
import io.github.wulkanowy.utils.capitalise
|
||||
import io.github.wulkanowy.utils.getThemeAttrColor
|
||||
import io.github.wulkanowy.utils.openInternetBrowser
|
||||
import io.github.wulkanowy.utils.toFormattedString
|
||||
import java.time.LocalDate
|
||||
import javax.inject.Inject
|
||||
@ -97,6 +99,13 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
|
||||
onConferencesTileClickListener = {
|
||||
mainActivity.pushView(ConferenceFragment.newInstance())
|
||||
}
|
||||
onAdminMessageClickListener = presenter::onAdminMessageSelected
|
||||
|
||||
registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
binding.dashboardRecycler.scrollToPosition(0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
with(binding) {
|
||||
@ -188,6 +197,10 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
|
||||
(requireActivity() as MainActivity).pushView(NotificationsCenterFragment.newInstance())
|
||||
}
|
||||
|
||||
override fun openInternetBrowser(url: String) {
|
||||
requireContext().openInternetBrowser(url)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
dashboardAdapter.clearTimers()
|
||||
presenter.onDetachView()
|
||||
|
@ -1,5 +1,6 @@
|
||||
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
|
||||
@ -16,6 +17,15 @@ sealed class DashboardItem(val type: Type) {
|
||||
|
||||
abstract val isDataLoaded: Boolean
|
||||
|
||||
data class AdminMessages(
|
||||
val adminMessage: AdminMessage? = null,
|
||||
override val error: Throwable? = null,
|
||||
override val isLoading: Boolean = false
|
||||
) : DashboardItem(Type.ADMIN_MESSAGE) {
|
||||
|
||||
override val isDataLoaded get() = adminMessage != null
|
||||
}
|
||||
|
||||
data class Account(
|
||||
val student: Student? = null,
|
||||
override val error: Throwable? = null,
|
||||
@ -96,6 +106,7 @@ sealed class DashboardItem(val type: Type) {
|
||||
}
|
||||
|
||||
enum class Type {
|
||||
ADMIN_MESSAGE,
|
||||
ACCOUNT,
|
||||
HORIZONTAL_GROUP,
|
||||
LESSONS,
|
||||
@ -108,6 +119,7 @@ sealed class DashboardItem(val type: Type) {
|
||||
}
|
||||
|
||||
enum class Tile {
|
||||
ADMIN_MESSAGE,
|
||||
ACCOUNT,
|
||||
LUCKY_NUMBER,
|
||||
MESSAGES,
|
||||
@ -123,6 +135,7 @@ sealed class DashboardItem(val type: Type) {
|
||||
}
|
||||
|
||||
fun DashboardItem.Tile.toDashboardItemType() = when (this) {
|
||||
DashboardItem.Tile.ADMIN_MESSAGE -> DashboardItem.Type.ADMIN_MESSAGE
|
||||
DashboardItem.Tile.ACCOUNT -> DashboardItem.Type.ACCOUNT
|
||||
DashboardItem.Tile.LUCKY_NUMBER -> DashboardItem.Type.HORIZONTAL_GROUP
|
||||
DashboardItem.Tile.MESSAGES -> DashboardItem.Type.HORIZONTAL_GROUP
|
||||
|
@ -21,7 +21,7 @@ class DashboardItemMoveCallback(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder
|
||||
): Int {
|
||||
val dragFlags = if (viewHolder.bindingAdapterPosition != 0) {
|
||||
val dragFlags = if (!viewHolder.isAdminMessageOrAccountItem) {
|
||||
ItemTouchHelper.UP or ItemTouchHelper.DOWN
|
||||
} else 0
|
||||
|
||||
@ -32,7 +32,7 @@ class DashboardItemMoveCallback(
|
||||
recyclerView: RecyclerView,
|
||||
current: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
) = target.bindingAdapterPosition != 0
|
||||
) = !target.isAdminMessageOrAccountItem
|
||||
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
@ -52,4 +52,7 @@ class DashboardItemMoveCallback(
|
||||
|
||||
onUserInteractionEndListener(dashboardAdapter.items.toList())
|
||||
}
|
||||
|
||||
private val RecyclerView.ViewHolder.isAdminMessageOrAccountItem: Boolean
|
||||
get() = this is DashboardAdapter.AdminMessageViewHolder || this is DashboardAdapter.AccountViewHolder
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import io.github.wulkanowy.data.Status
|
||||
import io.github.wulkanowy.data.db.entities.LuckyNumber
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.enums.MessageFolder
|
||||
import io.github.wulkanowy.data.repositories.AdminMessageRepository
|
||||
import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository
|
||||
import io.github.wulkanowy.data.repositories.ConferenceRepository
|
||||
import io.github.wulkanowy.data.repositories.ExamRepository
|
||||
@ -50,7 +51,8 @@ class DashboardPresenter @Inject constructor(
|
||||
private val examRepository: ExamRepository,
|
||||
private val conferenceRepository: ConferenceRepository,
|
||||
private val preferencesRepository: PreferencesRepository,
|
||||
private val schoolAnnouncementRepository: SchoolAnnouncementRepository
|
||||
private val schoolAnnouncementRepository: SchoolAnnouncementRepository,
|
||||
private val adminMessageRepository: AdminMessageRepository
|
||||
) : BasePresenter<DashboardView>(errorHandler, studentRepository) {
|
||||
|
||||
private val dashboardItemLoadedList = mutableListOf<DashboardItem>()
|
||||
@ -149,7 +151,7 @@ class DashboardPresenter @Inject constructor(
|
||||
tileList: List<DashboardItem.Type>,
|
||||
forceRefresh: Boolean
|
||||
) {
|
||||
launch {
|
||||
presenterScope.launch {
|
||||
Timber.i("Loading dashboard account data started")
|
||||
val student = runCatching { studentRepository.getCurrentStudent(true) }
|
||||
.onFailure {
|
||||
@ -179,6 +181,7 @@ class DashboardPresenter @Inject constructor(
|
||||
loadConferences(student, forceRefresh)
|
||||
}
|
||||
DashboardItem.Type.ADS -> TODO()
|
||||
DashboardItem.Type.ADMIN_MESSAGE -> loadAdminMessage(student, forceRefresh)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -225,6 +228,10 @@ class DashboardPresenter @Inject constructor(
|
||||
}.toSet()
|
||||
}
|
||||
|
||||
fun onAdminMessageSelected(url: String?) {
|
||||
url?.let { view?.openInternetBrowser(it) }
|
||||
}
|
||||
|
||||
private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) {
|
||||
flow {
|
||||
val semester = semesterRepository.getCurrentSemester(student)
|
||||
@ -309,18 +316,17 @@ class DashboardPresenter @Inject constructor(
|
||||
|
||||
gradeRepository.getGrades(student, semester, forceRefresh)
|
||||
}.map { originalResource ->
|
||||
val filteredSubjectWithGrades = originalResource.data?.first.orEmpty()
|
||||
.filter { grade ->
|
||||
grade.date.isAfter(LocalDate.now().minusDays(7))
|
||||
}
|
||||
.groupBy { grade -> grade.subject }
|
||||
val filteredSubjectWithGrades = originalResource.data?.first
|
||||
.orEmpty()
|
||||
.filter { it.date >= LocalDate.now().minusDays(7) }
|
||||
.groupBy { it.subject }
|
||||
.mapValues { entry ->
|
||||
entry.value
|
||||
.take(5)
|
||||
.sortedBy { grade -> grade.date }
|
||||
.sortedByDescending { it.date }
|
||||
}
|
||||
.toList()
|
||||
.sortedBy { subjectWithGrades -> subjectWithGrades.second[0].date }
|
||||
.sortedByDescending { (_, grades) -> grades[0].date }
|
||||
.toMap()
|
||||
|
||||
Resource(
|
||||
@ -424,9 +430,9 @@ class DashboardPresenter @Inject constructor(
|
||||
}.map { homeworkResource ->
|
||||
val currentDate = LocalDate.now()
|
||||
|
||||
val filteredHomework = homeworkResource.data?.filter {
|
||||
(it.date.isAfter(currentDate) || it.date == currentDate) && !it.isDone
|
||||
}
|
||||
val filteredHomework = homeworkResource.data
|
||||
?.filter { (it.date.isAfter(currentDate) || it.date == currentDate) && !it.isDone }
|
||||
?.sortedBy { it.date }
|
||||
|
||||
homeworkResource.copy(data = filteredHomework)
|
||||
}.onEach {
|
||||
@ -567,6 +573,38 @@ class DashboardPresenter @Inject constructor(
|
||||
}.launch("dashboard_conferences")
|
||||
}
|
||||
|
||||
private fun loadAdminMessage(student: Student, forceRefresh: Boolean) {
|
||||
flowWithResourceIn { adminMessageRepository.getAdminMessages(student, forceRefresh) }
|
||||
.onEach {
|
||||
when (it.status) {
|
||||
Status.LOADING -> {
|
||||
Timber.i("Loading dashboard admin message data started")
|
||||
if (forceRefresh) return@onEach
|
||||
updateData(DashboardItem.AdminMessages(), forceRefresh)
|
||||
}
|
||||
Status.SUCCESS -> {
|
||||
Timber.i("Loading dashboard admin message result: Success")
|
||||
updateData(
|
||||
dashboardItem = DashboardItem.AdminMessages(adminMessage = it.data),
|
||||
forceRefresh = forceRefresh
|
||||
)
|
||||
}
|
||||
Status.ERROR -> {
|
||||
Timber.i("Loading dashboard admin message result: An exception occurred")
|
||||
errorHandler.dispatch(it.error!!)
|
||||
updateData(
|
||||
dashboardItem = DashboardItem.AdminMessages(
|
||||
adminMessage = it.data,
|
||||
error = it.error
|
||||
),
|
||||
forceRefresh = forceRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.launch("dashboard_admin_messages")
|
||||
}
|
||||
|
||||
private fun updateData(dashboardItem: DashboardItem, forceRefresh: Boolean) {
|
||||
val isForceRefreshError = forceRefresh && dashboardItem.error != null
|
||||
val isFirstRunDataLoadedError =
|
||||
@ -579,6 +617,13 @@ class DashboardPresenter @Inject constructor(
|
||||
|
||||
sortDashboardItems()
|
||||
|
||||
if (dashboardItem is DashboardItem.AdminMessages && !dashboardItem.isDataLoaded) {
|
||||
dashboardItemsToLoad = dashboardItemsToLoad - DashboardItem.Type.ADMIN_MESSAGE
|
||||
dashboardTileLoadedList = dashboardTileLoadedList - DashboardItem.Tile.ADMIN_MESSAGE
|
||||
|
||||
dashboardItemLoadedList.removeAll { it.type == DashboardItem.Type.ADMIN_MESSAGE }
|
||||
}
|
||||
|
||||
if (forceRefresh) {
|
||||
updateForceRefreshData(dashboardItem)
|
||||
} else {
|
||||
@ -610,9 +655,12 @@ class DashboardPresenter @Inject constructor(
|
||||
}
|
||||
|
||||
private fun updateForceRefreshData(dashboardItem: DashboardItem) {
|
||||
val isNotLoadedAdminMessage =
|
||||
dashboardItem is DashboardItem.AdminMessages && !dashboardItem.isDataLoaded
|
||||
|
||||
with(dashboardItemRefreshLoadedList) {
|
||||
removeAll { it.type == dashboardItem.type }
|
||||
add(dashboardItem)
|
||||
if (!isNotLoadedAdminMessage) add(dashboardItem)
|
||||
}
|
||||
|
||||
val isRefreshItemLoaded =
|
||||
@ -644,7 +692,9 @@ class DashboardPresenter @Inject constructor(
|
||||
itemsLoadedList: List<DashboardItem>,
|
||||
forceRefresh: Boolean
|
||||
) {
|
||||
val filteredItems = itemsLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT }
|
||||
val filteredItems = itemsLoadedList.filterNot {
|
||||
it.type == DashboardItem.Type.ACCOUNT || it.type == DashboardItem.Type.ADMIN_MESSAGE
|
||||
}
|
||||
val isAccountItemError =
|
||||
itemsLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null
|
||||
val isGeneralError =
|
||||
@ -676,10 +726,13 @@ class DashboardPresenter @Inject constructor(
|
||||
val dashboardItemsPosition = preferencesRepository.dashboardItemsPosition
|
||||
|
||||
dashboardItemLoadedList.sortBy { tile ->
|
||||
dashboardItemsPosition?.getOrDefault(
|
||||
tile.type,
|
||||
val defaultPosition = if (tile is DashboardItem.AdminMessages) {
|
||||
-1
|
||||
} else {
|
||||
tile.type.ordinal + 100
|
||||
) ?: tile.type.ordinal
|
||||
}
|
||||
|
||||
dashboardItemsPosition?.getOrDefault(tile.type, defaultPosition) ?: tile.type.ordinal
|
||||
}
|
||||
}
|
||||
}
|
@ -25,4 +25,6 @@ interface DashboardView : BaseView {
|
||||
fun popViewToRoot()
|
||||
|
||||
fun openNotificationsCenterView()
|
||||
|
||||
fun openInternetBrowser(url: String)
|
||||
}
|
@ -3,6 +3,8 @@ package io.github.wulkanowy.ui.modules.debug.notification
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.Student
|
||||
import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.services.sync.notifications.ChangeTimetableNotification
|
||||
import io.github.wulkanowy.services.sync.notifications.NewAttendanceNotification
|
||||
import io.github.wulkanowy.services.sync.notifications.NewConferenceNotification
|
||||
import io.github.wulkanowy.services.sync.notifications.NewExamNotification
|
||||
import io.github.wulkanowy.services.sync.notifications.NewGradeNotification
|
||||
@ -13,6 +15,7 @@ import io.github.wulkanowy.services.sync.notifications.NewNoteNotification
|
||||
import io.github.wulkanowy.services.sync.notifications.NewSchoolAnnouncementNotification
|
||||
import io.github.wulkanowy.ui.base.BasePresenter
|
||||
import io.github.wulkanowy.ui.base.ErrorHandler
|
||||
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugAttendanceItems
|
||||
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugConferenceItems
|
||||
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugExamItems
|
||||
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugGradeDetailsItems
|
||||
@ -22,6 +25,7 @@ import io.github.wulkanowy.ui.modules.debug.notification.mock.debugLuckyNumber
|
||||
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugMessageItems
|
||||
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugNoteItems
|
||||
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugSchoolAnnouncementItems
|
||||
import io.github.wulkanowy.ui.modules.debug.notification.mock.debugTimetableItems
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
@ -37,6 +41,8 @@ class NotificationDebugPresenter @Inject constructor(
|
||||
private val newNoteNotification: NewNoteNotification,
|
||||
private val newSchoolAnnouncementNotification: NewSchoolAnnouncementNotification,
|
||||
private val newLuckyNumberNotification: NewLuckyNumberNotification,
|
||||
private val changeTimetableNotification: ChangeTimetableNotification,
|
||||
private val newAttendanceNotification: NewAttendanceNotification,
|
||||
) : BasePresenter<NotificationDebugView>(errorHandler, studentRepository) {
|
||||
|
||||
private val items = listOf(
|
||||
@ -64,6 +70,12 @@ class NotificationDebugPresenter @Inject constructor(
|
||||
NotificationDebugItem(R.string.note_title) { n ->
|
||||
withStudent { newNoteNotification.notify(debugNoteItems.take(n), it) }
|
||||
},
|
||||
NotificationDebugItem(R.string.attendance_title) { n ->
|
||||
withStudent { newAttendanceNotification.notify(debugAttendanceItems.take(n), it) }
|
||||
},
|
||||
NotificationDebugItem(R.string.timetable_title) { n ->
|
||||
withStudent { changeTimetableNotification.notify(debugTimetableItems.take(n), it) }
|
||||
},
|
||||
NotificationDebugItem(R.string.school_announcement_title) { n ->
|
||||
withStudent {
|
||||
newSchoolAnnouncementNotification.notify(debugSchoolAnnouncementItems.take(n), it)
|
||||
@ -88,7 +100,7 @@ class NotificationDebugPresenter @Inject constructor(
|
||||
}
|
||||
|
||||
private fun withStudent(block: suspend (Student) -> Unit) {
|
||||
launch {
|
||||
presenterScope.launch {
|
||||
block(studentRepository.getCurrentStudent(false))
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,35 @@
|
||||
package io.github.wulkanowy.ui.modules.debug.notification.mock
|
||||
|
||||
import io.github.wulkanowy.data.db.entities.Attendance
|
||||
import java.time.LocalDate
|
||||
|
||||
val debugAttendanceItems = listOf(
|
||||
generateAttendance("Matematyka", "PRESENCE"),
|
||||
generateAttendance("Język angielski", "UNEXCUSED_LATENESS"),
|
||||
generateAttendance("Geografia", "ABSENCE_UNEXCUSED"),
|
||||
generateAttendance("Sieci komputerowe", "ABSENCE_EXCUSED"),
|
||||
generateAttendance("Systemy operacyjne", "EXCUSED_LATENESS"),
|
||||
generateAttendance("Język niemiecki", "ABSENCE_UNEXCUSED"),
|
||||
generateAttendance("Biologia", "ABSENCE_UNEXCUSED"),
|
||||
generateAttendance("Chemia", "ABSENCE_EXCUSED"),
|
||||
generateAttendance("Fizyka", "ABSENCE_UNEXCUSED"),
|
||||
generateAttendance("Matematyka", "ABSENCE_EXCUSED"),
|
||||
)
|
||||
|
||||
private fun generateAttendance(subject: String, name: String) = Attendance(
|
||||
subject = subject,
|
||||
studentId = 0,
|
||||
diaryId = 0,
|
||||
date = LocalDate.now(),
|
||||
timeId = 0,
|
||||
number = 1,
|
||||
name = name,
|
||||
presence = false,
|
||||
absence = false,
|
||||
exemption = false,
|
||||
lateness = false,
|
||||
excused = false,
|
||||
deleted = false,
|
||||
excusable = false,
|
||||
excuseStatus = ""
|
||||
)
|
@ -0,0 +1,39 @@
|
||||
package io.github.wulkanowy.ui.modules.debug.notification.mock
|
||||
|
||||
import io.github.wulkanowy.data.db.entities.Timetable
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.random.Random
|
||||
|
||||
val debugTimetableItems = listOf(
|
||||
generateTimetable("Matematyka", "12", "01"),
|
||||
generateTimetable("Język angielski", "23", "12"),
|
||||
generateTimetable("Geografia", "34", "23"),
|
||||
generateTimetable("Sieci komputerowe", "45", "34"),
|
||||
generateTimetable("Systemy operacyjne", "56", "45"),
|
||||
generateTimetable("Język niemiecki", "67", "56"),
|
||||
generateTimetable("Biologia", "78", "67"),
|
||||
generateTimetable("Chemia", "89", "78"),
|
||||
generateTimetable("Fizyka", "90", "89"),
|
||||
generateTimetable("Matematyka", "01", "90"),
|
||||
)
|
||||
|
||||
private fun generateTimetable(subject: String, room: String, roomOld: String) = Timetable(
|
||||
subject = subject,
|
||||
studentId = 0,
|
||||
diaryId = 0,
|
||||
date = LocalDate.now().minusDays(Random.nextLong(0, 8)),
|
||||
number = 1,
|
||||
start = LocalDateTime.now().plusHours(1),
|
||||
end = LocalDateTime.now(),
|
||||
subjectOld = "",
|
||||
group = "",
|
||||
room = room,
|
||||
roomOld = roomOld,
|
||||
teacher = "Wtorkowska Renata",
|
||||
teacherOld = "",
|
||||
info = "",
|
||||
isStudentPlan = true,
|
||||
changes = true,
|
||||
canceled = true
|
||||
)
|
@ -64,7 +64,7 @@ class ExamFragment : BaseFragment<FragmentExamBinding>(R.layout.fragment_exam),
|
||||
examPreviousButton.setOnClickListener { presenter.onPreviousWeek() }
|
||||
examNextButton.setOnClickListener { presenter.onNextWeek() }
|
||||
|
||||
examNavContainer.setElevationCompat(requireContext().dpToPx(8f))
|
||||
examNavContainer.elevation = requireContext().dpToPx(8f)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,9 @@
|
||||
package io.github.wulkanowy.ui.modules.grade
|
||||
|
||||
enum class GradeExpandMode(val value: String) {
|
||||
ONE("one"), UNLIMITED("any"), ALWAYS_EXPANDED("always");
|
||||
|
||||
companion object {
|
||||
fun getByValue(value: String) = values().firstOrNull { it.value == value } ?: ONE
|
||||
}
|
||||
}
|
@ -8,6 +8,7 @@ import android.view.View
|
||||
import android.view.View.INVISIBLE
|
||||
import android.view.View.VISIBLE
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.Semester
|
||||
@ -29,7 +30,13 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
|
||||
@Inject
|
||||
lateinit var presenter: GradePresenter
|
||||
|
||||
private val pagerAdapter by lazy { BaseFragmentPagerAdapter(childFragmentManager) }
|
||||
private val pagerAdapter by lazy {
|
||||
BaseFragmentPagerAdapter(
|
||||
fragmentManager = childFragmentManager,
|
||||
pagesCount = 3,
|
||||
lifecycle = lifecycle,
|
||||
)
|
||||
}
|
||||
|
||||
private var semesterSwitchMenu: MenuItem? = null
|
||||
|
||||
@ -62,28 +69,35 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
|
||||
}
|
||||
|
||||
override fun initView() {
|
||||
with(pagerAdapter) {
|
||||
containerId = binding.gradeViewPager.id
|
||||
addFragmentsWithTitle(
|
||||
mapOf(
|
||||
GradeDetailsFragment.newInstance() to getString(R.string.all_details),
|
||||
GradeSummaryFragment.newInstance() to getString(R.string.grade_menu_summary),
|
||||
GradeStatisticsFragment.newInstance() to getString(R.string.grade_menu_statistics)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
with(binding.gradeViewPager) {
|
||||
adapter = pagerAdapter
|
||||
offscreenPageLimit = 3
|
||||
setOnSelectPageListener(presenter::onPageSelected)
|
||||
}
|
||||
|
||||
with(binding.gradeTabLayout) {
|
||||
setupWithViewPager(binding.gradeViewPager)
|
||||
setElevationCompat(context.dpToPx(4f))
|
||||
with(pagerAdapter) {
|
||||
containerId = binding.gradeViewPager.id
|
||||
titleFactory = {
|
||||
when (it) {
|
||||
0 -> getString(R.string.all_details)
|
||||
1 -> getString(R.string.grade_menu_summary)
|
||||
2 -> getString(R.string.grade_menu_statistics)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
itemFactory = {
|
||||
when (it) {
|
||||
0 -> GradeDetailsFragment.newInstance()
|
||||
1 -> GradeSummaryFragment.newInstance()
|
||||
2 -> GradeStatisticsFragment.newInstance()
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
TabLayoutMediator(binding.gradeTabLayout, binding.gradeViewPager, this).attach()
|
||||
}
|
||||
|
||||
binding.gradeTabLayout.elevation = requireContext().dpToPx(4f)
|
||||
|
||||
with(binding) {
|
||||
gradeErrorRetry.setOnClickListener { presenter.onRetry() }
|
||||
gradeErrorDetails.setOnClickListener { presenter.onDetailsClick() }
|
||||
|
@ -101,7 +101,6 @@ class GradePresenter @Inject constructor(
|
||||
private fun loadData() {
|
||||
flowWithResource {
|
||||
val student = studentRepository.getCurrentStudent()
|
||||
delay(200)
|
||||
semesterRepository.getSemesters(student, refreshOnNoCurrent = true)
|
||||
}.onEach {
|
||||
when (it.status) {
|
||||
|
@ -5,6 +5,7 @@ import android.content.res.Resources
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.NO_POSITION
|
||||
@ -13,9 +14,11 @@ import io.github.wulkanowy.data.db.entities.Grade
|
||||
import io.github.wulkanowy.databinding.HeaderGradeDetailsBinding
|
||||
import io.github.wulkanowy.databinding.ItemGradeDetailsBinding
|
||||
import io.github.wulkanowy.ui.base.BaseExpandableAdapter
|
||||
import io.github.wulkanowy.ui.modules.grade.GradeExpandMode
|
||||
import io.github.wulkanowy.utils.getBackgroundColor
|
||||
import io.github.wulkanowy.utils.toFormattedString
|
||||
import timber.log.Timber
|
||||
import java.util.BitSet
|
||||
import javax.inject.Inject
|
||||
|
||||
class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<RecyclerView.ViewHolder>() {
|
||||
@ -24,19 +27,20 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
|
||||
|
||||
private var items = mutableListOf<GradeDetailsItem>()
|
||||
|
||||
private var expandedPosition = NO_POSITION
|
||||
private val expandedPositions = BitSet(items.size)
|
||||
|
||||
private var isExpandable = false
|
||||
private var expandMode = GradeExpandMode.ONE
|
||||
|
||||
var onClickListener: (Grade, position: Int) -> Unit = { _, _ -> }
|
||||
|
||||
var colorTheme = ""
|
||||
|
||||
fun setDataItems(data: List<GradeDetailsItem>, isExpanded: Boolean = isExpandable) {
|
||||
fun setDataItems(data: List<GradeDetailsItem>, expandMode: GradeExpandMode = this.expandMode) {
|
||||
headers = data.filter { it.viewType == ViewType.HEADER }.toMutableList()
|
||||
items = if (isExpanded) headers else data.toMutableList()
|
||||
isExpandable = isExpanded
|
||||
expandedPosition = NO_POSITION
|
||||
items =
|
||||
(if (expandMode != GradeExpandMode.ALWAYS_EXPANDED) headers else data).toMutableList()
|
||||
this.expandMode = expandMode
|
||||
expandedPositions.clear()
|
||||
}
|
||||
|
||||
fun updateDetailsItem(position: Int, grade: Grade) {
|
||||
@ -48,7 +52,7 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
|
||||
val candidates = headers.filter { (it.value as GradeDetailsHeader).subject == subject }
|
||||
|
||||
if (candidates.size > 1) {
|
||||
Timber.e("Header with subject $subject found ${candidates.size} times! Expanded: $expandedPosition. Items: $candidates")
|
||||
Timber.e("Header with subject $subject found ${candidates.size} times! Expanded: $expandedPositions. Items: $candidates")
|
||||
}
|
||||
|
||||
return candidates.first()
|
||||
@ -64,9 +68,9 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
|
||||
}
|
||||
|
||||
fun collapseAll() {
|
||||
if (expandedPosition != -1) {
|
||||
refreshList(headers)
|
||||
expandedPosition = NO_POSITION
|
||||
if (!expandedPositions.isEmpty) {
|
||||
refreshList(headers.toMutableList())
|
||||
expandedPositions.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,8 +90,12 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
|
||||
return when (viewType) {
|
||||
ViewType.HEADER.id -> HeaderViewHolder(HeaderGradeDetailsBinding.inflate(inflater, parent, false))
|
||||
ViewType.ITEM.id -> ItemViewHolder(ItemGradeDetailsBinding.inflate(inflater, parent, false))
|
||||
ViewType.HEADER.id -> HeaderViewHolder(
|
||||
HeaderGradeDetailsBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
ViewType.ITEM.id -> ItemViewHolder(
|
||||
ItemGradeDetailsBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
else -> throw IllegalStateException()
|
||||
}
|
||||
}
|
||||
@ -106,46 +114,91 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindHeaderViewHolder(holder: HeaderViewHolder, header: GradeDetailsHeader, position: Int) {
|
||||
val headerPosition = headers.indexOf(items[position])
|
||||
val adapterPosition = holder.bindingAdapterPosition
|
||||
private fun bindHeaderViewHolder(
|
||||
holder: HeaderViewHolder,
|
||||
header: GradeDetailsHeader,
|
||||
position: Int
|
||||
) {
|
||||
val context = holder.binding.root.context
|
||||
val item = items[position]
|
||||
val headerPosition = headers.indexOf(item)
|
||||
|
||||
with(holder.binding) {
|
||||
gradeHeaderDivider.visibility = if (adapterPosition == 0) View.GONE else View.VISIBLE
|
||||
gradeHeaderDivider.isVisible = holder.bindingAdapterPosition != 0
|
||||
with(gradeHeaderSubject) {
|
||||
text = header.subject
|
||||
maxLines = if (headerPosition == expandedPosition) 2 else 1
|
||||
maxLines = if (expandedPositions[headerPosition]) 2 else 1
|
||||
}
|
||||
gradeHeaderAverage.text = formatAverage(header.average, root.context.resources)
|
||||
gradeHeaderPointsSum.text = root.context.getString(R.string.grade_points_sum, header.pointsSum)
|
||||
gradeHeaderPointsSum.visibility = if (!header.pointsSum.isNullOrEmpty()) View.VISIBLE else View.GONE
|
||||
gradeHeaderNumber.text = root.context.resources.getQuantityString(R.plurals.grade_number_item, header.grades.size, header.grades.size)
|
||||
gradeHeaderNote.visibility = if (header.newGrades > 0) View.VISIBLE else View.GONE
|
||||
if (header.newGrades > 0) gradeHeaderNote.text = header.newGrades.toString(10)
|
||||
gradeHeaderPointsSum.text =
|
||||
context.getString(R.string.grade_points_sum, header.pointsSum)
|
||||
gradeHeaderPointsSum.isVisible = !header.pointsSum.isNullOrEmpty()
|
||||
gradeHeaderNumber.text = context.resources.getQuantityString(
|
||||
R.plurals.grade_number_item,
|
||||
header.grades.size,
|
||||
header.grades.size
|
||||
)
|
||||
gradeHeaderNote.isVisible = header.newGrades > 0
|
||||
|
||||
gradeHeaderContainer.isEnabled = isExpandable
|
||||
if (header.newGrades > 0) {
|
||||
gradeHeaderNote.text = header.newGrades.toString()
|
||||
}
|
||||
|
||||
gradeHeaderContainer.isEnabled = expandMode != GradeExpandMode.ALWAYS_EXPANDED
|
||||
gradeHeaderContainer.setOnClickListener {
|
||||
expandedPosition = if (expandedPosition == adapterPosition) -1 else adapterPosition
|
||||
|
||||
if (expandedPosition != NO_POSITION) {
|
||||
refreshList(headers.toMutableList().apply {
|
||||
addAll(headerPosition + 1, header.grades)
|
||||
})
|
||||
scrollToHeaderWithSubItems(headerPosition, header.grades.size)
|
||||
} else {
|
||||
refreshList(headers)
|
||||
}
|
||||
expandGradeHeader(headerPosition, header, holder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatAverage(average: Double?, resources: Resources): String {
|
||||
return if (average == null || average == .0) resources.getString(R.string.grade_no_average)
|
||||
else resources.getString(R.string.grade_average, average)
|
||||
private fun expandGradeHeader(
|
||||
headerPosition: Int,
|
||||
header: GradeDetailsHeader,
|
||||
holder: HeaderViewHolder
|
||||
) {
|
||||
if (expandMode == GradeExpandMode.ONE) {
|
||||
val isHeaderExpanded = expandedPositions[headerPosition]
|
||||
|
||||
expandedPositions.clear()
|
||||
|
||||
if (!isHeaderExpanded) {
|
||||
val updatedItemList = headers.toMutableList()
|
||||
.apply { addAll(headerPosition + 1, header.grades) }
|
||||
|
||||
expandedPositions.set(headerPosition)
|
||||
refreshList(updatedItemList)
|
||||
scrollToHeaderWithSubItems(headerPosition, header.grades.size)
|
||||
} else {
|
||||
refreshList(headers.toMutableList())
|
||||
}
|
||||
} else if (expandMode == GradeExpandMode.UNLIMITED) {
|
||||
val headerAdapterPosition = holder.bindingAdapterPosition
|
||||
val isHeaderExpanded = expandedPositions[headerPosition]
|
||||
|
||||
expandedPositions.flip(headerPosition)
|
||||
|
||||
if (!isHeaderExpanded) {
|
||||
val updatedList = items.toMutableList()
|
||||
.apply { addAll(headerAdapterPosition + 1, header.grades) }
|
||||
|
||||
refreshList(updatedList)
|
||||
scrollToHeaderWithSubItems(headerAdapterPosition, header.grades.size)
|
||||
} else {
|
||||
val startPosition = headerAdapterPosition + 1
|
||||
val updatedList = items.toMutableList()
|
||||
.apply {
|
||||
subList(startPosition, startPosition + header.grades.size).clear()
|
||||
}
|
||||
|
||||
refreshList(updatedList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun bindItemViewHolder(holder: ItemViewHolder, grade: Grade) {
|
||||
val context = holder.binding.root.context
|
||||
|
||||
with(holder.binding) {
|
||||
gradeItemValue.run {
|
||||
text = grade.entry
|
||||
@ -154,26 +207,37 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
|
||||
gradeItemDescription.text = when {
|
||||
grade.description.isNotBlank() -> grade.description
|
||||
grade.gradeSymbol.isNotBlank() -> grade.gradeSymbol
|
||||
else -> root.context.getString(R.string.all_no_description)
|
||||
else -> context.getString(R.string.all_no_description)
|
||||
}
|
||||
gradeItemDate.text = grade.date.toFormattedString()
|
||||
gradeItemWeight.text = "${root.context.getString(R.string.grade_weight)}: ${grade.weight}"
|
||||
gradeItemWeight.text = "${context.getString(R.string.grade_weight)}: ${grade.weight}"
|
||||
gradeItemNote.visibility = if (!grade.isRead) View.VISIBLE else View.GONE
|
||||
|
||||
root.setOnClickListener {
|
||||
holder.bindingAdapterPosition.let { if (it != NO_POSITION) onClickListener(grade, it) }
|
||||
holder.bindingAdapterPosition.let {
|
||||
if (it != NO_POSITION) onClickListener(grade, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatAverage(average: Double?, resources: Resources) =
|
||||
if (average == null || average == .0) {
|
||||
resources.getString(R.string.grade_no_average)
|
||||
} else {
|
||||
resources.getString(R.string.grade_average, average)
|
||||
}
|
||||
|
||||
private class HeaderViewHolder(val binding: HeaderGradeDetailsBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
private class ItemViewHolder(val binding: ItemGradeDetailsBinding) :
|
||||
RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
class GradeDetailsDiffUtil(private val old: List<GradeDetailsItem>, private val new: List<GradeDetailsItem>) :
|
||||
DiffUtil.Callback() {
|
||||
private class GradeDetailsDiffUtil(
|
||||
private val old: List<GradeDetailsItem>,
|
||||
private val new: List<GradeDetailsItem>
|
||||
) : DiffUtil.Callback() {
|
||||
|
||||
override fun getOldListSize() = old.size
|
||||
|
||||
|
@ -12,6 +12,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.Grade
|
||||
import io.github.wulkanowy.ui.modules.grade.GradeExpandMode
|
||||
import io.github.wulkanowy.databinding.FragmentGradeDetailsBinding
|
||||
import io.github.wulkanowy.ui.base.BaseFragment
|
||||
import io.github.wulkanowy.ui.modules.grade.GradeFragment
|
||||
@ -79,10 +80,10 @@ class GradeDetailsFragment :
|
||||
else false
|
||||
}
|
||||
|
||||
override fun updateData(data: List<GradeDetailsItem>, isGradeExpandable: Boolean, gradeColorTheme: String) {
|
||||
override fun updateData(data: List<GradeDetailsItem>, expandMode: GradeExpandMode, gradeColorTheme: String) {
|
||||
with(gradeDetailsAdapter) {
|
||||
colorTheme = gradeColorTheme
|
||||
setDataItems(data, isGradeExpandable)
|
||||
setDataItems(data, expandMode)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import io.github.wulkanowy.data.repositories.StudentRepository
|
||||
import io.github.wulkanowy.ui.base.BasePresenter
|
||||
import io.github.wulkanowy.ui.base.ErrorHandler
|
||||
import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider
|
||||
import io.github.wulkanowy.ui.modules.grade.GradeExpandMode
|
||||
import io.github.wulkanowy.ui.modules.grade.GradeSortingMode.ALPHABETIC
|
||||
import io.github.wulkanowy.ui.modules.grade.GradeSortingMode.DATE
|
||||
import io.github.wulkanowy.ui.modules.grade.GradeSubject
|
||||
@ -16,6 +17,7 @@ import io.github.wulkanowy.utils.AnalyticsHelper
|
||||
import io.github.wulkanowy.utils.afterLoading
|
||||
import io.github.wulkanowy.utils.flowWithResource
|
||||
import io.github.wulkanowy.utils.flowWithResourceIn
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import timber.log.Timber
|
||||
@ -46,8 +48,8 @@ class GradeDetailsPresenter @Inject constructor(
|
||||
fun onParentViewLoadData(semesterId: Int, forceRefresh: Boolean) {
|
||||
currentSemesterId = semesterId
|
||||
|
||||
loadData(semesterId, forceRefresh)
|
||||
if (!forceRefresh) view?.showErrorView(false)
|
||||
loadData(semesterId, forceRefresh)
|
||||
}
|
||||
|
||||
fun onGradeItemSelected(grade: Grade, position: Int) {
|
||||
@ -113,7 +115,7 @@ class GradeDetailsPresenter @Inject constructor(
|
||||
fun onParentViewReselected() {
|
||||
view?.run {
|
||||
if (!isViewEmpty) {
|
||||
if (preferencesRepository.isGradeExpandable) collapseAllItems()
|
||||
if (preferencesRepository.gradeExpandMode != GradeExpandMode.ALWAYS_EXPANDED) collapseAllItems()
|
||||
scrollToStart()
|
||||
}
|
||||
}
|
||||
@ -157,7 +159,7 @@ class GradeDetailsPresenter @Inject constructor(
|
||||
showContent(true)
|
||||
updateData(
|
||||
data = items,
|
||||
isGradeExpandable = preferencesRepository.isGradeExpandable,
|
||||
expandMode = preferencesRepository.gradeExpandMode,
|
||||
gradeColorTheme = preferencesRepository.gradeColorTheme
|
||||
)
|
||||
notifyParentDataLoaded(semesterId)
|
||||
@ -175,7 +177,7 @@ class GradeDetailsPresenter @Inject constructor(
|
||||
showContent(items.isNotEmpty())
|
||||
updateData(
|
||||
data = items,
|
||||
isGradeExpandable = preferencesRepository.isGradeExpandable,
|
||||
expandMode = preferencesRepository.gradeExpandMode,
|
||||
gradeColorTheme = preferencesRepository.gradeColorTheme
|
||||
)
|
||||
}
|
||||
@ -197,6 +199,9 @@ class GradeDetailsPresenter @Inject constructor(
|
||||
enableSwipe(true)
|
||||
notifyParentDataLoaded(semesterId)
|
||||
}
|
||||
}.catch {
|
||||
errorHandler.dispatch(it)
|
||||
view?.notifyParentDataLoaded(semesterId)
|
||||
}.launch()
|
||||
}
|
||||
|
||||
@ -213,6 +218,7 @@ class GradeDetailsPresenter @Inject constructor(
|
||||
setErrorDetails(message)
|
||||
showErrorView(true)
|
||||
showEmpty(false)
|
||||
showProgress(false)
|
||||
} else showError(message, error)
|
||||
}
|
||||
}
|
||||
@ -235,14 +241,24 @@ class GradeDetailsPresenter @Inject constructor(
|
||||
.sortedByDescending { it.date }
|
||||
.map { GradeDetailsItem(it, ViewType.ITEM) }
|
||||
|
||||
listOf(GradeDetailsItem(GradeDetailsHeader(
|
||||
subject = subject,
|
||||
average = average,
|
||||
pointsSum = points,
|
||||
grades = subItems
|
||||
).apply {
|
||||
newGrades = grades.filter { grade -> !grade.isRead }.size
|
||||
}, ViewType.HEADER)) + if (preferencesRepository.isGradeExpandable) emptyList() else subItems
|
||||
val gradeDetailsItems = listOf(
|
||||
GradeDetailsItem(
|
||||
GradeDetailsHeader(
|
||||
subject = subject,
|
||||
average = average,
|
||||
pointsSum = points,
|
||||
grades = subItems
|
||||
).apply {
|
||||
newGrades = grades.filter { grade -> !grade.isRead }.size
|
||||
}, ViewType.HEADER
|
||||
)
|
||||
)
|
||||
|
||||
if (preferencesRepository.gradeExpandMode == GradeExpandMode.ALWAYS_EXPANDED) {
|
||||
gradeDetailsItems + subItems
|
||||
} else {
|
||||
gradeDetailsItems
|
||||
}
|
||||
}.flatten()
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
package io.github.wulkanowy.ui.modules.grade.details
|
||||
|
||||
import io.github.wulkanowy.data.db.entities.Grade
|
||||
import io.github.wulkanowy.ui.modules.grade.GradeExpandMode
|
||||
import io.github.wulkanowy.ui.base.BaseView
|
||||
|
||||
interface GradeDetailsView : BaseView {
|
||||
@ -9,7 +10,7 @@ interface GradeDetailsView : BaseView {
|
||||
|
||||
fun initView()
|
||||
|
||||
fun updateData(data: List<GradeDetailsItem>, isGradeExpandable: Boolean, gradeColorTheme: String)
|
||||
fun updateData(data: List<GradeDetailsItem>, expandMode: GradeExpandMode, gradeColorTheme: String)
|
||||
|
||||
fun updateItem(item: Grade, position: Int)
|
||||
|
||||
|
@ -68,7 +68,7 @@ class GradeStatisticsFragment :
|
||||
}
|
||||
|
||||
with(binding) {
|
||||
gradeStatisticsSubjectsContainer.setElevationCompat(requireContext().dpToPx(1f))
|
||||
gradeStatisticsSubjectsContainer.elevation = requireContext().dpToPx(1f)
|
||||
|
||||
gradeStatisticsSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
|
||||
gradeStatisticsSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
|
||||
|
@ -10,6 +10,7 @@ import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.Homework
|
||||
import io.github.wulkanowy.databinding.FragmentHomeworkBinding
|
||||
import io.github.wulkanowy.ui.base.BaseFragment
|
||||
import io.github.wulkanowy.ui.modules.homework.add.HomeworkAddDialog
|
||||
import io.github.wulkanowy.ui.modules.homework.details.HomeworkDetailsDialog
|
||||
import io.github.wulkanowy.ui.modules.main.MainActivity
|
||||
import io.github.wulkanowy.ui.modules.main.MainView
|
||||
@ -64,7 +65,9 @@ class HomeworkFragment : BaseFragment<FragmentHomeworkBinding>(R.layout.fragment
|
||||
homeworkPreviousButton.setOnClickListener { presenter.onPreviousDay() }
|
||||
homeworkNextButton.setOnClickListener { presenter.onNextDay() }
|
||||
|
||||
homeworkNavContainer.setElevationCompat(requireContext().dpToPx(8f))
|
||||
openAddHomeworkButton.setOnClickListener { presenter.onHomeworkAddButtonClicked() }
|
||||
|
||||
homeworkNavContainer.elevation = requireContext().dpToPx(8f)
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,10 +125,14 @@ class HomeworkFragment : BaseFragment<FragmentHomeworkBinding>(R.layout.fragment
|
||||
binding.homeworkNextButton.visibility = if (show) VISIBLE else View.INVISIBLE
|
||||
}
|
||||
|
||||
override fun showTimetableDialog(homework: Homework) {
|
||||
override fun showHomeworkDialog(homework: Homework) {
|
||||
(activity as? MainActivity)?.showDialogFragment(HomeworkDetailsDialog.newInstance(homework))
|
||||
}
|
||||
|
||||
override fun showAddHomeworkDialog() {
|
||||
(activity as? MainActivity)?.showDialogFragment(HomeworkAddDialog())
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putLong(SAVED_DATE_KEY, presenter.currentDate.toEpochDay())
|
||||
|
@ -78,7 +78,11 @@ class HomeworkPresenter @Inject constructor(
|
||||
|
||||
fun onHomeworkItemSelected(homework: Homework) {
|
||||
Timber.i("Select homework item ${homework.id}")
|
||||
view?.showTimetableDialog(homework)
|
||||
view?.showHomeworkDialog(homework)
|
||||
}
|
||||
|
||||
fun onHomeworkAddButtonClicked() {
|
||||
view?.showAddHomeworkDialog()
|
||||
}
|
||||
|
||||
private fun setBaseDateOnHolidays() {
|
||||
|
@ -33,5 +33,7 @@ interface HomeworkView : BaseView {
|
||||
|
||||
fun showNextButton(show: Boolean)
|
||||
|
||||
fun showTimetableDialog(homework: Homework)
|
||||
fun showHomeworkDialog(homework: Homework)
|
||||
|
||||
fun showAddHomeworkDialog()
|
||||
}
|
||||
|
@ -0,0 +1,124 @@
|
||||
package io.github.wulkanowy.ui.modules.homework.add
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.widget.doOnTextChanged
|
||||
import com.google.android.material.datepicker.CalendarConstraints
|
||||
import com.google.android.material.datepicker.MaterialDatePicker
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.databinding.DialogHomeworkAddBinding
|
||||
import io.github.wulkanowy.ui.base.BaseDialogFragment
|
||||
import io.github.wulkanowy.utils.toFormattedString
|
||||
import io.github.wulkanowy.utils.toLocalDateTime
|
||||
import io.github.wulkanowy.utils.toTimestamp
|
||||
import java.time.LocalDate
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class HomeworkAddDialog : BaseDialogFragment<DialogHomeworkAddBinding>(), HomeworkAddView {
|
||||
|
||||
@Inject
|
||||
lateinit var presenter: HomeworkAddPresenter
|
||||
|
||||
private var date: LocalDate? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NO_TITLE, 0)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
) = DialogHomeworkAddBinding.inflate(inflater).apply { binding = this }.root
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
presenter.onAttachView(this)
|
||||
}
|
||||
|
||||
override fun initView() {
|
||||
with(binding) {
|
||||
homeworkDialogSubjectEdit.doOnTextChanged { _, _, _, _ ->
|
||||
homeworkDialogSubject.error = null
|
||||
homeworkDialogSubject.isErrorEnabled = false
|
||||
}
|
||||
homeworkDialogDateEdit.doOnTextChanged { _, _, _, _ ->
|
||||
homeworkDialogDate.error = null
|
||||
homeworkDialogDate.isErrorEnabled = false
|
||||
}
|
||||
homeworkDialogContentEdit.doOnTextChanged { _, _, _, _ ->
|
||||
homeworkDialogContent.error = null
|
||||
homeworkDialogContent.isErrorEnabled = false
|
||||
}
|
||||
homeworkDialogClose.setOnClickListener { dismiss() }
|
||||
homeworkDialogDateEdit.setOnClickListener { presenter.showDatePicker(date) }
|
||||
homeworkDialogAdd.setOnClickListener {
|
||||
presenter.onAddHomeworkClicked(
|
||||
subject = homeworkDialogSubjectEdit.text?.toString(),
|
||||
teacher = homeworkDialogTeacherEdit.text?.toString(),
|
||||
date = homeworkDialogDateEdit.text?.toString(),
|
||||
content = homeworkDialogContentEdit.text?.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun showSuccessMessage() {
|
||||
showMessage(getString(R.string.homework_add_success))
|
||||
}
|
||||
|
||||
override fun setErrorSubjectRequired() {
|
||||
with(binding.homeworkDialogSubject) {
|
||||
isErrorEnabled = true
|
||||
error = getString(R.string.error_field_required)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setErrorDateRequired() {
|
||||
with(binding.homeworkDialogDate) {
|
||||
isErrorEnabled = true
|
||||
error = getString(R.string.error_field_required)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setErrorContentRequired() {
|
||||
with(binding.homeworkDialogContent) {
|
||||
isErrorEnabled = true
|
||||
error = getString(R.string.error_field_required)
|
||||
}
|
||||
}
|
||||
|
||||
override fun closeDialog() {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
override fun showDatePickerDialog(currentDate: LocalDate) {
|
||||
val constraintsBuilder = CalendarConstraints.Builder().apply {
|
||||
setStart(LocalDate.now().toEpochDay())
|
||||
}
|
||||
val datePicker =
|
||||
MaterialDatePicker.Builder.datePicker()
|
||||
.setCalendarConstraints(constraintsBuilder.build())
|
||||
.setSelection(currentDate.toTimestamp())
|
||||
.build()
|
||||
|
||||
datePicker.addOnPositiveButtonClickListener {
|
||||
date = it.toLocalDateTime().toLocalDate()
|
||||
binding.homeworkDialogDate.editText?.setText(date!!.toFormattedString())
|
||||
}
|
||||
|
||||
if (!parentFragmentManager.isStateSaved) {
|
||||
datePicker.show(this.parentFragmentManager, null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
presenter.onDetachView()
|
||||
super.onDestroyView()
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
package io.github.wulkanowy.ui.modules.homework.add
|
||||
|
||||
import io.github.wulkanowy.data.Status
|
||||
import io.github.wulkanowy.data.db.entities.Homework
|
||||
import io.github.wulkanowy.data.repositories.HomeworkRepository
|
||||
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
|
||||
import io.github.wulkanowy.utils.flowWithResource
|
||||
import io.github.wulkanowy.utils.toLocalDate
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import timber.log.Timber
|
||||
import java.time.LocalDate
|
||||
import javax.inject.Inject
|
||||
|
||||
class HomeworkAddPresenter @Inject constructor(
|
||||
errorHandler: ErrorHandler,
|
||||
studentRepository: StudentRepository,
|
||||
private val homeworkRepository: HomeworkRepository,
|
||||
private val semesterRepository: SemesterRepository
|
||||
) : BasePresenter<HomeworkAddView>(errorHandler, studentRepository) {
|
||||
|
||||
override fun onAttachView(view: HomeworkAddView) {
|
||||
super.onAttachView(view)
|
||||
view.initView()
|
||||
Timber.i("Homework details view was initialized")
|
||||
}
|
||||
|
||||
fun showDatePicker(date: LocalDate?) {
|
||||
view?.showDatePickerDialog(date ?: LocalDate.now())
|
||||
}
|
||||
|
||||
fun onAddHomeworkClicked(subject: String?, teacher: String?, date: String?, content: String?) {
|
||||
var isError = false
|
||||
|
||||
if (subject.isNullOrBlank()) {
|
||||
view?.setErrorSubjectRequired()
|
||||
isError = true
|
||||
}
|
||||
|
||||
if (date.isNullOrBlank()) {
|
||||
view?.setErrorDateRequired()
|
||||
isError = true
|
||||
}
|
||||
|
||||
if (content.isNullOrBlank()) {
|
||||
view?.setErrorContentRequired()
|
||||
isError = true
|
||||
}
|
||||
|
||||
if (!isError) {
|
||||
saveHomework(subject!!, teacher.orEmpty(), date!!.toLocalDate(), content!!)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveHomework(subject: String, teacher: String, date: LocalDate, content: String) {
|
||||
flowWithResource {
|
||||
val student = studentRepository.getCurrentStudent()
|
||||
val semester = semesterRepository.getCurrentSemester(student)
|
||||
val entryDate = LocalDate.now()
|
||||
homeworkRepository.saveHomework(
|
||||
Homework(
|
||||
semesterId = semester.semesterId,
|
||||
studentId = student.studentId,
|
||||
date = date,
|
||||
entryDate = entryDate,
|
||||
subject = subject,
|
||||
content = content,
|
||||
teacher = teacher,
|
||||
teacherSymbol = "",
|
||||
attachments = emptyList(),
|
||||
).apply { isAddedByUser = true }
|
||||
)
|
||||
}.onEach {
|
||||
when (it.status) {
|
||||
Status.LOADING -> Timber.i("Homework insert start")
|
||||
Status.SUCCESS -> {
|
||||
Timber.i("Homework insert: Success")
|
||||
view?.run {
|
||||
showSuccessMessage()
|
||||
closeDialog()
|
||||
}
|
||||
}
|
||||
Status.ERROR -> {
|
||||
Timber.i("Homework insert result: An exception occurred")
|
||||
errorHandler.dispatch(it.error!!)
|
||||
}
|
||||
}
|
||||
}.launch("add_homework")
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package io.github.wulkanowy.ui.modules.homework.add
|
||||
|
||||
import io.github.wulkanowy.ui.base.BaseView
|
||||
import java.time.LocalDate
|
||||
|
||||
interface HomeworkAddView : BaseView {
|
||||
|
||||
fun initView()
|
||||
|
||||
fun showSuccessMessage()
|
||||
|
||||
fun setErrorSubjectRequired()
|
||||
|
||||
fun setErrorDateRequired()
|
||||
|
||||
fun setErrorContentRequired()
|
||||
|
||||
fun closeDialog()
|
||||
|
||||
fun showDatePickerDialog(currentDate: LocalDate)
|
||||
}
|
@ -5,10 +5,12 @@ import android.view.View.GONE
|
||||
import android.view.View.VISIBLE
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.github.wulkanowy.R
|
||||
import io.github.wulkanowy.data.db.entities.Homework
|
||||
import io.github.wulkanowy.databinding.ItemHomeworkDialogAttachmentBinding
|
||||
import io.github.wulkanowy.databinding.ItemHomeworkDialogAttachmentsHeaderBinding
|
||||
import io.github.wulkanowy.databinding.ItemHomeworkDialogDetailsBinding
|
||||
import io.github.wulkanowy.utils.ifNullOrBlank
|
||||
import io.github.wulkanowy.utils.toFormattedString
|
||||
import javax.inject.Inject
|
||||
|
||||
@ -37,6 +39,8 @@ class HomeworkDetailsAdapter @Inject constructor() :
|
||||
|
||||
var onFullScreenExitClickListener = {}
|
||||
|
||||
var onDeleteClickListener: (homework: Homework) -> Unit = {}
|
||||
|
||||
override fun getItemCount() = 1 + if (attachments.isNotEmpty()) attachments.size + 1 else 0
|
||||
|
||||
override fun getItemViewType(position: Int) = when (position) {
|
||||
@ -49,9 +53,15 @@ class HomeworkDetailsAdapter @Inject constructor() :
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
|
||||
return when (viewType) {
|
||||
ViewType.ATTACHMENTS_HEADER.id -> AttachmentsHeaderViewHolder(ItemHomeworkDialogAttachmentsHeaderBinding.inflate(inflater, parent, false))
|
||||
ViewType.ATTACHMENT.id -> AttachmentViewHolder(ItemHomeworkDialogAttachmentBinding.inflate(inflater, parent, false))
|
||||
else -> DetailsViewHolder(ItemHomeworkDialogDetailsBinding.inflate(inflater, parent, false))
|
||||
ViewType.ATTACHMENTS_HEADER.id -> AttachmentsHeaderViewHolder(
|
||||
ItemHomeworkDialogAttachmentsHeaderBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
ViewType.ATTACHMENT.id -> AttachmentViewHolder(
|
||||
ItemHomeworkDialogAttachmentBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
else -> DetailsViewHolder(
|
||||
ItemHomeworkDialogDetailsBinding.inflate(inflater, parent, false)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,12 +73,15 @@ class HomeworkDetailsAdapter @Inject constructor() :
|
||||
}
|
||||
|
||||
private fun bindDetailsViewHolder(holder: DetailsViewHolder) {
|
||||
val noDataString = holder.binding.root.context.getString(R.string.all_no_data)
|
||||
|
||||
with(holder.binding) {
|
||||
homeworkDialogDate.text = homework?.date?.toFormattedString()
|
||||
homeworkDialogEntryDate.text = homework?.entryDate?.toFormattedString()
|
||||
homeworkDialogSubject.text = homework?.subject
|
||||
homeworkDialogTeacher.text = homework?.teacher
|
||||
homeworkDialogContent.text = homework?.content
|
||||
homeworkDialogSubject.text = homework?.subject.ifNullOrBlank { noDataString }
|
||||
homeworkDialogTeacher.text = homework?.teacher.ifNullOrBlank { noDataString }
|
||||
homeworkDialogContent.text = homework?.content.ifNullOrBlank { noDataString }
|
||||
homeworkDialogDelete.visibility = if (homework?.isAddedByUser == true) VISIBLE else GONE
|
||||
homeworkDialogFullScreen.visibility = if (isHomeworkFullscreen) GONE else VISIBLE
|
||||
homeworkDialogFullScreenExit.visibility = if (isHomeworkFullscreen) VISIBLE else GONE
|
||||
homeworkDialogFullScreen.setOnClickListener {
|
||||
@ -81,6 +94,9 @@ class HomeworkDetailsAdapter @Inject constructor() :
|
||||
homeworkDialogFullScreenExit.visibility = GONE
|
||||
onFullScreenExitClickListener()
|
||||
}
|
||||
homeworkDialogDelete.setOnClickListener {
|
||||
onDeleteClickListener(homework!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,6 +25,9 @@ class HomeworkDetailsDialog : BaseDialogFragment<DialogHomeworkBinding>(), Homew
|
||||
@Inject
|
||||
lateinit var detailsAdapter: HomeworkDetailsAdapter
|
||||
|
||||
override val homeworkDeleteSuccess: String
|
||||
get() = getString(R.string.homework_delete_success)
|
||||
|
||||
private lateinit var homework: Homework
|
||||
|
||||
companion object {
|
||||
@ -82,12 +85,17 @@ class HomeworkDetailsDialog : BaseDialogFragment<DialogHomeworkBinding>(), Homew
|
||||
dialog?.window?.setLayout(WRAP_CONTENT, WRAP_CONTENT)
|
||||
presenter.isHomeworkFullscreen = false
|
||||
}
|
||||
onDeleteClickListener = { homework -> presenter.deleteHomework(homework) }
|
||||
isHomeworkFullscreen = presenter.isHomeworkFullscreen
|
||||
homework = this@HomeworkDetailsDialog.homework
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun closeDialog() {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
override fun updateMarkAsDoneLabel(isDone: Boolean) {
|
||||
binding.homeworkDialogRead.text =
|
||||
view?.context?.getString(if (isDone) R.string.homework_mark_as_undone else R.string.homework_mark_as_done)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user