1
0

Compare commits

..

2 Commits

Author SHA1 Message Date
7a85d13812 Merge branch 'develop' into feature/update-colors 2024-03-13 19:20:39 +01:00
b978abfcbe Update primary colors 2024-02-19 22:33:15 +01:00
173 changed files with 629 additions and 11429 deletions

2
.gitignore vendored
View File

@ -71,7 +71,6 @@ captures/
.idea/deploymentTargetDropDown.xml
.idea/deploymentTargetSelector.xml
.idea/kotlinc.xml
.idea/studiobot.xml
# Keystore files
*.jks
@ -128,4 +127,3 @@ google-services.json
!app/google-services.json
.idea/appInsightsSettings.xml

View File

@ -27,8 +27,8 @@ android {
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21
targetSdkVersion 34
versionCode 177
versionName "2.7.0"
versionCode 150
versionName "2.5.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resValue "string", "app_name", "Wulkanowy"
@ -160,8 +160,8 @@ play {
defaultToAppBundles = false
track = 'production'
releaseStatus = ReleaseStatus.IN_PROGRESS
userFraction = 0.99d
updatePriority = 2
userFraction = 0.50d
updatePriority = 1
enabled.set(false)
}
@ -186,30 +186,28 @@ ext {
android_hilt = "1.2.0"
room = "2.6.1"
chucker = "4.0.0"
mockk = "1.13.11"
coroutines = "1.8.1"
mockk = "1.13.10"
coroutines = "1.8.0"
}
dependencies {
implementation 'io.github.wulkanowy:sdk:2.7.0'
implementation 'io.github.wulkanowy:sdk:2.5.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:$coroutines"
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation "androidx.activity:activity-ktx:1.9.0"
implementation "androidx.activity:activity-ktx:1.8.2"
implementation "androidx.appcompat:appcompat:1.6.1"
implementation "androidx.fragment:fragment-ktx:1.7.1"
implementation "androidx.annotation:annotation:1.8.0"
implementation "androidx.javascriptengine:javascriptengine:1.0.0-beta01"
implementation "androidx.fragment:fragment-ktx:1.6.2"
implementation "androidx.annotation:annotation:1.7.1"
implementation "androidx.preference:preference-ktx:1.2.1"
implementation "androidx.recyclerview:recyclerview:1.3.2"
implementation "androidx.viewpager2:viewpager2:1.1.0"
implementation "androidx.viewpager2:viewpager2:1.1.0-beta02"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
@ -221,7 +219,7 @@ dependencies {
implementation "androidx.work:work-runtime:$work_manager"
playImplementation "androidx.work:work-gcm:$work_manager"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.7.0"
implementation "androidx.room:room-runtime:$room"
implementation "androidx.room:room-ktx:$room"
@ -235,7 +233,7 @@ dependencies {
implementation 'com.github.ncapdevi:FragNav:3.3.0'
implementation "com.github.YarikSOffice:lingver:1.3.0"
implementation 'com.squareup.retrofit2:retrofit:2.11.0'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0"
implementation "com.squareup.okhttp3:logging-interceptor:4.12.0"
implementation "com.squareup.okhttp3:okhttp-urlconnection:4.12.0"
@ -248,15 +246,15 @@ dependencies {
implementation "io.github.wulkanowy:AppKillerManager:3.0.1"
implementation 'me.xdrop:fuzzywuzzy:1.4.0'
implementation 'com.fredporciuncula:flow-preferences:1.9.1'
implementation 'org.apache.commons:commons-text:1.12.0'
implementation 'org.apache.commons:commons-text:1.11.0'
playImplementation platform('com.google.firebase:firebase-bom:33.0.0')
playImplementation platform('com.google.firebase:firebase-bom:32.7.4')
playImplementation 'com.google.firebase:firebase-analytics'
playImplementation 'com.google.firebase:firebase-messaging'
playImplementation 'com.google.firebase:firebase-crashlytics:'
playImplementation 'com.google.firebase:firebase-config'
playImplementation 'com.google.android.gms:play-services-ads:22.6.0'
playImplementation 'com.google.android.gms:play-services-ads:23.0.0'
playImplementation "com.google.android.play:integrity:1.3.0"
playImplementation 'com.google.android.play:app-update-ktx:2.1.0'
playImplementation 'com.google.android.play:review-ktx:2.0.1'
@ -276,7 +274,7 @@ dependencies {
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines"
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
testImplementation 'org.robolectric:robolectric:4.12.2'
testImplementation 'org.robolectric:robolectric:4.11.1'
testImplementation "androidx.test:runner:1.5.2"
testImplementation "androidx.test.ext:junit:1.1.5"
testImplementation "androidx.test:core:1.5.0"

View File

@ -36,37 +36,6 @@
"status": 2
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:1091101852179:android:b558a25f65d088b1",
"android_client_info": {
"package_name": "io.github.wulkanowy"
}
},
"oauth_client": [
{
"client_id": "",
"client_type": 3
}
],
"api_key": [
{
"current_key": ""
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
}
}
}
],
"configuration_version": "1"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -3,8 +3,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="internalOnly">
<uses-sdk tools:overrideLibrary="androidx.javascriptengine" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
@ -44,16 +42,16 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:resizeableActivity="true"
android:supportsRtl="false"
android:theme="@style/WulkanowyTheme"
android:resizeableActivity="true"
tools:ignore="DataExtractionRules,UnusedAttribute">
<activity
android:name=".ui.modules.splash.SplashActivity"
android:exported="true"
android:screenOrientation="portrait"
android:theme="@style/WulkanowyTheme.SplashScreen"
tools:ignore="DiscouragedApi,LockedOrientationActivity">
tools:ignore="LockedOrientationActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

View File

@ -13,8 +13,8 @@ import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import io.github.wulkanowy.data.api.services.SchoolsService
import io.github.wulkanowy.data.api.services.WulkanowyService
import io.github.wulkanowy.data.api.AdminMessageService
import io.github.wulkanowy.data.api.SchoolsService
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.PreferencesRepository
@ -71,7 +71,7 @@ internal class DataModule {
okHttpClient: OkHttpClient,
json: Json,
appInfo: AppInfo
): WulkanowyService = Retrofit.Builder()
): AdminMessageService = Retrofit.Builder()
.baseUrl(appInfo.messagesBaseUrl)
.client(okHttpClient)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))

View File

@ -1,6 +1,5 @@
package io.github.wulkanowy.data
import io.github.wulkanowy.data.repositories.isEndDateReached
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
@ -268,8 +267,7 @@ inline fun <DatabaseType, ApiType, OutputType> networkBoundResource(
emit(Resource.Loading())
val data = query().first()
val updatedShouldFetch = if (isEndDateReached) false else shouldFetch(data)
if (updatedShouldFetch) {
if (shouldFetch(data)) {
emit(Resource.Intermediate(data))
try {

View File

@ -1,49 +1,25 @@
package io.github.wulkanowy.data
import android.content.Context
import android.os.Build
import androidx.javascriptengine.JavaScriptSandbox
import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.google.common.util.concurrent.ListenableFuture
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentIsEduOne
import io.github.wulkanowy.data.repositories.WulkanowyRepository
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.scrapper.EvaluateHandler
import io.github.wulkanowy.utils.RemoteConfigHelper
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
import kotlinx.coroutines.guava.await
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class WulkanowySdkFactory @Inject constructor(
@ApplicationContext private val context: Context,
private val chuckerInterceptor: ChuckerInterceptor,
private val remoteConfig: RemoteConfigHelper,
private val webkitCookieManagerProxy: WebkitCookieManagerProxy,
private val studentDb: StudentDao,
private val wulkanowyRepository: WulkanowyRepository,
private val webkitCookieManagerProxy: WebkitCookieManagerProxy
) {
private val eduOneMutex = Mutex()
private val migrationFailedStudentIds = mutableSetOf<Long>()
private val sandbox: ListenableFuture<JavaScriptSandbox>? =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && JavaScriptSandbox.isSupported())
runCatching { JavaScriptSandbox.createConnectedInstanceAsync(context) }
.onFailure { Timber.e(it) }
.getOrNull()
else null
private val sdk = Sdk().apply {
androidVersion = Build.VERSION.RELEASE
buildTag = Build.MODEL
androidVersion = android.os.Build.VERSION.RELEASE
buildTag = android.os.Build.MODEL
userAgentTemplate = remoteConfig.userAgentTemplate
setSimpleHttpLogger { Timber.d(it) }
setAdditionalCookieManager(webkitCookieManagerProxy)
@ -52,47 +28,9 @@ class WulkanowySdkFactory @Inject constructor(
addInterceptor(chuckerInterceptor, network = true)
}
fun createBase() = sdk
fun create() = sdk
suspend fun create(): Sdk {
val mapping = wulkanowyRepository.getMapping()
return createBase().apply {
if (mapping != null) {
endpointsMapping = mapping.endpoints
vTokenMapping = mapping.vTokens
vHeaders = mapping.vHeaders
responseMapping = mapping.responseMap
vParamsEvaluation = createIsolate()
}
}
}
private suspend fun createIsolate(): suspend () -> EvaluateHandler {
return {
val isolate = sandbox?.await()?.createIsolate()
object : EvaluateHandler {
override suspend fun evaluate(code: String): String? {
return isolate?.evaluateJavaScriptAsync(code)?.await()
}
override fun close() {
isolate?.close()
}
}
}
}
suspend fun create(student: Student, semester: Semester? = null): Sdk {
val overrideIsEduOne = checkEduOneAndMigrateIfNecessary(student)
return buildSdk(student, semester, overrideIsEduOne)
}
private suspend fun buildSdk(
student: Student,
semester: Semester?,
isStudentEduOne: Boolean
): Sdk {
fun create(student: Student, semester: Semester? = null): Sdk {
return create().apply {
email = student.email
password = student.password
@ -101,7 +39,6 @@ class WulkanowySdkFactory @Inject constructor(
studentId = student.studentId
classId = student.classId
emptyCookieJarInterceptor = true
isEduOne = isStudentEduOne
if (Sdk.Mode.valueOf(student.loginMode) == Sdk.Mode.HEBE) {
mobileBaseUrl = student.mobileBaseUrl
@ -124,51 +61,4 @@ class WulkanowySdkFactory @Inject constructor(
}
}
}
private suspend fun checkEduOneAndMigrateIfNecessary(student: Student): Boolean {
if (student.isEduOne != null) return student.isEduOne
if (student.id in migrationFailedStudentIds) {
Timber.i("Migration eduOne: skipping because of previous failure")
return false
}
eduOneMutex.withLock {
if (student.id in migrationFailedStudentIds) {
Timber.i("Migration eduOne: skipping because of previous failure")
return false
}
val studentFromDatabase = studentDb.loadById(student.id)
if (studentFromDatabase?.isEduOne != null) {
Timber.i("Migration eduOne: already done")
return studentFromDatabase.isEduOne
}
Timber.i("Migration eduOne: flag missing. Running migration...")
val initializedSdk = buildSdk(
student = student,
semester = null,
isStudentEduOne = false, // doesn't matter
)
val newCurrentStudent = runCatching { initializedSdk.getCurrentStudent() }
.onFailure { Timber.e(it, "Migration eduOne: can't get current student") }
.getOrNull()
if (newCurrentStudent == null) {
Timber.i("Migration eduOne: failed, so skipping")
migrationFailedStudentIds.add(student.id)
return false
}
Timber.i("Migration eduOne: success. New isEduOne flag: ${newCurrentStudent.isEduOne}")
val studentIsEduOne = StudentIsEduOne(
id = student.id,
isEduOne = newCurrentStudent.isEduOne
)
studentDb.update(studentIsEduOne)
return newCurrentStudent.isEduOne
}
}
}

View File

@ -1,16 +1,12 @@
package io.github.wulkanowy.data.api.services
package io.github.wulkanowy.data.api
import io.github.wulkanowy.data.api.models.Mapping
import io.github.wulkanowy.data.db.entities.AdminMessage
import retrofit2.http.GET
import javax.inject.Singleton
@Singleton
interface WulkanowyService {
interface AdminMessageService {
@GET("/v1.json")
suspend fun getAdminMessages(): List<AdminMessage>
@GET("/mapping4.json")
suspend fun getMapping(): Mapping
}
}

View File

@ -1,4 +1,4 @@
package io.github.wulkanowy.data.api.services
package io.github.wulkanowy.data.api
import io.github.wulkanowy.data.pojos.IntegrityRequest
import io.github.wulkanowy.data.pojos.LoginEvent

View File

@ -1,23 +0,0 @@
package io.github.wulkanowy.data.api.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Mapping(
@SerialName("endpoints")
val endpoints: Map<String, Map<String, Map<String, String>>>,
@SerialName("vTokens")
val vTokens: Map<String, Map<String, Map<String, String>>>,
@SerialName("vTokenScheme")
val vTokenScheme: Map<String, Map<String, String>> = emptyMap(),
@SerialName("vHeaders")
val vHeaders: Map<String, Map<String, Map<String, String>>> = emptyMap(),
@SerialName("responseMap")
val responseMap: Map<String, Map<String, Map<String, Map<String, String>>>> = emptyMap(),
)

View File

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

View File

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

View File

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

View File

@ -14,6 +14,6 @@ interface SemesterDao : BaseDao<Semester> {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertSemesters(items: List<Semester>): List<Long>
@Query("SELECT * FROM Semesters WHERE (student_id = :studentId AND class_id = :classId) OR (student_id = :studentId AND class_id = 0)")
@Query("SELECT * FROM Semesters WHERE student_id = :studentId AND class_id = :classId")
suspend fun loadAll(studentId: Int, classId: Int): List<Semester>
}

View File

@ -9,8 +9,6 @@ import androidx.room.Transaction
import androidx.room.Update
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentIsAuthorized
import io.github.wulkanowy.data.db.entities.StudentIsEduOne
import io.github.wulkanowy.data.db.entities.StudentName
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import javax.inject.Singleton
@ -25,12 +23,6 @@ abstract class StudentDao {
@Delete
abstract suspend fun delete(student: Student)
@Update(entity = Student::class)
abstract suspend fun update(studentIsAuthorized: StudentIsAuthorized)
@Update(entity = Student::class)
abstract suspend fun update(studentIsEduOne: StudentIsEduOne)
@Update(entity = Student::class)
abstract suspend fun update(studentNickAndAvatar: StudentNickAndAvatar)
@ -47,11 +39,11 @@ abstract class StudentDao {
abstract suspend fun loadAll(): List<Student>
@Transaction
@Query("SELECT * FROM Students JOIN Semesters ON (Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id) OR (Students.student_id = Semesters.student_id AND Semesters.class_id = 0)")
@Query("SELECT * FROM Students JOIN Semesters ON Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id")
abstract suspend fun loadStudentsWithSemesters(): Map<Student, List<Semester>>
@Transaction
@Query("SELECT * FROM Students JOIN Semesters ON (Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id) OR (Students.student_id = Semesters.student_id AND Semesters.class_id = 0) WHERE Students.id = :id")
@Query("SELECT * FROM Students JOIN Semesters ON Students.student_id = Semesters.student_id AND Students.class_id = Semesters.class_id WHERE Students.id = :id")
abstract suspend fun loadStudentWithSemestersById(id: Long): Map<Student, List<Semester>>
@Query("UPDATE Students SET is_current = 1 WHERE id = :id")

View File

@ -4,8 +4,6 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.serializers.SafeMessageTypeEnumListSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
@ -36,8 +34,6 @@ data class AdminMessage(
val priority: String,
@SerialName("messageTypes")
@Serializable(with = SafeMessageTypeEnumListSerializer::class)
@ColumnInfo(name = "types", defaultValue = "[]")
val types: List<MessageType> = emptyList(),

View File

@ -33,13 +33,7 @@ data class GradeSummary(
@ColumnInfo(name = "points_sum")
val pointsSum: String,
@ColumnInfo(name = "points_sum_all_year")
val pointsSumAllYear: String?,
val average: Double,
@ColumnInfo(name = "average_all_year")
val averageAllYear: Double? = null,
val average: Double
) {
@PrimaryKey(autoGenerate = true)
var id: Long = 0

View File

@ -9,8 +9,8 @@ import java.time.Instant
@Entity(tableName = "MobileDevices")
data class MobileDevice(
@ColumnInfo(name = "user_login_id") // todo: change column name
val studentId: Int,
@ColumnInfo(name = "user_login_id")
val userLoginId: Int,
@ColumnInfo(name = "device_id")
val deviceId: Int,

View File

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

View File

@ -49,7 +49,6 @@ data class Student(
@ColumnInfo(name = "student_id")
val studentId: Int,
@Deprecated("not available in VULCAN anymore")
@ColumnInfo(name = "user_login_id")
val userLoginId: Int,
@ -79,13 +78,6 @@ data class Student(
@ColumnInfo(name = "registration_date")
val registrationDate: Instant,
@ColumnInfo(name = "is_authorized", defaultValue = "0")
val isAuthorized: Boolean,
@ColumnInfo(name = "is_edu_one", defaultValue = "NULL")
val isEduOne: Boolean?,
) : Serializable {
@PrimaryKey(autoGenerate = true)
@ -96,22 +88,3 @@ data class Student(
@ColumnInfo(name = "avatar_color")
var avatarColor = 0L
}
@Entity
data class StudentIsAuthorized(
@PrimaryKey
var id: Long,
@ColumnInfo(name = "is_authorized", defaultValue = "NULL")
val isAuthorized: Boolean?,
) : Serializable
@Entity
data class StudentIsEduOne(
@PrimaryKey
var id: Long,
@ColumnInfo(name = "is_edu_one", defaultValue = "NULL")
val isEduOne: Boolean?,
) : Serializable

View File

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

View File

@ -4,8 +4,6 @@ enum class MessageType {
GENERAL_MESSAGE,
DASHBOARD_MESSAGE,
LOGIN_MESSAGE,
LOGIN_STUDENT_SELECT_MESSAGE,
LOGIN_SYMBOL_MESSAGE,
PASS_RESET_MESSAGE,
ERROR_OVERRIDE,
}

View File

@ -1,11 +0,0 @@
package io.github.wulkanowy.data.enums
enum class ShowAdditionalLessonsMode(val value: String) {
NONE("none"),
INLINE("inline"),
BELOW("below");
companion object {
fun getByValue(value: String) = entries.find { it.value == value } ?: INLINE
}
}

View File

@ -8,7 +8,7 @@ import io.github.wulkanowy.sdk.pojo.LastAnnouncement as SdkLastAnnouncement
@JvmName("mapDirectorInformationToEntities")
fun List<SdkDirectorInformation>.mapToEntities(student: Student) = map {
SchoolAnnouncement(
studentId = student.studentId,
userLoginId = student.userLoginId,
date = it.date,
subject = it.subject,
content = it.content,
@ -19,7 +19,7 @@ fun List<SdkDirectorInformation>.mapToEntities(student: Student) = map {
@JvmName("mapLastAnnouncementsToEntities")
fun List<SdkLastAnnouncement>.mapToEntities(student: Student) = map {
SchoolAnnouncement(
studentId = student.studentId,
userLoginId = student.userLoginId,
date = it.date,
subject = it.subject,
content = it.content,

View File

@ -37,11 +37,9 @@ fun List<SdkGradeSummary>.mapToEntities(semester: Semester) = map {
predictedGrade = it.predicted,
finalGrade = it.final,
pointsSum = it.pointsSum,
pointsSumAllYear = it.pointsSumAllYear,
proposedPoints = it.proposedPoints,
finalPoints = it.finalPoints,
average = it.average,
averageAllYear = it.averageAllYear,
average = it.average
)
}

View File

@ -8,7 +8,7 @@ import io.github.wulkanowy.sdk.pojo.Token as SdkToken
fun List<SdkDevice>.mapToEntities(student: Student) = map {
MobileDevice(
studentId = student.studentId,
userLoginId = student.userLoginId,
date = it.createDate.toInstant(),
deviceId = it.id,
name = it.name

View File

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

View File

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

View File

@ -0,0 +1,34 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.api.AdminMessageService
import io.github.wulkanowy.data.db.dao.AdminMessageDao
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.networkBoundResource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AdminMessageRepository @Inject constructor(
private val adminMessageService: AdminMessageService,
private val adminMessageDao: AdminMessageDao,
) {
private val saveFetchResultMutex = Mutex()
fun getAdminMessages(): Flow<Resource<List<AdminMessage>>> =
networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = { false },
query = { adminMessageDao.loadAll() },
fetch = { adminMessageService.getAdminMessages() },
shouldFetch = { true },
saveFetchResult = { oldItems, newItems ->
adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
},
)
.filterNot { it is Resource.Intermediate }
}

View File

@ -6,8 +6,6 @@ import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntity
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.ui.modules.luckynumberwidget.LuckyNumberWidgetProvider
import io.github.wulkanowy.utils.AppWidgetUpdater
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
@ -20,7 +18,6 @@ import javax.inject.Singleton
class LuckyNumberRepository @Inject constructor(
private val luckyNumberDb: LuckyNumberDao,
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val appWidgetUpdater: AppWidgetUpdater,
) {
private val saveFetchResultMutex = Mutex()
@ -29,7 +26,6 @@ class LuckyNumberRepository @Inject constructor(
student: Student,
forceRefresh: Boolean,
notify: Boolean = false,
isFromAppWidget: Boolean = false
) = networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = { it == null },
@ -48,9 +44,6 @@ class LuckyNumberRepository @Inject constructor(
oldItems = listOfNotNull(oldLuckyNumber),
newItems = listOf(newLuckyNumber.apply { if (notify) isNotified = false }),
)
if (!isFromAppWidget) {
appWidgetUpdater.updateAllAppWidgetsByProvider(LuckyNumberWidgetProvider::class)
}
}
}
)

View File

@ -38,7 +38,7 @@ class MobileDeviceRepository @Inject constructor(
val isExpired = refreshHelper.shouldBeRefreshed(getRefreshKey(cacheKey, student))
it.isEmpty() || forceRefresh || isExpired
},
query = { mobileDb.loadAll(student.studentId) },
query = { mobileDb.loadAll(student.userLoginId) },
fetch = {
wulkanowySdkFactory.create(student, semester)
.getRegisteredDevices()

View File

@ -9,13 +9,11 @@ import com.fredporciuncula.flow.preferences.Preference
import com.fredporciuncula.flow.preferences.Serializer
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.data.api.models.Mapping
import io.github.wulkanowy.data.enums.AppTheme
import io.github.wulkanowy.data.enums.AttendanceCalculatorSortingMode
import io.github.wulkanowy.data.enums.GradeColorTheme
import io.github.wulkanowy.data.enums.GradeExpandMode
import io.github.wulkanowy.data.enums.GradeSortingMode
import io.github.wulkanowy.data.enums.ShowAdditionalLessonsMode
import io.github.wulkanowy.data.enums.TimetableGapsMode
import io.github.wulkanowy.data.enums.TimetableMode
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
@ -215,12 +213,6 @@ class PreferencesRepository @Inject constructor(
)
)
val showAdditionalLessonsInPlan: ShowAdditionalLessonsMode
get() = getString(
R.string.pref_key_timetable_show_additional_lessons,
R.string.pref_default_timetable_show_additional_lessons
).let { ShowAdditionalLessonsMode.getByValue(it) }
val gradeSortingMode: GradeSortingMode
get() = GradeSortingMode.getByValue(
getString(
@ -376,15 +368,6 @@ class PreferencesRepository @Inject constructor(
get() = sharedPref.getString(PREF_KEY_INSTALLATION_ID, null).orEmpty()
private set(value) = sharedPref.edit { putString(PREF_KEY_INSTALLATION_ID, value) }
var mapping: Mapping?
get() {
val value = sharedPref.getString("mapping", null)
return value?.let { json.decodeFromString(it) }
}
set(value) = sharedPref.edit(commit = true) {
putString("mapping", value?.let { json.encodeToString(it) })
}
init {
if (installationId.isEmpty()) {
installationId = UUID.randomUUID().toString()

View File

@ -37,7 +37,7 @@ class SchoolAnnouncementRepository @Inject constructor(
it.isEmpty() || forceRefresh || isExpired
},
query = {
schoolAnnouncementDb.loadAll(student.studentId)
schoolAnnouncementDb.loadAll(student.userLoginId)
},
fetch = {
val sdk = wulkanowySdkFactory.create(student)
@ -57,7 +57,7 @@ class SchoolAnnouncementRepository @Inject constructor(
)
fun getSchoolAnnouncementFromDatabase(student: Student): Flow<List<SchoolAnnouncement>> {
return schoolAnnouncementDb.loadAll(student.studentId)
return schoolAnnouncementDb.loadAll(student.userLoginId)
}
suspend fun updateSchoolAnnouncement(schoolAnnouncement: List<SchoolAnnouncement>) =

View File

@ -1,7 +1,7 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.data.api.services.SchoolsService
import io.github.wulkanowy.data.api.SchoolsService
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters

View File

@ -64,10 +64,7 @@ class SemesterRepository @Inject constructor(
.getSemesters()
.mapToEntities(student.studentId)
if (new.isEmpty()) {
Timber.i("Empty semester list from SDK!")
return
}
if (new.isEmpty()) return Timber.i("Empty semester list!")
val old = semesterDb.loadAll(student.studentId, student.classId)
semesterDb.removeOldAndSaveNew(

View File

@ -7,19 +7,16 @@ import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentIsAuthorized
import io.github.wulkanowy.data.db.entities.StudentName
import io.github.wulkanowy.data.db.entities.StudentNickAndAvatar
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.mappers.mapToPojo
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.DispatchersProvider
import io.github.wulkanowy.utils.security.Scrambler
import kotlinx.coroutines.withContext
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -42,7 +39,6 @@ class StudentRepository @Inject constructor(
): RegisterUser = wulkanowySdkFactory.create()
.getStudentsFromHebe(token, pin, symbol, "")
.mapToPojo(null)
.also { it.logErrors() }
suspend fun getUserSubjectsFromScrapper(
email: String,
@ -53,7 +49,6 @@ class StudentRepository @Inject constructor(
): RegisterUser = wulkanowySdkFactory.create()
.getUserSubjectsFromScrapper(email, password, scrapperBaseUrl, domainSuffix, symbol)
.mapToPojo(password)
.also { it.logErrors() }
suspend fun getStudentsHybrid(
email: String,
@ -63,7 +58,6 @@ class StudentRepository @Inject constructor(
): RegisterUser = wulkanowySdkFactory.create()
.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol)
.mapToPojo(password)
.also { it.logErrors() }
suspend fun getSavedStudents(decryptPass: Boolean = true): List<StudentWithSemesters> {
return studentDb.loadStudentsWithSemesters().map { (student, semesters) ->
@ -105,46 +99,6 @@ class StudentRepository @Inject constructor(
return student
}
suspend fun updateCurrentStudentAuthStatus() {
Timber.i("Check isAuthorized: started")
val student = getCurrentStudent()
if (student.isAuthorized) {
Timber.i("Check isAuthorized: already authorized")
return
}
val initializedSdk = wulkanowySdkFactory.create(student)
val newCurrentStudent = runCatching { initializedSdk.getCurrentStudent() }
.onFailure { Timber.e(it, "Check isAuthorized: error occurred") }
.getOrNull()
if (newCurrentStudent == null) {
Timber.d("Check isAuthorized: current user is null")
return
}
val currentStudentSemesters = semesterDb.loadAll(student.studentId, student.classId)
if (currentStudentSemesters.isEmpty()) {
Timber.d("Check isAuthorized: apply empty semesters workaround")
semesterDb.insertSemesters(
items = newCurrentStudent.semesters.mapToEntities(student.studentId),
)
}
if (!newCurrentStudent.isAuthorized) {
Timber.i("Check isAuthorized: authorization required")
throw NoAuthorizationException()
}
val studentIsAuthorized = StudentIsAuthorized(
id = student.id,
isAuthorized = true
)
Timber.i("Check isAuthorized: already authorized, update local status")
studentDb.update(studentIsAuthorized)
}
suspend fun getCurrentStudent(decryptPass: Boolean = true): Student {
val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException()
@ -197,21 +151,15 @@ class StudentRepository @Inject constructor(
wulkanowySdkFactory.create(student, semester)
.authorizePermission(pesel)
suspend fun refreshStudentAfterAuthorize(student: Student, semester: Semester) {
val wulkanowySdk = wulkanowySdkFactory.create(student, semester)
val newCurrentApiStudent = runCatching { wulkanowySdk.getCurrentStudent() }
.onFailure { Timber.e(it, "Can't find student with id ${student.studentId}") }
.getOrNull() ?: return
suspend fun refreshStudentName(student: Student, semester: Semester) {
val newCurrentApiStudent = wulkanowySdkFactory.create(student, semester)
.getCurrentStudent() ?: return
val studentName = StudentName(
studentName = "${newCurrentApiStudent.studentName} ${newCurrentApiStudent.studentSurname}"
).apply { id = student.id }
studentDb.update(studentName)
semesterDb.removeOldAndSaveNew(
oldItems = semesterDb.loadAll(student.studentId, semester.classId),
newItems = newCurrentApiStudent.semesters.mapToEntities(newCurrentApiStudent.studentId)
)
}
suspend fun deleteStudentsAssociatedWithAccount(student: Student) {
@ -224,18 +172,4 @@ class StudentRepository @Inject constructor(
appDatabase.clearAllTables()
}
}
private fun RegisterUser.logErrors() {
val symbolsErrors = symbols.filter { it.error != null }
.map { it.error }
val unitsErrors = symbols.flatMap { it.schools }
.filter { it.error != null }
.map { it.error }
(symbolsErrors + unitsErrors).forEach { error ->
Timber.e(error, "Error occurred while fetching students")
}
}
}
class NoAuthorizationException : Exception()

View File

@ -13,8 +13,6 @@ import io.github.wulkanowy.data.mappers.mapToEntities
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.data.pojos.TimetableFull
import io.github.wulkanowy.services.alarm.TimetableNotificationSchedulerHelper
import io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetProvider
import io.github.wulkanowy.utils.AppWidgetUpdater
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import io.github.wulkanowy.utils.monday
@ -28,7 +26,6 @@ import java.time.LocalDate
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class TimetableRepository @Inject constructor(
private val timetableDb: TimetableDao,
@ -37,7 +34,6 @@ class TimetableRepository @Inject constructor(
private val wulkanowySdkFactory: WulkanowySdkFactory,
private val schedulerHelper: TimetableNotificationSchedulerHelper,
private val refreshHelper: AutoRefreshHelper,
private val appWidgetUpdater: AppWidgetUpdater,
) {
private val saveFetchResultMutex = Mutex()
@ -56,8 +52,7 @@ class TimetableRepository @Inject constructor(
forceRefresh: Boolean,
refreshAdditional: Boolean = false,
notify: Boolean = false,
timetableType: TimetableType = TimetableType.NORMAL,
isFromAppWidget: Boolean = false
timetableType: TimetableType = TimetableType.NORMAL
) = networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = {
@ -88,9 +83,6 @@ class TimetableRepository @Inject constructor(
refreshDayHeaders(timetableOld.headers, timetableNew.headers)
refreshHelper.updateLastRefreshTimestamp(getRefreshKey(cacheKey, semester, start, end))
if (!isFromAppWidget) {
appWidgetUpdater.updateAllAppWidgetsByProvider(TimetableWidgetProvider::class)
}
},
filterResult = { (timetable, additional, headers) ->
TimetableFull(

View File

@ -1,69 +0,0 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.api.models.Mapping
import io.github.wulkanowy.data.api.services.WulkanowyService
import io.github.wulkanowy.data.db.dao.AdminMessageDao
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.utils.AutoRefreshHelper
import io.github.wulkanowy.utils.getRefreshKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.sync.Mutex
import timber.log.Timber
import java.time.LocalDate
import javax.inject.Inject
import javax.inject.Singleton
private val endDate = LocalDate.of(2024, 6, 25)
val isEndDateReached = LocalDate.now() >= endDate
@Singleton
class WulkanowyRepository @Inject constructor(
private val wulkanowyService: WulkanowyService,
private val adminMessageDao: AdminMessageDao,
private val preferencesRepository: PreferencesRepository,
private val refreshHelper: AutoRefreshHelper,
) {
private val saveFetchResultMutex = Mutex()
private val cacheKey = "mapping_refresh_key"
fun getAdminMessages(): Flow<Resource<List<AdminMessage>>> =
networkBoundResource(
mutex = saveFetchResultMutex,
isResultEmpty = { false },
query = { adminMessageDao.loadAll() },
fetch = { wulkanowyService.getAdminMessages() },
shouldFetch = { true },
saveFetchResult = { oldItems, newItems ->
adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
},
)
.filterNot { it is Resource.Intermediate }
suspend fun getMapping(): Mapping? {
var savedMapping = preferencesRepository.mapping
val isExpired = refreshHelper.shouldBeRefreshed(
key = getRefreshKey(cacheKey)
)
if (savedMapping == null || isExpired) {
fetchMapping()
savedMapping = preferencesRepository.mapping
}
return savedMapping
}
suspend fun fetchMapping() {
runCatching { wulkanowyService.getMapping() }
.onFailure { Timber.e(it) }
.onSuccess {
preferencesRepository.mapping = it
refreshHelper.updateLastRefreshTimestamp(cacheKey)
}
}
}

View File

@ -1,27 +0,0 @@
package io.github.wulkanowy.data.serializers
import io.github.wulkanowy.data.enums.MessageType
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
@OptIn(ExperimentalSerializationApi::class)
object SafeMessageTypeEnumListSerializer : KSerializer<List<MessageType>> {
private val serializer = ListSerializer(String.serializer())
override val descriptor = serializer.descriptor
override fun serialize(encoder: Encoder, value: List<MessageType>) {
encoder.encodeNotNullMark()
serializer.serialize(encoder, value.map { it.name })
}
override fun deserialize(decoder: Decoder): List<MessageType> =
serializer.deserialize(decoder).mapNotNull { enumName ->
MessageType.entries.find { it.name == enumName }
}
}

View File

@ -5,14 +5,14 @@ import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.mapResourceData
import io.github.wulkanowy.data.repositories.AdminMessageRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.WulkanowyRepository
import io.github.wulkanowy.utils.AppInfo
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class GetAppropriateAdminMessageUseCase @Inject constructor(
private val wulkanowyRepository: WulkanowyRepository,
private val adminMessageRepository: AdminMessageRepository,
private val preferencesRepository: PreferencesRepository,
private val appInfo: AppInfo
) {
@ -22,7 +22,7 @@ class GetAppropriateAdminMessageUseCase @Inject constructor(
}
operator fun invoke(scrapperBaseUrl: String, type: MessageType): Flow<Resource<AdminMessage?>> {
return wulkanowyRepository.getAdminMessages().mapResourceData { adminMessages ->
return adminMessageRepository.getAdminMessages().mapResourceData { adminMessages ->
adminMessages
.asSequence()
.filter { it.isNotDismissed() }

View File

@ -59,7 +59,7 @@ class GetMailboxByStudentUseCase @Inject constructor(
private fun String.getUnauthorizedVersion(): String {
return normalizeStudentName().split(" ")
.joinToString(" ") {
it.firstOrNull()?.toString().orEmpty() + "*".repeat((it.length - 1).coerceAtLeast(0))
it.first() + "*".repeat(it.length - 1)
}
}
}

View File

@ -4,27 +4,19 @@ import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES.O
import androidx.core.app.NotificationManagerCompat
import androidx.lifecycle.asFlow
import androidx.work.*
import androidx.work.BackoffPolicy.EXPONENTIAL
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingPeriodicWorkPolicy.KEEP
import androidx.work.ExistingPeriodicWorkPolicy.UPDATE
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType.CONNECTED
import androidx.work.NetworkType.UNMETERED
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.db.SharedPrefProvider.Companion.APP_VERSION_CODE_KEY
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.isEndDateReached
import io.github.wulkanowy.services.sync.channels.Channel
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.isHolidays
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import timber.log.Timber
import java.time.LocalDate.now
import java.util.concurrent.TimeUnit.MINUTES
@ -42,9 +34,7 @@ class SyncManager @Inject constructor(
) {
init {
if (now().isHolidays || isEndDateReached) {
stopSyncWorker()
}
if (now().isHolidays) stopSyncWorker()
if (SDK_INT >= O) {
channels.forEach { it.create() }
@ -60,7 +50,7 @@ class SyncManager @Inject constructor(
}
fun startPeriodicSyncWorker(restart: Boolean = false) {
if (preferencesRepository.isServiceEnabled && !now().isHolidays && isEndDateReached) {
if (preferencesRepository.isServiceEnabled && !now().isHolidays) {
val serviceInterval = preferencesRepository.servicesInterval
workManager.enqueueUniquePeriodicWork(
@ -80,10 +70,6 @@ class SyncManager @Inject constructor(
// if quiet, no notifications will be sent
fun startOneTimeSyncWorker(quiet: Boolean = false): Flow<WorkInfo?> {
if (isEndDateReached) {
return flowOf(null)
}
val work = OneTimeWorkRequestBuilder<SyncWorker>()
.setInputData(
Data.Builder()

View File

@ -15,10 +15,8 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.isEndDateReached
import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
import io.github.wulkanowy.sdk.scrapper.exception.FeatureUnavailableException
import io.github.wulkanowy.services.sync.channels.DebugChannel
import io.github.wulkanowy.services.sync.works.Work
import io.github.wulkanowy.utils.DispatchersProvider
@ -43,16 +41,13 @@ class SyncWorker @AssistedInject constructor(
override suspend fun doWork(): Result = withContext(dispatchersProvider.io) {
Timber.i("SyncWorker is starting")
if (!studentRepository.isCurrentStudentSet() || isEndDateReached) {
return@withContext Result.failure()
}
if (!studentRepository.isCurrentStudentSet()) return@withContext Result.failure()
val (student, semester) = try {
val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student, true)
student to semester
} catch (e: Throwable) {
Timber.e(e)
return@withContext getResultFromErrors(listOf(e))
}
@ -64,7 +59,7 @@ class SyncWorker @AssistedInject constructor(
null
} catch (e: Throwable) {
Timber.w("${work::class.java.simpleName} result: An exception ${e.message} occurred")
if (e is FeatureDisabledException || e is FeatureNotAvailableException || e is FeatureUnavailableException) {
if (e is FeatureDisabledException || e is FeatureNotAvailableException) {
null
} else {
Timber.e(e)
@ -94,7 +89,6 @@ class SyncWorker @AssistedInject constructor(
.build()
)
}
errors.isNotEmpty() -> Result.retry()
else -> {
preferencesRepository.lasSyncDate = Instant.now()

View File

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

View File

@ -1,9 +1,6 @@
package io.github.wulkanowy.ui.modules.attendance.calculator
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
@ -12,9 +9,7 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.pojos.AttendanceData
import io.github.wulkanowy.databinding.FragmentAttendanceCalculatorBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.settings.appearance.AppearanceFragment
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject
@ -38,12 +33,6 @@ class AttendanceCalculatorFragment :
override val isViewEmpty get() = attendanceCalculatorAdapter.items.isEmpty()
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentAttendanceCalculatorBinding.bind(view)
@ -51,19 +40,6 @@ class AttendanceCalculatorFragment :
presenter.onAttachView(this)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.action_menu_attendance_calculator, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.attendance_calculator_menu_settings) presenter.onSettingsSelected()
else false
}
override fun openSettingsView() {
(activity as? MainActivity)?.pushView(AppearanceFragment.withFocusedPreference(getString(R.string.pref_key_attendance_target)))
}
override fun initView() {
with(binding.attendanceCalculatorRecycler) {
layoutManager = LinearLayoutManager(context)
@ -74,11 +50,7 @@ class AttendanceCalculatorFragment :
with(binding) {
attendanceCalculatorSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
attendanceCalculatorSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
attendanceCalculatorSwipe.setProgressBackgroundColorSchemeColor(
requireContext().getThemeAttrColor(
R.attr.colorSwipeRefresh
)
)
attendanceCalculatorSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
attendanceCalculatorErrorRetry.setOnClickListener { presenter.onRetry() }
attendanceCalculatorErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}

View File

@ -1,11 +1,6 @@
package io.github.wulkanowy.ui.modules.attendance.calculator
import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.onResourceData
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceIntermediate
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.domain.attendance.GetAttendanceCalculatorDataUseCase
@ -86,9 +81,4 @@ class AttendanceCalculatorPresenter @Inject constructor(
} else showError(message, error)
}
}
fun onSettingsSelected(): Boolean {
view?.openSettingsView()
return true
}
}

View File

@ -26,6 +26,4 @@ interface AttendanceCalculatorView : BaseView {
fun updateData(data: List<AttendanceData>)
fun clearView()
fun openSettingsView()
}

View File

@ -5,7 +5,6 @@ import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
class AuthPresenter @Inject constructor(
@ -27,12 +26,8 @@ class AuthPresenter @Inject constructor(
private fun loadName() {
presenterScope.launch {
runCatching {
studentRepository.getCurrentStudent(false)
.studentName
.replace(" ", "\u00A0")
}
.onSuccess { view?.showDescriptionWithName(it) }
runCatching { studentRepository.getCurrentStudent(false) }
.onSuccess { view?.showDescriptionWithName(it.studentName) }
.onFailure { errorHandler.dispatch(it) }
}
}
@ -62,9 +57,8 @@ class AuthPresenter @Inject constructor(
val semester = semesterRepository.getCurrentSemester(student)
val isSuccess = studentRepository.authorizePermission(student, semester, pesel)
Timber.d("Auth succeed: $isSuccess")
if (isSuccess) {
studentRepository.refreshStudentAfterAuthorize(student, semester)
studentRepository.refreshStudentName(student, semester)
}
isSuccess
}
@ -74,7 +68,6 @@ class AuthPresenter @Inject constructor(
view?.showContent(true)
}
.onSuccess {
Timber.d("Auth fully succeed: $it")
if (it) {
view?.showSuccess(true)
view?.showContent(false)

View File

@ -59,7 +59,7 @@ class CaptchaDialog : BaseDialogFragment<DialogCaptchaBinding>() {
webView = this
with(settings) {
javaScriptEnabled = true
userAgentString = wulkanowySdkFactory.createBase().userAgent
userAgentString = wulkanowySdkFactory.create().userAgent
}
webViewClient = object : WebViewClient() {

View File

@ -30,7 +30,6 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.notificationscenter.NotificationsCenterFragment
import io.github.wulkanowy.ui.modules.panicmode.PanicModeFragment
import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import io.github.wulkanowy.utils.capitalise
@ -126,7 +125,6 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
mainActivity.pushView(ConferenceFragment.newInstance())
}
onAdminMessageClickListener = presenter::onAdminMessageSelected
onPanicButtonClickListener = presenter::onPanicButtonClicked
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed
registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
@ -210,11 +208,7 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
binding = binding.dashboardErrorAdminMessage,
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
onAdminMessageClickListener = presenter::onAdminMessageSelected,
onPanicButtonClickListener = presenter::onPanicButtonClicked,
).bind(
item = adminMessageItem.adminMessage,
showPanicButton = true,
)
).bind(adminMessageItem.adminMessage)
}
}
@ -242,10 +236,6 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
requireContext().openInternetBrowser(url)
}
override fun openPanicWebView(url: String) {
(requireActivity() as MainActivity).pushView(PanicModeFragment.newInstance(url))
}
override fun onDestroyView() {
dashboardAdapter.clearTimers()
presenter.onDetachView()

View File

@ -11,7 +11,6 @@ import io.github.wulkanowy.data.errorOrNull
import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.mapResourceData
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository
import io.github.wulkanowy.data.repositories.ConferenceRepository
import io.github.wulkanowy.data.repositories.ExamRepository
@ -24,7 +23,6 @@ import io.github.wulkanowy.data.repositories.SchoolAnnouncementRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
import io.github.wulkanowy.domain.timetable.IsStudentHasLessonsOnWeekendUseCase
import io.github.wulkanowy.ui.base.BasePresenter
@ -46,7 +44,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import okhttp3.HttpUrl.Companion.toHttpUrl
import timber.log.Timber
import java.time.Instant
import java.time.LocalDate
@ -285,22 +282,6 @@ class DashboardPresenter @Inject constructor(
url?.let { view?.openInternetBrowser(it) }
}
fun onPanicButtonClicked() {
resourceFlow { studentRepository.getCurrentStudent() }
.onResourceError { errorHandler.dispatch(it) }
.onResourceSuccess {
val baseUrl = it.scrapperBaseUrl.toHttpUrl()
val urlToOpen = baseUrl.newBuilder()
.host("uonetplus${it.scrapperDomainSuffix}.${baseUrl.host}")
.addPathSegment(it.symbol)
.build()
.toString()
view?.openPanicWebView(urlToOpen)
}
.launch("panic_button")
}
private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) {
flow {
val selectedTiles = selectedDashboardTiles

View File

@ -31,6 +31,4 @@ interface DashboardView : BaseView {
fun openNotificationsCenterView()
fun openInternetBrowser(url: String)
fun openPanicWebView(url: String)
}

View File

@ -59,8 +59,6 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
var onAdminMessageClickListener: (String?) -> Unit = {}
var onPanicButtonClickListener: () -> Unit = {}
var onAdminMessageDismissClickListener: (AdminMessage) -> Unit = {}
val items = mutableListOf<DashboardItem>()
@ -88,46 +86,35 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
DashboardItem.Type.ACCOUNT.ordinal -> AccountViewHolder(
ItemDashboardAccountBinding.inflate(inflater, parent, false)
)
DashboardItem.Type.HORIZONTAL_GROUP.ordinal -> HorizontalGroupViewHolder(
ItemDashboardHorizontalGroupBinding.inflate(inflater, parent, false)
)
DashboardItem.Type.GRADES.ordinal -> GradesViewHolder(
ItemDashboardGradesBinding.inflate(inflater, parent, false)
)
DashboardItem.Type.LESSONS.ordinal -> LessonsViewHolder(
ItemDashboardLessonsBinding.inflate(inflater, parent, false)
)
DashboardItem.Type.HOMEWORK.ordinal -> HomeworkViewHolder(
ItemDashboardHomeworkBinding.inflate(inflater, parent, false)
)
DashboardItem.Type.ANNOUNCEMENTS.ordinal -> AnnouncementsViewHolder(
ItemDashboardAnnouncementsBinding.inflate(inflater, parent, false)
)
DashboardItem.Type.EXAMS.ordinal -> ExamsViewHolder(
ItemDashboardExamsBinding.inflate(inflater, parent, false)
)
DashboardItem.Type.CONFERENCES.ordinal -> ConferencesViewHolder(
ItemDashboardConferencesBinding.inflate(inflater, parent, false)
)
DashboardItem.Type.ADMIN_MESSAGE.ordinal -> AdminMessageViewHolder(
ItemDashboardAdminMessageBinding.inflate(inflater, parent, false),
onAdminMessageDismissClickListener = onAdminMessageDismissClickListener,
onAdminMessageClickListener = onAdminMessageClickListener,
onPanicButtonClickListener = onPanicButtonClickListener,
)
DashboardItem.Type.ADS.ordinal -> AdsViewHolder(
ItemDashboardAdsBinding.inflate(inflater, parent, false)
)
else -> throw IllegalArgumentException()
}
}
@ -142,11 +129,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 -> holder.bind(
(items[position] as DashboardItem.AdminMessages).adminMessage,
showPanicButton = true
)
is AdminMessageViewHolder -> holder.bind((items[position] as DashboardItem.AdminMessages).adminMessage)
is AdsViewHolder -> bindAdsViewHolder(holder, position)
}
}
@ -257,15 +240,12 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
attendancePercentage == null || attendancePercentage == .0 -> {
root.context.getThemeAttrColor(R.attr.colorOnSurface)
}
attendancePercentage <= ATTENDANCE_SECOND_WARNING_THRESHOLD -> {
root.context.getThemeAttrColor(R.attr.colorPrimary)
}
attendancePercentage <= ATTENDANCE_FIRST_WARNING_THRESHOLD -> {
root.context.getThemeAttrColor(R.attr.colorTimetableChange)
}
else -> root.context.getThemeAttrColor(R.attr.colorOnSurface)
}
val attendanceString = if (attendancePercentage == null || attendancePercentage == .0) {
@ -356,28 +336,24 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
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 = currentDate
updateLessonView(item, emptyList(), binding)
@ -485,7 +461,6 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
firstTitleText =
context.getString(R.string.dashboard_timetable_first_lesson_title_moment)
}
minutesToStartLesson < 240 -> {
firstTitleAndValueTextColor =
context.getThemeAttrColor(R.attr.colorOnSurface)
@ -493,7 +468,6 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
firstTitleText =
context.getString(R.string.dashboard_timetable_first_lesson_title_soon)
}
else -> {
firstTitleAndValueTextColor =
context.getThemeAttrColor(R.attr.colorOnSurface)

View File

@ -13,10 +13,9 @@ class AdminMessageViewHolder(
private val binding: ItemDashboardAdminMessageBinding,
private val onAdminMessageDismissClickListener: (AdminMessage) -> Unit,
private val onAdminMessageClickListener: (String?) -> Unit,
private val onPanicButtonClickListener: () -> Unit,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: AdminMessage?, showPanicButton: Boolean = false) {
fun bind(item: AdminMessage?) {
item ?: return
val context = binding.root.context
@ -49,14 +48,10 @@ class AdminMessageViewHolder(
dashboardAdminMessageItemClose.setOnClickListener {
onAdminMessageDismissClickListener(item)
}
dashboardPanicSection.root.isVisible = showPanicButton
dashboardPanicSection.dashboardPanicButton.setOnClickListener {
onPanicButtonClickListener()
}
dashboardAdminMessage.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) })
root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) })
item.destinationUrl?.let { url ->
dashboardAdminMessage.setOnClickListener { onAdminMessageClickListener(url) }
root.setOnClickListener { onAdminMessageClickListener(url) }
}
}
}

View File

@ -26,7 +26,5 @@ private fun generateSummary(subject: String, predicted: String, final: String) =
proposedPoints = "",
finalPoints = "",
pointsSum = "",
average = .0,
pointsSumAllYear = null,
averageAllYear = null,
average = .0
)

View File

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

View File

@ -1,31 +0,0 @@
package io.github.wulkanowy.ui.modules.end
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.View
import androidx.activity.addCallback
import androidx.core.text.HtmlCompat
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.FragmentEndBinding
import io.github.wulkanowy.ui.base.BaseFragment
@AndroidEntryPoint
class EndFragment : BaseFragment<FragmentEndBinding>(R.layout.fragment_end), EndView {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentEndBinding.bind(view)
requireActivity().onBackPressedDispatcher.addCallback {
requireActivity().finishAffinity()
}
binding.endClose.setOnClickListener { requireActivity().finishAffinity() }
val message = getString(R.string.end_message)
binding.endDescription.movementMethod = LinkMovementMethod.getInstance()
binding.endDescription.text =
HtmlCompat.fromHtml(message, HtmlCompat.FROM_HTML_MODE_COMPACT)
}
}

View File

@ -1,5 +0,0 @@
package io.github.wulkanowy.ui.modules.end
import io.github.wulkanowy.ui.base.BaseView
interface EndView : BaseView

View File

@ -266,9 +266,7 @@ class GradeAverageProvider @Inject constructor(
proposedPoints = "",
finalPoints = "",
pointsSum = "",
pointsSumAllYear = null,
average = .0,
averageAllYear = null,
average = .0
)
}
@ -296,15 +294,13 @@ class GradeAverageProvider @Inject constructor(
proposedPoints = "",
finalPoints = "",
pointsSum = "",
pointsSumAllYear = null,
average = when {
calcAverage -> details
.updateModifiers(student, params)
.calcAverage(isOptionalArithmeticAverage = params.isOptionalArithmeticAverage)
else -> .0
},
averageAllYear = null,
}
)
}
}

View File

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

View File

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

View File

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

View File

@ -96,11 +96,9 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
ViewType.HEADER.id -> HeaderViewHolder(
HeaderGradeDetailsBinding.inflate(inflater, parent, false)
)
ViewType.ITEM.id -> ItemViewHolder(
ItemGradeDetailsBinding.inflate(inflater, parent, false)
)
else -> throw IllegalStateException()
}
}
@ -112,7 +110,6 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
header = items[position].value as GradeDetailsHeader,
position = position
)
is ItemViewHolder -> bindItemViewHolder(
holder = holder,
grade = items[position].value as Grade
@ -136,10 +133,6 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
maxLines = if (expandedPositions[headerPosition]) 2 else 1
}
gradeHeaderAverage.text = formatAverage(header.average, root.context.resources)
with(gradeHeaderAverageAllYear) {
isVisible = header.averageAllYear != null && header.averageAllYear != .0
text = formatAverageAllYear(header.averageAllYear, root.context.resources)
}
gradeHeaderPointsSum.text =
context.getString(R.string.grade_points_sum, header.pointsSum)
gradeHeaderPointsSum.isVisible = !header.pointsSum.isNullOrEmpty()
@ -240,13 +233,6 @@ class GradeDetailsAdapter @Inject constructor() : BaseExpandableAdapter<Recycler
resources.getString(R.string.grade_average, average)
}
private fun formatAverageAllYear(average: Double?, resources: Resources) =
if (average == null || average == .0) {
resources.getString(R.string.grade_no_average)
} else {
resources.getString(R.string.grade_average_year, average)
}
private class HeaderViewHolder(val binding: HeaderGradeDetailsBinding) :
RecyclerView.ViewHolder(binding.root)

View File

@ -13,7 +13,6 @@ data class GradeDetailsItem(
data class GradeDetailsHeader(
val subject: String,
val average: Double?,
val averageAllYear: Double?,
val pointsSum: String?,
val grades: List<GradeDetailsItem>
) {

View File

@ -226,9 +226,8 @@ class GradeDetailsPresenter @Inject constructor(
GradeDetailsHeader(
subject = gradeSubject.subject,
average = gradeSubject.average,
averageAllYear = gradeSubject.summary.averageAllYear,
pointsSum = gradeSubject.points,
grades = subItems,
grades = subItems
).apply {
newGrades = gradeSubject.grades.filter { grade -> !grade.isRead }.size
}, ViewType.HEADER

View File

@ -3,7 +3,6 @@ package io.github.wulkanowy.ui.modules.grade.summary
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
@ -66,55 +65,37 @@ class GradeSummaryAdapter @Inject constructor(
val gradeSummaries = items
.filter { it.gradeDescriptive == null }
.map { it.gradeSummary }
val isSecondSemester = items.any { item ->
item.gradeSummary.let { it.averageAllYear != null && it.averageAllYear != .0 }
}
val context = binding.root.context
val finalItemsCount = gradeSummaries.count { isGradeValid(it.finalGrade) }
val calculatedSemesterItemsCount = gradeSummaries.count { value -> value.average != 0.0 }
val calculatedAnnualItemsCount =
gradeSummaries.count { value -> value.averageAllYear != 0.0 }
val calculatedItemsCount = gradeSummaries.count { value -> value.average != 0.0 }
val allItemsCount = gradeSummaries.count { !it.subject.equals("zachowanie", true) }
val finalAverage = gradeSummaries.calcFinalAverage(
plusModifier = preferencesRepository.gradePlusModifier,
minusModifier = preferencesRepository.gradeMinusModifier,
preferencesRepository.gradePlusModifier,
preferencesRepository.gradeMinusModifier
)
val calculatedSemesterAverage = gradeSummaries.filter { value -> value.average != 0.0 }
val calculatedAverage = gradeSummaries.filter { value -> value.average != 0.0 }
.map { values -> values.average }
.reversed() // fix average precision
.average()
.let { if (it.isNaN()) 0.0 else it }
val calculatedAnnualAverage = gradeSummaries.filter { value -> value.averageAllYear != 0.0 }
.mapNotNull { values -> values.averageAllYear }
.reversed() // fix average precision
.average()
.let { if (it.isNaN()) 0.0 else it }
with(binding) {
gradeSummaryScrollableHeaderCalculated.text = formatAverage(calculatedSemesterAverage)
gradeSummaryScrollableHeaderCalculatedAnnual.text =
formatAverage(calculatedAnnualAverage)
gradeSummaryScrollableHeaderFinal.text = formatAverage(finalAverage)
gradeSummaryScrollableHeaderFinalSubjectCount.text = context.getString(
R.string.grade_summary_from_subjects,
finalItemsCount,
allItemsCount
)
gradeSummaryScrollableHeaderCalculated.text = formatAverage(calculatedAverage)
gradeSummaryScrollableHeaderFinalSubjectCount.text =
context.getString(
R.string.grade_summary_from_subjects,
finalItemsCount,
allItemsCount
)
gradeSummaryScrollableHeaderCalculatedSubjectCount.text = context.getString(
R.string.grade_summary_from_subjects,
calculatedSemesterItemsCount,
calculatedItemsCount,
allItemsCount
)
gradeSummaryScrollableHeaderCalculatedSubjectCountAnnual.text = context.getString(
R.string.grade_summary_from_subjects,
calculatedAnnualItemsCount,
allItemsCount
)
gradeSummaryScrollableHeaderCalculatedAnnualContainer.isVisible = isSecondSemester
gradeSummaryCalculatedAverageHelp.setOnClickListener { onCalculatedHelpClickListener() }
gradeSummaryCalculatedAverageHelpAnnual.setOnClickListener { onCalculatedHelpClickListener() }
gradeSummaryFinalAverageHelp.setOnClickListener { onFinalHelpClickListener() }
}
}
@ -126,12 +107,7 @@ class GradeSummaryAdapter @Inject constructor(
with(binding) {
gradeSummaryItemTitle.text = gradeSummary.subject
gradeSummaryItemPoints.text = gradeSummary.pointsSum
gradeSummaryItemAverage.text = formatAverage(gradeSummary.average, "")
gradeSummaryItemAverageAllYear.text = gradeSummary.averageAllYear?.let {
formatAverage(it, "")
}
gradeSummaryItemPredicted.text =
"${gradeSummary.predictedGrade} ${gradeSummary.proposedPoints}".trim()
gradeSummaryItemFinal.text =
@ -140,12 +116,6 @@ class GradeSummaryAdapter @Inject constructor(
root.context.getString(R.string.all_no_data)
}
gradeSummaryItemAverageContainer.isVisible = gradeSummary.average != .0
gradeSummaryItemAverageDivider.isVisible = gradeSummaryItemAverageContainer.isVisible
gradeSummaryItemAverageAllYearContainer.isGone =
gradeSummary.averageAllYear == null || gradeSummary.averageAllYear == .0
gradeSummaryItemAverageAllYearDivider.isGone =
gradeSummaryItemAverageAllYearContainer.isGone
gradeSummaryItemFinalDivider.isVisible = gradeDescriptive == null
gradeSummaryItemPredictedDivider.isVisible = gradeDescriptive == null
gradeSummaryItemPointsDivider.isVisible = gradeDescriptive == null
@ -153,7 +123,6 @@ class GradeSummaryAdapter @Inject constructor(
gradeSummaryItemFinalContainer.isVisible = gradeDescriptive == null
gradeSummaryItemDescriptiveContainer.isVisible = gradeDescriptive != null
gradeSummaryItemPointsContainer.isVisible = gradeSummary.pointsSum.isNotBlank()
gradeSummaryItemPointsDivider.isVisible = gradeSummaryItemPointsContainer.isVisible
}
}

View File

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

View File

@ -15,7 +15,6 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.databinding.ActivityLoginBinding
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.end.EndFragment
import io.github.wulkanowy.ui.modules.login.advanced.LoginAdvancedFragment
import io.github.wulkanowy.ui.modules.login.form.LoginFormFragment
import io.github.wulkanowy.ui.modules.login.recover.LoginRecoverFragment
@ -116,14 +115,8 @@ class LoginActivity : BaseActivity<LoginPresenter, ActivityLoginBinding>(), Logi
}
}
override fun navigateToEnd() {
openFragment(EndFragment(), clearBackStack = true)
}
override fun onResume() {
super.onResume()
inAppUpdateHelper.onResume()
presenter.updateSdkMappings()
presenter.checkIfEnd()
}
}

View File

@ -1,20 +1,14 @@
package io.github.wulkanowy.ui.modules.login
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.WulkanowyRepository
import io.github.wulkanowy.data.repositories.isEndDateReached
import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
class LoginPresenter @Inject constructor(
private val wulkanowyRepository: WulkanowyRepository,
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val syncManager: SyncManager
studentRepository: StudentRepository
) : BasePresenter<LoginView>(errorHandler, studentRepository) {
override fun onAttachView(view: LoginView) {
@ -22,18 +16,4 @@ class LoginPresenter @Inject constructor(
view.initView()
Timber.i("Login view was initialized")
}
fun updateSdkMappings() {
presenterScope.launch {
runCatching { wulkanowyRepository.fetchMapping() }
.onFailure { Timber.e(it) }
}
}
fun checkIfEnd() {
if (isEndDateReached) {
syncManager.stopSyncWorker()
view?.navigateToEnd()
}
}
}

View File

@ -5,6 +5,4 @@ import io.github.wulkanowy.ui.base.BaseView
interface LoginView : BaseView {
fun initView()
fun navigateToEnd()
}

View File

@ -238,7 +238,6 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
binding = binding.loginFormMessage,
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
onAdminMessageClickListener = presenter::onAdminMessageSelected,
onPanicButtonClickListener = {},
).bind(message)
binding.loginFormMessage.root.isVisible = message != null
}

View File

@ -19,23 +19,19 @@ class LoginStudentSelectAdapter @Inject constructor() :
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (LoginStudentSelectItemType.entries[viewType]) {
return when (LoginStudentSelectItemType.values()[viewType]) {
LoginStudentSelectItemType.EMPTY_SYMBOLS_HEADER -> EmptySymbolsHeaderViewHolder(
ItemLoginStudentSelectEmptySymbolHeaderBinding.inflate(inflater, parent, false),
)
LoginStudentSelectItemType.SYMBOL_HEADER -> SymbolsHeaderViewHolder(
ItemLoginStudentSelectHeaderSymbolBinding.inflate(inflater, parent, false)
)
LoginStudentSelectItemType.SCHOOL_HEADER -> SchoolHeaderViewHolder(
ItemLoginStudentSelectHeaderSchoolBinding.inflate(inflater, parent, false)
)
LoginStudentSelectItemType.STUDENT -> StudentViewHolder(
ItemLoginStudentSelectStudentBinding.inflate(inflater, parent, false)
)
LoginStudentSelectItemType.HELP -> HelpViewHolder(
ItemLoginStudentSelectHelpBinding.inflate(inflater, parent, false)
)
@ -102,11 +98,9 @@ class LoginStudentSelectAdapter @Inject constructor() :
with(binding) {
loginStudentSelectHeaderSchoolName.text = buildString {
append(item.unit.schoolName.trim())
if (item.unit.schoolShortName.isNotBlank()) {
append(" (")
append(item.unit.schoolShortName)
append(")")
}
append(" (")
append(item.unit.schoolShortName)
append(")")
}
loginStudentSelectHeaderSchoolDetails.isVisible = item.unit.students.isEmpty()
loginStudentSelectHeaderSchoolError.text = item.unit.error?.message
@ -176,11 +170,9 @@ class LoginStudentSelectAdapter @Inject constructor() :
oldItem is LoginStudentSelectItem.SymbolHeader && newItem is LoginStudentSelectItem.SymbolHeader -> {
oldItem.symbol == newItem.symbol
}
oldItem is LoginStudentSelectItem.Student && newItem is LoginStudentSelectItem.Student -> {
oldItem.student == newItem.student
}
else -> oldItem == newItem
}

View File

@ -6,12 +6,10 @@ import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginStudentSelectBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog
@ -113,20 +111,6 @@ class LoginStudentSelectFragment :
LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog")
}
override fun showAdminMessage(adminMessage: AdminMessage?) {
AdminMessageViewHolder(
binding = binding.loginStudentSelectAdminMessage,
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
onAdminMessageClickListener = presenter::onAdminMessageSelected,
onPanicButtonClickListener = {},
).bind(adminMessage)
binding.loginStudentSelectAdminMessage.root.isVisible = adminMessage != null
}
override fun openInternetBrowser(url: String) {
requireContext().openInternetBrowser(url)
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()

View File

@ -2,24 +2,16 @@ package io.github.wulkanowy.ui.modules.login.studentselect
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.mappers.mapToStudentWithSemesters
import io.github.wulkanowy.data.onResourceData
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.pojos.RegisterStudent
import io.github.wulkanowy.data.pojos.RegisterSymbol
import io.github.wulkanowy.data.pojos.RegisterUnit
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SchoolsRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
import io.github.wulkanowy.sdk.scrapper.exception.StudentGraduateException
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter
@ -40,8 +32,6 @@ class LoginStudentSelectPresenter @Inject constructor(
private val syncManager: SyncManager,
private val analytics: AnalyticsHelper,
private val appInfo: AppInfo,
private val preferencesRepository: PreferencesRepository,
private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase
) : BasePresenter<LoginStudentSelectView>(loginErrorHandler, studentRepository) {
private var lastError: Throwable? = null
@ -74,7 +64,6 @@ class LoginStudentSelectPresenter @Inject constructor(
this.loginData = loginData
this.registerUser = registerUser
loadData()
loadAdminMessage()
}
private fun loadData() {
@ -98,20 +87,7 @@ class LoginStudentSelectPresenter @Inject constructor(
refreshItems()
}
}
}.launch("load_data")
}
private fun loadAdminMessage() {
flatResourceFlow {
getAppropriateAdminMessageUseCase(
scrapperBaseUrl = registerUser.scrapperBaseUrl.orEmpty(),
type = MessageType.LOGIN_STUDENT_SELECT_MESSAGE,
)
}
.logResourceStatus("load login admin message")
.onResourceData { view?.showAdminMessage(it) }
.onResourceError { view?.showAdminMessage(null) }
.launch("load_admin_message")
}.launch()
}
private fun getStudentsWithCurrentlyActiveSemesters(): List<LoginStudentSelectItem.Student> {
@ -132,8 +108,8 @@ class LoginStudentSelectPresenter @Inject constructor(
}
private fun createItems(): List<LoginStudentSelectItem> = buildList {
val notEmptySymbols = registerUser.symbols.filter { it.shouldShowOnTop() }
val emptySymbols = registerUser.symbols.filter { !it.shouldShowOnTop() }
val notEmptySymbols = registerUser.symbols.filter { it.schools.isNotEmpty() }
val emptySymbols = registerUser.symbols.filter { it.schools.isEmpty() }
if (emptySymbols.isNotEmpty() && notEmptySymbols.isNotEmpty() && emptySymbols.any { it.symbol == loginData.userEnteredSymbol }) {
add(createEmptySymbolItem(emptySymbols.first { it.symbol == loginData.userEnteredSymbol }))
@ -151,10 +127,6 @@ class LoginStudentSelectPresenter @Inject constructor(
add(helpItem)
}
private fun RegisterSymbol.shouldShowOnTop(): Boolean {
return schools.isNotEmpty() || error is StudentGraduateException
}
private fun createNotEmptySymbolItems(
notEmptySymbols: List<RegisterSymbol>,
students: List<StudentWithSemesters>,
@ -364,14 +336,4 @@ class LoginStudentSelectPresenter @Inject constructor(
)
}
}
fun onAdminMessageSelected(url: String?) {
url?.let { view?.openInternetBrowser(it) }
}
fun onAdminMessageDismissed(adminMessage: AdminMessage) {
preferencesRepository.dismissedAdminMessageIds += adminMessage.id
view?.showAdminMessage(null)
}
}

View File

@ -1,6 +1,5 @@
package io.github.wulkanowy.ui.modules.login.studentselect
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.support.LoginSupportInfo
@ -26,8 +25,4 @@ interface LoginStudentSelectView : BaseView {
fun openDiscordInvite()
fun openEmail(supportInfo: LoginSupportInfo)
fun showAdminMessage(adminMessage: AdminMessage?)
fun openInternetBrowser(url: String)
}

View File

@ -9,16 +9,13 @@ import android.view.inputmethod.EditorInfo.IME_NULL
import android.widget.ArrayAdapter
import androidx.core.os.bundleOf
import androidx.core.text.parseAsHtml
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginSymbolBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.support.LoginSupportDialog
@ -182,18 +179,4 @@ class LoginSymbolFragment :
override fun openSupportDialog(supportInfo: LoginSupportInfo) {
LoginSupportDialog.newInstance(supportInfo).show(childFragmentManager, "support_dialog")
}
override fun showAdminMessage(adminMessage: AdminMessage?) {
AdminMessageViewHolder(
binding = binding.loginSymbolAdminMessage,
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
onAdminMessageClickListener = presenter::onAdminMessageSelected,
onPanicButtonClickListener = {},
).bind(adminMessage)
binding.loginSymbolAdminMessage.root.isVisible = adminMessage != null
}
override fun openInternetBrowser(url: String) {
requireContext().openInternetBrowser(url)
}
}

View File

@ -2,18 +2,10 @@ package io.github.wulkanowy.ui.modules.login.symbol
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.dataOrNull
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.onResourceData
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
import io.github.wulkanowy.sdk.scrapper.getNormalizedSymbol
import io.github.wulkanowy.sdk.scrapper.login.InvalidSymbolException
import io.github.wulkanowy.ui.base.BasePresenter
@ -29,9 +21,7 @@ import javax.inject.Inject
class LoginSymbolPresenter @Inject constructor(
studentRepository: StudentRepository,
private val loginErrorHandler: LoginErrorHandler,
private val analytics: AnalyticsHelper,
private val preferencesRepository: PreferencesRepository,
private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase,
private val analytics: AnalyticsHelper
) : BasePresenter<LoginSymbolView>(loginErrorHandler, studentRepository) {
private var lastError: Throwable? = null
@ -53,21 +43,6 @@ class LoginSymbolPresenter @Inject constructor(
clearAndFocusSymbol()
showSoftKeyboard()
}
loadAdminMessage()
}
private fun loadAdminMessage() {
flatResourceFlow {
getAppropriateAdminMessageUseCase(
scrapperBaseUrl = loginData.baseUrl,
type = MessageType.LOGIN_SYMBOL_MESSAGE,
)
}
.logResourceStatus("load login admin message")
.onResourceData { view?.showAdminMessage(it) }
.onResourceError { view?.showAdminMessage(null) }
.launch("load_admin_message")
}
fun onSymbolTextChanged() {
@ -191,14 +166,4 @@ class LoginSymbolPresenter @Inject constructor(
)
)
}
fun onAdminMessageSelected(url: String?) {
url?.let { view?.openInternetBrowser(it) }
}
fun onAdminMessageDismissed(adminMessage: AdminMessage) {
preferencesRepository.dismissedAdminMessageIds += adminMessage.id
view?.showAdminMessage(null)
}
}

View File

@ -1,6 +1,5 @@
package io.github.wulkanowy.ui.modules.login.symbol
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData
@ -45,8 +44,4 @@ interface LoginSymbolView : BaseView {
fun openFaqPage()
fun openSupportDialog(supportInfo: LoginSupportInfo)
fun showAdminMessage(adminMessage: AdminMessage?)
fun openInternetBrowser(url: String)
}

View File

@ -33,4 +33,4 @@ class LuckyNumberHistoryAdapter @Inject constructor() :
}
class ItemViewHolder(val binding: ItemLuckyNumberHistoryBinding) : RecyclerView.ViewHolder(binding.root)
}
}

View File

@ -14,9 +14,7 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.dataOrThrow
import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.LuckyNumberRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.isEndDateReached
import io.github.wulkanowy.data.toFirstResult
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.splash.SplashActivity
@ -37,9 +35,6 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() {
@Inject
lateinit var sharedPref: SharedPrefProvider
@Inject
lateinit var preferencesRepository: PreferencesRepository
companion object {
private const val LUCKY_NUMBER_WIDGET_MAX_SIZE = 196
@ -135,10 +130,6 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() {
}
private fun getLuckyNumber(studentId: Long, appWidgetId: Int) = runBlocking {
if (isEndDateReached) {
return@runBlocking null
}
try {
val students = studentRepository.getSavedStudents()
val student = students.singleOrNull { it.student.id == studentId }?.student
@ -154,11 +145,7 @@ class LuckyNumberWidgetProvider : AppWidgetProvider() {
}
if (currentStudent != null) {
luckyNumberRepository.getLuckyNumber(
student = currentStudent,
forceRefresh = false,
isFromAppWidget = true
)
luckyNumberRepository.getLuckyNumber(currentStudent, forceRefresh = false)
.toFirstResult()
.dataOrThrow
} else null

View File

@ -33,7 +33,6 @@ import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog
import io.github.wulkanowy.ui.modules.auth.AuthDialog
import io.github.wulkanowy.ui.modules.captcha.CaptchaDialog
import io.github.wulkanowy.ui.modules.end.EndFragment
import io.github.wulkanowy.ui.modules.settings.appearance.menuorder.AppMenuItem
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo
@ -139,8 +138,6 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
override fun onResume() {
super.onResume()
inAppUpdateHelper.onResume()
presenter.updateSdkMappings()
presenter.checkIfEnd()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@ -364,10 +361,4 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
super.onSaveInstanceState(outState)
navController.onSaveInstanceState(outState)
}
override fun navigateToEnd() {
binding.mainToolbar.isVisible = false
pushView(EndFragment())
onBackCallback?.isEnabled = false
}
}

View File

@ -6,8 +6,6 @@ import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.WulkanowyRepository
import io.github.wulkanowy.data.repositories.isEndDateReached
import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter
@ -16,7 +14,6 @@ import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.ui.modules.account.AccountView
import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsView
import io.github.wulkanowy.ui.modules.end.EndView
import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView
import io.github.wulkanowy.utils.AdsHelper
import io.github.wulkanowy.utils.AnalyticsHelper
@ -32,7 +29,6 @@ class MainPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val preferencesRepository: PreferencesRepository,
private val wulkanowyRepository: WulkanowyRepository,
private val syncManager: SyncManager,
private val analytics: AnalyticsHelper,
private val json: Json,
@ -77,7 +73,6 @@ class MainPresenter @Inject constructor(
syncManager.startPeriodicSyncWorker()
checkAppSupport()
updateCurrentStudentAuthStatus()
analytics.logEvent("app_open", "destination" to initDestination.toString())
Timber.i("Main view was initialized with $initDestination")
@ -112,7 +107,6 @@ class MainPresenter @Inject constructor(
}
private fun shouldShowBottomNavigation(destination: BaseView) = when (destination) {
is EndView,
is AccountView,
is StudentInfoView,
is AccountDetailsView -> false
@ -197,25 +191,4 @@ class MainPresenter @Inject constructor(
view?.showStudentAvatar(currentStudent)
}
private fun updateCurrentStudentAuthStatus() {
presenterScope.launch {
runCatching { studentRepository.updateCurrentStudentAuthStatus() }
.onFailure { errorHandler.dispatch(it) }
}
}
fun updateSdkMappings() {
presenterScope.launch {
runCatching { wulkanowyRepository.fetchMapping() }
.onFailure { Timber.e(it) }
}
}
fun checkIfEnd() {
if (isEndDateReached) {
syncManager.stopSyncWorker()
view?.navigateToEnd()
}
}
}

View File

@ -48,8 +48,6 @@ interface MainView : BaseView {
fun openMoreDestination(destination: Destination)
fun navigateToEnd()
interface MainChildView {
fun onFragmentReselected()

View File

@ -27,7 +27,6 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.message.mailboxchooser.MailboxChooserDialog
import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment
import io.github.wulkanowy.ui.modules.panicmode.PanicModeFragment
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
@ -133,7 +132,6 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
)
messageTabErrorRetry.setOnClickListener { presenter.onRetry() }
messageTabErrorDetails.setOnClickListener { presenter.onDetailsClick() }
messageTabPanicSection.dashboardPanicButton.setOnClickListener { presenter.onPanicButtonClicked() }
}
setFragmentResultListener(requireArguments().getString(MESSAGE_TAB_FOLDER_ID)!!) { _, bundle ->
@ -285,10 +283,6 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
)
}
override fun openPanicWebView(url: String) {
(requireActivity() as MainActivity).pushView(PanicModeFragment.newInstance(url))
}
override fun hideKeyboard() {
activity?.hideSoftInput()
}

View File

@ -20,7 +20,6 @@ import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import me.xdrop.fuzzywuzzy.FuzzySearch
import okhttp3.HttpUrl.Companion.toHttpUrl
import timber.log.Timber
import javax.inject.Inject
import kotlin.math.pow
@ -430,20 +429,4 @@ class MessageTabPresenter @Inject constructor(
+ dateRatio.toDouble().pow(2) * 2
).toInt()
}
fun onPanicButtonClicked() {
resourceFlow { studentRepository.getCurrentStudent() }
.onResourceError { errorHandler.dispatch(it) }
.onResourceSuccess {
val baseUrl = it.scrapperBaseUrl.toHttpUrl()
val urlToOpen = baseUrl.newBuilder()
.host("uonetplus${it.scrapperDomainSuffix}-wiadomosciplus.${baseUrl.host}")
.addPathSegment(it.symbol)
.build()
.toString()
view?.openPanicWebView(urlToOpen)
}
.launch("panic_button")
}
}

View File

@ -50,6 +50,4 @@ interface MessageTabView : BaseView {
fun showRecyclerBottomPadding(show: Boolean)
fun showMailboxChooser(mailboxes: List<Mailbox>)
fun openPanicWebView(url: String)
}

View File

@ -1,99 +0,0 @@
package io.github.wulkanowy.ui.modules.panicmode
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.View
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.addCallback
import androidx.core.os.bundleOf
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.WulkanowySdkFactory
import io.github.wulkanowy.databinding.FragmentPanicModeBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.WebkitCookieManagerProxy
import io.github.wulkanowy.utils.openInternetBrowser
import javax.inject.Inject
@AndroidEntryPoint
class PanicModeFragment : BaseFragment<FragmentPanicModeBinding>(R.layout.fragment_panic_mode),
MainView.TitledView {
@Inject
lateinit var wulkanowySdkFactory: WulkanowySdkFactory
@Inject
lateinit var webkitCookieManagerProxy: WebkitCookieManagerProxy
private var webView: WebView? = null
override val titleStringId: Int get() = R.string.panic_mode_title
companion object {
private const val PANIC_URL = "panic_mode_url"
fun newInstance(url: String?): PanicModeFragment {
return PanicModeFragment().apply {
arguments = bundleOf(PANIC_URL to url)
}
}
}
@SuppressLint("SetJavaScriptEnabled")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentPanicModeBinding.bind(view)
binding.panicModeRefresh.setOnClickListener {
binding.panicModeWebview.loadUrl(
binding.panicModeWebview.url ?: arguments?.getString(PANIC_URL).orEmpty()
)
}
binding.panicModeBack.setOnClickListener { binding.panicModeWebview.goBack() }
binding.panicModeHome.setOnClickListener {
binding.panicModeWebview.loadUrl(
arguments?.getString(PANIC_URL).orEmpty()
)
}
binding.panicModeForward.setOnClickListener { binding.panicModeWebview.goForward() }
binding.panicModeShare.setOnClickListener {
requireContext().openInternetBrowser(
binding.panicModeWebview.url.toString(),
)
}
val onBackPressedCallback = requireActivity().onBackPressedDispatcher
.addCallback(viewLifecycleOwner) {
binding.panicModeWebview.goBack()
}
with(binding.panicModeWebview) {
webView = this
with(settings) {
javaScriptEnabled = true
userAgentString = wulkanowySdkFactory.createBase().userAgent
}
webViewClient = object : WebViewClient() {
override fun doUpdateVisitedHistory(
view: WebView?,
url: String?,
isReload: Boolean
) {
binding.panicModeBack.isEnabled = binding.panicModeWebview.canGoBack()
binding.panicModeForward.isEnabled = binding.panicModeWebview.canGoForward()
onBackPressedCallback.isEnabled = binding.panicModeWebview.canGoBack()
}
}
loadUrl(arguments?.getString(PANIC_URL).orEmpty())
}
}
override fun onDestroy() {
webkitCookieManagerProxy.webkitCookieManager?.flush()
webView?.destroy()
super.onDestroy()
}
}

View File

@ -3,7 +3,6 @@ package io.github.wulkanowy.ui.modules.settings.appearance
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import androidx.core.os.bundleOf
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SeekBarPreference
import com.yariksoffice.lingver.Lingver
@ -31,18 +30,9 @@ class AppearanceFragment : PreferenceFragmentCompat(),
override val titleStringId get() = R.string.pref_settings_appearance_title
companion object {
fun withFocusedPreference(key: String) = AppearanceFragment().apply {
arguments = bundleOf(FOCUSED_KEY to key)
}
private const val FOCUSED_KEY = "focusedKey"
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.onAttachView(this)
arguments?.getString(FOCUSED_KEY)?.let { scrollToPreference(it) }
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {

View File

@ -7,28 +7,27 @@ import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.databinding.ItemTimetableBinding
import io.github.wulkanowy.databinding.ItemTimetableEmptyBinding
import io.github.wulkanowy.databinding.ItemTimetableMainAdditionalBinding
import io.github.wulkanowy.databinding.ItemTimetableSmallBinding
import io.github.wulkanowy.utils.SyncListAdapter
import io.github.wulkanowy.utils.getPlural
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.toFormattedString
import javax.inject.Inject
class TimetableAdapter @Inject constructor() :
SyncListAdapter<TimetableItem, RecyclerView.ViewHolder>(Differ) {
ListAdapter<TimetableItem, RecyclerView.ViewHolder>(differ) {
override fun getItemViewType(position: Int): Int = getItem(position).type.ordinal
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (TimetableItemType.entries[viewType]) {
return when (TimetableItemType.values()[viewType]) {
TimetableItemType.SMALL -> SmallViewHolder(
ItemTimetableSmallBinding.inflate(inflater, parent, false)
)
@ -40,10 +39,6 @@ class TimetableAdapter @Inject constructor() :
TimetableItemType.EMPTY -> EmptyViewHolder(
ItemTimetableEmptyBinding.inflate(inflater, parent, false)
)
TimetableItemType.ADDITIONAL -> AdditionalViewHolder(
ItemTimetableMainAdditionalBinding.inflate(inflater, parent, false)
)
}
}
@ -66,30 +61,16 @@ class TimetableAdapter @Inject constructor() :
binding = holder.binding,
item = getItem(position) as TimetableItem.Small,
)
is NormalViewHolder -> bindNormalView(
binding = holder.binding,
item = getItem(position) as TimetableItem.Normal,
)
is EmptyViewHolder -> bindEmptyView(
binding = holder.binding,
item = getItem(position) as TimetableItem.Empty,
)
is AdditionalViewHolder -> bindAdditionalView(
binding = holder.binding,
item = getItem(position) as TimetableItem.Additional,
)
}
}
private fun bindAdditionalView(
binding: ItemTimetableMainAdditionalBinding,
item: TimetableItem.Additional
) {
with(binding) {
timetableItemSubject.text = item.additional.subject
timetableItemTimeStart.text = item.additional.start.toFormattedString("HH:mm")
timetableItemTimeFinish.text = item.additional.end.toFormattedString("HH:mm")
}
}
@ -98,7 +79,6 @@ class TimetableAdapter @Inject constructor() :
with(binding) {
timetableSmallItemNumber.text = lesson.number.toString()
timetableSmallItemNumber.isVisible = item.isLessonNumberVisible
timetableSmallItemSubject.text = lesson.subject
timetableSmallItemTimeStart.text = lesson.start.toFormattedString("HH:mm")
timetableSmallItemRoom.text = lesson.room
@ -117,7 +97,6 @@ class TimetableAdapter @Inject constructor() :
with(binding) {
timetableItemNumber.text = lesson.number.toString()
timetableItemNumber.isVisible = item.isLessonNumberVisible
timetableItemSubject.text = lesson.subject
timetableItemGroup.text = lesson.group
timetableItemRoom.text = lesson.room
@ -326,32 +305,31 @@ class TimetableAdapter @Inject constructor() :
private class EmptyViewHolder(val binding: ItemTimetableEmptyBinding) :
RecyclerView.ViewHolder(binding.root)
private class AdditionalViewHolder(val binding: ItemTimetableMainAdditionalBinding) :
RecyclerView.ViewHolder(binding.root)
companion object {
private val differ = object : DiffUtil.ItemCallback<TimetableItem>() {
override fun areItemsTheSame(oldItem: TimetableItem, newItem: TimetableItem): Boolean =
when {
oldItem is TimetableItem.Small && newItem is TimetableItem.Small -> {
oldItem.lesson.start == newItem.lesson.start
}
private object Differ : DiffUtil.ItemCallback<TimetableItem>() {
override fun areItemsTheSame(oldItem: TimetableItem, newItem: TimetableItem): Boolean =
when {
oldItem is TimetableItem.Small && newItem is TimetableItem.Small -> {
oldItem.lesson.start == newItem.lesson.start
oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal -> {
oldItem.lesson.start == newItem.lesson.start
}
else -> oldItem == newItem
}
oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal -> {
oldItem.lesson.start == newItem.lesson.start
}
override fun areContentsTheSame(oldItem: TimetableItem, newItem: TimetableItem) =
oldItem == newItem
else -> oldItem == newItem
}
override fun areContentsTheSame(oldItem: TimetableItem, newItem: TimetableItem) =
oldItem == newItem
override fun getChangePayload(oldItem: TimetableItem, newItem: TimetableItem): Any? {
return if (oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal) {
if (oldItem.lesson == newItem.lesson && oldItem.showGroupsInPlan == newItem.showGroupsInPlan && oldItem.timeLeft != newItem.timeLeft) {
"time_left"
override fun getChangePayload(oldItem: TimetableItem, newItem: TimetableItem): Any? {
return if (oldItem is TimetableItem.Normal && newItem is TimetableItem.Normal) {
if (oldItem.lesson == newItem.lesson && oldItem.showGroupsInPlan == newItem.showGroupsInPlan && oldItem.timeLeft != newItem.timeLeft) {
"time_left"
} else super.getChangePayload(oldItem, newItem)
} else super.getChangePayload(oldItem, newItem)
} else super.getChangePayload(oldItem, newItem)
}
}
}
}

View File

@ -21,11 +21,7 @@ import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.timetable.additional.AdditionalLessonsFragment
import io.github.wulkanowy.ui.modules.timetable.completed.CompletedLessonsFragment
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.firstSchoolDayInSchoolYear
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.lastSchoolDayInSchoolYear
import io.github.wulkanowy.utils.openMaterialDatePicker
import io.github.wulkanowy.utils.*
import java.time.LocalDate
import javax.inject.Inject
@ -108,11 +104,8 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
}
}
override fun updateData(data: List<TimetableItem>, isDayChanged: Boolean) {
when {
isDayChanged -> timetableAdapter.recreate(data)
else -> timetableAdapter.submitList(data)
}
override fun updateData(data: List<TimetableItem>) {
timetableAdapter.submitList(data)
}
override fun clearData() {

View File

@ -1,21 +1,18 @@
package io.github.wulkanowy.ui.modules.timetable
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.db.entities.TimetableAdditional
import java.time.Duration
sealed class TimetableItem(val type: TimetableItemType) {
data class Small(
val lesson: Timetable,
val isLessonNumberVisible: Boolean,
val onClick: (Timetable) -> Unit,
) : TimetableItem(TimetableItemType.SMALL)
data class Normal(
val lesson: Timetable,
val showGroupsInPlan: Boolean,
val isLessonNumberVisible: Boolean,
val timeLeft: TimeLeft?,
val onClick: (Timetable) -> Unit,
) : TimetableItem(TimetableItemType.NORMAL)
@ -24,10 +21,6 @@ sealed class TimetableItem(val type: TimetableItemType) {
val numFrom: Int,
val numTo: Int
) : TimetableItem(TimetableItemType.EMPTY)
data class Additional(
val additional: TimetableAdditional,
) : TimetableItem(TimetableItemType.ADDITIONAL)
}
data class TimeLeft(
@ -39,6 +32,5 @@ data class TimeLeft(
enum class TimetableItemType {
SMALL,
NORMAL,
EMPTY,
ADDITIONAL,
EMPTY
}

View File

@ -4,9 +4,6 @@ import android.os.Handler
import android.os.Looper
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.db.entities.TimetableAdditional
import io.github.wulkanowy.data.enums.ShowAdditionalLessonsMode.BELOW
import io.github.wulkanowy.data.enums.ShowAdditionalLessonsMode.NONE
import io.github.wulkanowy.data.enums.TimetableGapsMode.BETWEEN_AND_BEFORE_LESSONS
import io.github.wulkanowy.data.enums.TimetableGapsMode.NO_GAPS
import io.github.wulkanowy.data.enums.TimetableMode
@ -17,7 +14,6 @@ import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceIntermediate
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.pojos.TimetableFull
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
@ -61,7 +57,6 @@ class TimetablePresenter @Inject constructor(
private var initialDate: LocalDate? = null
private var isWeekendHasLessons: Boolean = false
private var isEduOne: Boolean = false
var currentDate: LocalDate? = null
private set
@ -85,7 +80,7 @@ class TimetablePresenter @Inject constructor(
} else currentDate?.previousSchoolDay
reloadView(date ?: return)
loadData(isDayChanged = true)
loadData()
}
fun onNextDay() {
@ -94,7 +89,7 @@ class TimetablePresenter @Inject constructor(
} else currentDate?.nextSchoolDay
reloadView(date ?: return)
loadData(isDayChanged = true)
loadData()
}
fun onPickDate() {
@ -108,7 +103,7 @@ class TimetablePresenter @Inject constructor(
fun onSwipeRefresh() {
Timber.i("Force refreshing the timetable")
loadData(forceRefresh = true)
loadData(true)
}
fun onRetry() {
@ -116,7 +111,7 @@ class TimetablePresenter @Inject constructor(
showErrorView(false)
showProgress(true)
}
loadData(forceRefresh = true)
loadData(true)
}
fun onDetailsClick() {
@ -149,12 +144,11 @@ class TimetablePresenter @Inject constructor(
return true
}
private fun loadData(forceRefresh: Boolean = false, isDayChanged: Boolean = false) {
private fun loadData(forceRefresh: Boolean = false) {
flatResourceFlow {
val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student)
isEduOne = student.isEduOne == true
checkInitialAndCurrentDate(semester)
timetableRepository.getTimetable(
student = student,
@ -173,9 +167,9 @@ class TimetablePresenter @Inject constructor(
enableSwipe(true)
showProgress(false)
showErrorView(false)
updateData(it, isDayChanged)
showContent(it.lessons.isNotEmpty() || it.additional.isNotEmpty())
showEmpty(it.lessons.isEmpty() && it.additional.isEmpty())
showContent(it.lessons.isNotEmpty())
showEmpty(it.lessons.isEmpty())
updateData(it.lessons)
setDayHeaderMessage(it.headers.find { header -> header.date == currentDate }?.content)
reloadNavigation()
}
@ -220,97 +214,66 @@ class TimetablePresenter @Inject constructor(
}
}
private fun updateData(lessons: TimetableFull, isDayChanged: Boolean) {
private fun updateData(lessons: List<Timetable>) {
tickTimer?.cancel()
view?.updateData(createItems(lessons), isDayChanged)
if (currentDate == now()) {
tickTimer = timer(period = 2_000, initialDelay = 2_000) {
if (currentDate != now()) {
view?.updateData(createItems(lessons))
} else {
tickTimer = timer(period = 2_000) {
Handler(Looper.getMainLooper()).post {
view?.updateData(createItems(lessons), isDayChanged)
view?.updateData(createItems(lessons))
}
}
}
}
private sealed class Item(
val isStudentPlan: Boolean,
val start: Instant,
val number: Int?,
) {
class Lesson(val lesson: Timetable) :
Item(lesson.isStudentPlan, lesson.start, lesson.number)
class Additional(val additional: TimetableAdditional) : Item(true, additional.start, null)
}
private fun createItems(fullTimetable: TimetableFull): List<TimetableItem> {
val showAdditionalLessonsInPlan = prefRepository.showAdditionalLessonsInPlan
val allItems =
fullTimetable.lessons.map(Item::Lesson) + fullTimetable.additional.map(Item::Additional)
.takeIf { showAdditionalLessonsInPlan != NONE }.orEmpty()
val filteredItems = allItems.filter {
if (prefRepository.showWholeClassPlan == TimetableMode.ONLY_CURRENT_GROUP) {
it.isStudentPlan
} else true
}.sortedWith(
(compareBy<Item> { it is Item.Additional }
.takeIf { showAdditionalLessonsInPlan == BELOW } ?: EmptyComparator())
.thenBy { it.start }
.thenBy { !it.isStudentPlan }
)
private fun createItems(items: List<Timetable>): List<TimetableItem> {
val filteredItems = items
.filter {
if (prefRepository.showWholeClassPlan == TimetableMode.ONLY_CURRENT_GROUP) {
it.isStudentPlan
} else true
}.sortedWith(
compareBy({ item -> item.number }, { item -> !item.isStudentPlan })
)
var prevNum = when (prefRepository.showTimetableGaps) {
BETWEEN_AND_BEFORE_LESSONS -> 0
else -> null
}
var prevIsAdditional = false
return buildList {
filteredItems.forEachIndexed { i, it ->
if (prefRepository.showTimetableGaps != NO_GAPS) {
if (prevNum != null && it.number != null && it.number > prevNum!! + 1) {
if (!prevIsAdditional) {
// Additional lessons do count as a lesson so don't add empty lessons
// when there is an additional lesson present
val emptyLesson = TimetableItem.Empty(
numFrom = prevNum!! + 1, numTo = it.number - 1
)
add(emptyLesson)
}
}
prevNum = it.number
prevIsAdditional = it is Item.Additional
if (prefRepository.showTimetableGaps != NO_GAPS && prevNum != null && it.number > prevNum!! + 1) {
val emptyLesson = TimetableItem.Empty(
numFrom = prevNum!! + 1,
numTo = it.number - 1
)
add(emptyLesson)
}
if (it is Item.Lesson) {
if (it.isStudentPlan) {
val normalLesson = TimetableItem.Normal(
lesson = it.lesson,
showGroupsInPlan = prefRepository.showGroupsInPlan,
timeLeft = filteredItems.getTimeLeftForLesson(it.lesson, i),
onClick = ::onTimetableItemSelected,
isLessonNumberVisible = !isEduOne
)
add(normalLesson)
} else {
val smallLesson = TimetableItem.Small(
lesson = it.lesson,
onClick = ::onTimetableItemSelected,
isLessonNumberVisible = !isEduOne
)
add(smallLesson)
}
} else if (it is Item.Additional) {
// If the user disabled showing additional lessons, they would've been filtered
// out already, so there's no need to check it again.
add(TimetableItem.Additional(it.additional))
if (it.isStudentPlan) {
val normalLesson = TimetableItem.Normal(
lesson = it,
showGroupsInPlan = prefRepository.showGroupsInPlan,
timeLeft = filteredItems.getTimeLeftForLesson(it, i),
onClick = ::onTimetableItemSelected
)
add(normalLesson)
} else {
val smallLesson = TimetableItem.Small(
lesson = it,
onClick = ::onTimetableItemSelected
)
add(smallLesson)
}
prevNum = it.number
}
}
}
private fun List<Item>.getTimeLeftForLesson(lesson: Timetable, index: Int): TimeLeft {
private fun List<Timetable>.getTimeLeftForLesson(lesson: Timetable, index: Int): TimeLeft {
val isShowTimeUntil = lesson.isShowTimeUntil(getPreviousLesson(index))
return TimeLeft(
until = lesson.until.plusMinutes(1).takeIf { isShowTimeUntil },
@ -319,20 +282,11 @@ class TimetablePresenter @Inject constructor(
)
}
private fun List<Item>.getPreviousLesson(position: Int): Instant? {
val lessonAdditionalOffset = filterIndexed { i, item ->
i < position && item is Item.Additional
}.size
val lessonStudentPlanOffset = filterIndexed { i, item ->
i < position && !item.isStudentPlan
}.size
val lessonIndex = position - 1 - lessonAdditionalOffset - lessonStudentPlanOffset
return filterIsInstance<Item.Lesson>()
.filter { it.isStudentPlan }
.getOrNull(lessonIndex)
private fun List<Timetable>.getPreviousLesson(position: Int): Instant? {
return filter { it.isStudentPlan }
.getOrNull(position - 1 - filterIndexed { i, item -> i < position && !item.isStudentPlan }.size)
?.let {
if (!it.lesson.canceled && it.isStudentPlan) it.lesson.end
if (!it.canceled && it.isStudentPlan) it.end
else null
}
}
@ -385,7 +339,3 @@ class TimetablePresenter @Inject constructor(
super.onDetachView()
}
}
private class EmptyComparator<T> : Comparator<T> {
override fun compare(o1: T, o2: T) = 0
}

View File

@ -12,7 +12,7 @@ interface TimetableView : BaseView {
fun initView()
fun updateData(data: List<TimetableItem>, isDayChanged: Boolean)
fun updateData(data: List<TimetableItem>)
fun updateNavigationDay(date: String)

View File

@ -13,11 +13,7 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.timetable.additional.add.AdditionalLessonAddDialog
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.firstSchoolDayInSchoolYear
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.lastSchoolDayInSchoolYear
import io.github.wulkanowy.utils.openMaterialDatePicker
import io.github.wulkanowy.utils.*
import java.time.LocalDate
import javax.inject.Inject
@ -136,12 +132,8 @@ class AdditionalLessonsFragment :
binding.additionalLessonsNextButton.visibility = if (show) View.VISIBLE else View.INVISIBLE
}
override fun showAddAdditionalLessonDialog(currentDate: LocalDate) {
(activity as? MainActivity)?.showDialogFragment(
AdditionalLessonAddDialog.newInstance(
currentDate
)
)
override fun showAddAdditionalLessonDialog() {
(activity as? MainActivity)?.showDialogFragment(AdditionalLessonAddDialog.newInstance())
}
override fun showDatePickerDialog(selectedDate: LocalDate) {

View File

@ -1,27 +1,14 @@
package io.github.wulkanowy.ui.modules.timetable.additional
import android.annotation.SuppressLint
import io.github.wulkanowy.data.*
import io.github.wulkanowy.data.db.entities.TimetableAdditional
import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.onResourceData
import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.domain.timetable.IsStudentHasLessonsOnWeekendUseCase
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.capitalise
import io.github.wulkanowy.utils.getLastSchoolDayIfHoliday
import io.github.wulkanowy.utils.isHolidays
import io.github.wulkanowy.utils.nextOrSameSchoolDay
import io.github.wulkanowy.utils.nextSchoolDay
import io.github.wulkanowy.utils.previousSchoolDay
import io.github.wulkanowy.utils.toFormattedString
import io.github.wulkanowy.utils.*
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onEach
@ -35,14 +22,11 @@ class AdditionalLessonsPresenter @Inject constructor(
errorHandler: ErrorHandler,
private val semesterRepository: SemesterRepository,
private val timetableRepository: TimetableRepository,
private val isStudentHasLessonsOnWeekendUseCase: IsStudentHasLessonsOnWeekendUseCase,
private val analytics: AnalyticsHelper
) : BasePresenter<AdditionalLessonsView>(errorHandler, studentRepository) {
private var baseDate: LocalDate = LocalDate.now().nextOrSameSchoolDay
private var isWeekendHasLessons: Boolean = false
lateinit var currentDate: LocalDate
private set
@ -59,18 +43,12 @@ class AdditionalLessonsPresenter @Inject constructor(
}
fun onPreviousDay() {
val date = if (isWeekendHasLessons) {
currentDate.minusDays(1)
} else currentDate.previousSchoolDay
loadData(date)
loadData(currentDate.previousSchoolDay)
reloadView()
}
fun onNextDay() {
val date = if (isWeekendHasLessons) {
currentDate.plusDays(1)
} else currentDate.nextSchoolDay
loadData(date)
loadData(currentDate.nextSchoolDay)
reloadView()
}
@ -79,7 +57,7 @@ class AdditionalLessonsPresenter @Inject constructor(
}
fun onAdditionalLessonAddButtonClicked() {
view?.showAddAdditionalLessonDialog(currentDate)
view?.showAddAdditionalLessonDialog()
}
fun onDateSet(year: Int, month: Int, day: Int) {
@ -153,8 +131,6 @@ class AdditionalLessonsPresenter @Inject constructor(
flatResourceFlow {
val student = studentRepository.getCurrentStudent()
val semester = semesterRepository.getCurrentSemester(student)
isWeekendHasLessons = isStudentHasLessonsOnWeekendUseCase(semester, currentDate)
timetableRepository.getTimetable(
student = student,
semester = semester,

View File

@ -36,7 +36,7 @@ interface AdditionalLessonsView : BaseView {
fun showDatePickerDialog(selectedDate: LocalDate)
fun showAddAdditionalLessonDialog(currentDate: LocalDate)
fun showAddAdditionalLessonDialog()
fun showSuccessMessage()

View File

@ -3,7 +3,6 @@ package io.github.wulkanowy.ui.modules.timetable.additional.add
import android.app.Dialog
import android.os.Bundle
import android.view.View
import androidx.core.os.bundleOf
import androidx.core.widget.doOnTextChanged
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.timepicker.MaterialTimePicker
@ -27,12 +26,10 @@ class AdditionalLessonAddDialog : BaseDialogFragment<DialogAdditionalAddBinding>
lateinit var presenter: AdditionalLessonAddPresenter
companion object {
const val ARGUMENT_KEY = "additional_lesson_default_date"
fun newInstance(defaultDate: LocalDate) = AdditionalLessonAddDialog().apply {
arguments = bundleOf(ARGUMENT_KEY to defaultDate.toEpochDay())
}
fun newInstance() = AdditionalLessonAddDialog()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return MaterialAlertDialogBuilder(requireContext(), theme)
.setView(
@ -43,13 +40,10 @@ class AdditionalLessonAddDialog : BaseDialogFragment<DialogAdditionalAddBinding>
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
arguments?.getLong(ARGUMENT_KEY)?.let(LocalDate::ofEpochDay)?.let {
presenter.onDateSelected(it)
}
presenter.onAttachView(this)
}
override fun initView(selectedDate: LocalDate) {
override fun initView() {
with(binding) {
additionalLessonDialogStartEdit.doOnTextChanged { _, _, _, _ ->
additionalLessonDialogStart.isErrorEnabled = false
@ -59,7 +53,6 @@ class AdditionalLessonAddDialog : BaseDialogFragment<DialogAdditionalAddBinding>
additionalLessonDialogEnd.isErrorEnabled = false
additionalLessonDialogEnd.error = null
}
additionalLessonDialogDateEdit.setText(selectedDate.toFormattedString())
additionalLessonDialogDateEdit.doOnTextChanged { _, _, _, _ ->
additionalLessonDialogDate.isErrorEnabled = false
additionalLessonDialogDate.error = null
@ -68,6 +61,7 @@ class AdditionalLessonAddDialog : BaseDialogFragment<DialogAdditionalAddBinding>
additionalLessonDialogContent.isErrorEnabled = false
additionalLessonDialogContent.error = null
}
additionalLessonDialogAdd.setOnClickListener {
presenter.onAddAdditionalClicked(
start = additionalLessonDialogStartEdit.text?.toString(),
@ -161,9 +155,7 @@ class AdditionalLessonAddDialog : BaseDialogFragment<DialogAdditionalAddBinding>
.build()
timePicker.addOnPositiveButtonClickListener {
if (isAdded) {
onTimeSelected(LocalTime.of(timePicker.hour, timePicker.minute))
}
onTimeSelected(LocalTime.of(timePicker.hour, timePicker.minute))
}
if (!parentFragmentManager.isStateSaved) {

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