Merge branch 'release/0.25.0'

This commit is contained in:
Mikołaj Pich 2021-02-02 00:47:35 +01:00
commit 624fd71dbb
160 changed files with 7968 additions and 826 deletions

View File

@ -18,18 +18,9 @@
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="CONTINUATION_INDENT_IN_PARAMETER_LISTS" value="false" />
<option name="CONTINUATION_INDENT_IN_ARGUMENT_LISTS" value="false" />
<option name="CONTINUATION_INDENT_FOR_EXPRESSION_BODIES" value="false" />
<option name="CONTINUATION_INDENT_FOR_CHAINED_CALLS" value="false" />
<option name="CONTINUATION_INDENT_IN_SUPERTYPE_LISTS" value="false" />
<option name="CONTINUATION_INDENT_IN_IF_CONDITIONS" value="false" />
<option name="CONTINUATION_INDENT_IN_ELVIS" value="false" />
<option name="WRAP_ELVIS_EXPRESSIONS" value="0" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<MarkdownNavigatorCodeStyleSettings>
<option name="RIGHT_MARGIN" value="72" />
</MarkdownNavigatorCodeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
@ -143,13 +134,11 @@
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="METHOD_PARAMETERS_LPAREN_ON_NEXT_LINE" value="true" />
<option name="METHOD_PARAMETERS_RPAREN_ON_NEXT_LINE" value="true" />
<option name="EXTENDS_LIST_WRAP" value="1" />
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>

View File

@ -11,19 +11,22 @@ apply from: 'hooks.gradle'
android {
compileSdkVersion 30
buildToolsVersion '30.0.2'
buildToolsVersion '30.0.3'
defaultConfig {
applicationId "io.github.wulkanowy"
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 17
targetSdkVersion 30
versionCode 81
versionName "0.24.3"
versionCode 82
versionName "0.25.0"
multiDexEnabled true
resValue "string", "app_name", "Wulkanowy"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true
resValue "string", "app_name", "Wulkanowy"
buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis())
manifestPlaceholders = [
firebase_enabled: project.hasProperty("enableFirebase")
]
@ -126,19 +129,20 @@ play {
serviceAccountCredentials = file('key.p12')
defaultToAppBundles = false
track = 'alpha'
updatePriority = 5
updatePriority = 3
}
ext {
work_manager = "2.4.0"
room = "2.2.6"
work_manager = "2.5.0"
work_hilt = "1.0.0-alpha03"
room = "2.3.0-beta01"
chucker = "3.4.0"
mockk = "1.10.5"
moshi = "1.11.0"
}
dependencies {
implementation "io.github.wulkanowy:sdk:0.24.1"
implementation "io.github.wulkanowy:sdk:0.25.0"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
@ -159,10 +163,9 @@ dependencies {
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
implementation "com.google.android.material:material:1.2.1"
implementation "com.google.android.material:material:1.3.0-rc01"
implementation "com.github.wulkanowy:material-chips-input:2.1.1"
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0"
implementation "me.zhanghai.android.materialprogressbar:library:1.6.1"
implementation "androidx.work:work-runtime-ktx:$work_manager"
playImplementation "androidx.work:work-gcm:$work_manager"
@ -175,12 +178,12 @@ dependencies {
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
implementation 'androidx.hilt:hilt-work:1.0.0-alpha02'
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
implementation "androidx.hilt:hilt-work:$work_hilt"
kapt "androidx.hilt:hilt-compiler:$work_hilt"
implementation "com.aurelhubert:ahbottomnavigation:2.3.4"
implementation "com.ncapdevi:frag-nav:3.3.0"
implementation "com.github.YarikSOffice:lingver:1.2.2"
implementation "com.github.YarikSOffice:lingver:1.3.0"
implementation "com.squareup.moshi:moshi:$moshi"
implementation "com.squareup.moshi:moshi-adapters:$moshi"
@ -194,7 +197,7 @@ dependencies {
implementation "io.github.wulkanowy:AppKillerManager:3.0.0"
implementation 'me.xdrop:fuzzywuzzy:1.3.1'
playImplementation platform('com.google.firebase:firebase-bom:26.3.0')
playImplementation platform('com.google.firebase:firebase-bom:26.4.0')
playImplementation 'com.google.firebase:firebase-analytics-ktx'
playImplementation 'com.google.firebase:firebase-inappmessaging-display-ktx'
playImplementation "com.google.firebase:firebase-inappmessaging-ktx"
@ -204,7 +207,7 @@ dependencies {
playImplementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
hmsImplementation 'com.huawei.hms:hianalytics:5.1.0.301'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.4.2.301'
hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.5.0.200'
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"
@ -214,6 +217,7 @@ dependencies {
testImplementation "junit:junit:4.13.1"
testImplementation "io.mockk:mockk:$mockk"
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2'
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
androidTestImplementation "androidx.test:core:1.3.0"
androidTestImplementation "androidx.test:runner:1.3.0"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +0,0 @@
package io.github.wulkanowy.data
import io.github.wulkanowy.utils.DispatchersProvider
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
class TestDispatchersProvider : DispatchersProvider() {
override val backgroundThread: CoroutineDispatcher
get() = Dispatchers.Unconfined
}

View File

@ -0,0 +1,19 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:scaleX="0.92"
android:scaleY="0.92"
android:translateX="0.96"
android:translateY="0.96">
<path
android:fillColor="#FFF"
android:pathData="M3.9512,2A2,2 0,0 0,2 4L2,18A2,2 0,0 0,4 20L10.0996,20C11.3596,21.24 13.09,22 15,22A7,7 0,0 0,15.7988 21.9551L15.7988,19.7832A4.85,4.85 0,0 1,15 19.8496C12.32,19.8496 10.1504,17.68 10.1504,15A4.85,4.85 0,0 1,15 10.1504C17.4677,10.1504 19.4978,11.9912 19.8047,14.375C20.566,14.3758 21.3108,14.5325 21.9922,14.834C21.9491,12.9905 21.2036,11.3226 20,10.0996L20,4A2,2 0,0 0,18 2L4,2A2,2 0,0 0,3.9512 2zM4,5L10,5L10,8L4,8L4,5zM12,5L18,5L18,8L12,8L12,5zM4,10L10.0996,10C9.2596,10.82 8.6291,11.85 8.2891,13L4,13L4,10zM14,12L14,15.6895L15.7988,16.7266L15.7988,14.9922L15.5,14.8203L15.5,12L14,12zM4,15L8,15C8,16.07 8.2399,17.09 8.6699,18L4,18L4,15z" />
<path
android:fillColor="#FFF"
android:pathData="m17.298,24v-8.1249h2.5c0.7143,0 1.3523,0.1618 1.9141,0.4855 0.5655,0.3199 1.0063,0.7775 1.3225,1.3728 0.3162,0.5915 0.4743,1.2649 0.4743,2.0201v0.3739c0,0.7552 -0.1562,1.4267 -0.4687,2.0145 -0.3088,0.5878 -0.7459,1.0435 -1.3114,1.3672C21.1633,23.8326 20.5253,23.9963 19.8148,24ZM18.9721,17.2311v5.4241h0.8091c0.6548,0 1.1551,-0.2139 1.5011,-0.6417 0.346,-0.4278 0.5227,-1.0398 0.5301,-1.8359v-0.4297c0,-0.8259 -0.1711,-1.4509 -0.5134,-1.875 -0.3423,-0.4278 -0.8426,-0.6417 -1.5011,-0.6417z" />
</group>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 700 B

View File

@ -18,6 +18,18 @@
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="mailto" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="tel" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="geo" />
</intent>
</queries>
<application

View File

@ -5,6 +5,7 @@ import android.content.Context
import android.util.Log.DEBUG
import android.util.Log.INFO
import android.util.Log.VERBOSE
import android.webkit.WebView
import androidx.hilt.work.HiltWorkerFactory
import androidx.multidex.MultiDex
import androidx.work.Configuration
@ -47,22 +48,23 @@ class WulkanowyApp : Application(), Configuration.Provider {
override fun onCreate() {
super.onCreate()
Lingver.init(this)
themeManager.applyDefaultTheme()
initializeAppLanguage()
themeManager.applyDefaultTheme()
initLogging()
logCurrentLanguage()
fixWebViewLocale()
}
private fun initLogging() {
if (appInfo.isDebug) {
Timber.plant(DebugLogTree())
Timber.plant(FileLoggerTree.Builder()
.withFileName("wulkanowy.%g.log")
.withDirName(applicationContext.filesDir.absolutePath)
.withFileLimit(10)
.withMinPriority(DEBUG)
.build()
Timber.plant(
FileLoggerTree.Builder()
.withFileName("wulkanowy.%g.log")
.withDirName(applicationContext.filesDir.absolutePath)
.withFileLimit(10)
.withMinPriority(DEBUG)
.build()
)
} else {
Timber.plant(CrashLogExceptionTree())
@ -71,14 +73,20 @@ class WulkanowyApp : Application(), Configuration.Provider {
registerActivityLifecycleCallbacks(ActivityLifecycleLogger())
}
private fun logCurrentLanguage() {
val newLang = if (preferencesRepository.appLanguage == "system") {
appInfo.systemLanguage
} else {
preferencesRepository.appLanguage
}
private fun initializeAppLanguage() {
Lingver.init(this)
analyticsHelper.logEvent("language", "startup" to newLang)
if (preferencesRepository.appLanguage == "system") {
Lingver.getInstance().setFollowSystemLocale(this)
analyticsHelper.logEvent("language", "startup" to appInfo.systemLanguage)
} else {
analyticsHelper.logEvent("language", "startup" to preferencesRepository.appLanguage)
}
}
private fun fixWebViewLocale() {
//https://stackoverflow.com/questions/40398528/android-webview-language-changes-abruptly-on-android-7-0-and-above
WebView(this).destroy()
}
override fun getWorkManagerConfiguration() = Configuration.Builder()

View File

@ -33,17 +33,21 @@ internal class RepositoryModule {
setSimpleHttpLogger { Timber.d(it) }
// for debug only
addInterceptor(ChuckerInterceptor.Builder(context)
.collector(chuckerCollector)
.alwaysReadResponseBody(true)
.build(), network = true
addInterceptor(
ChuckerInterceptor.Builder(context)
.collector(chuckerCollector)
.alwaysReadResponseBody(true)
.build(), network = true
)
}
}
@Singleton
@Provides
fun provideChuckerCollector(@ApplicationContext context: Context, prefRepository: PreferencesRepository): ChuckerCollector {
fun provideChuckerCollector(
@ApplicationContext context: Context,
prefRepository: PreferencesRepository
): ChuckerCollector {
return ChuckerCollector(
context = context,
showNotification = prefRepository.isDebugNotificationEnable,
@ -53,7 +57,10 @@ internal class RepositoryModule {
@Singleton
@Provides
fun provideDatabase(@ApplicationContext context: Context, sharedPrefProvider: SharedPrefProvider) = AppDatabase.newInstance(context, sharedPrefProvider)
fun provideDatabase(
@ApplicationContext context: Context,
sharedPrefProvider: SharedPrefProvider,
) = AppDatabase.newInstance(context, sharedPrefProvider)
@Singleton
@Provides
@ -65,7 +72,8 @@ internal class RepositoryModule {
@Singleton
@Provides
fun provideSharedPref(@ApplicationContext context: Context): SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
fun provideSharedPref(@ApplicationContext context: Context): SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context)
@Singleton
@Provides
@ -89,7 +97,8 @@ internal class RepositoryModule {
@Singleton
@Provides
fun provideGradeSemesterStatisticsDao(database: AppDatabase) = database.gradeSemesterStatisticsDao
fun provideGradeSemesterStatisticsDao(database: AppDatabase) =
database.gradeSemesterStatisticsDao
@Singleton
@Provides
@ -166,4 +175,8 @@ internal class RepositoryModule {
@Singleton
@Provides
fun provideTimetableAdditionalDao(database: AppDatabase) = database.timetableAdditionalDao
@Singleton
@Provides
fun provideStudentInfoDao(database: AppDatabase) = database.studentInfoDao
}

View File

@ -28,6 +28,7 @@ import io.github.wulkanowy.data.db.dao.ReportingUnitDao
import io.github.wulkanowy.data.db.dao.SchoolDao
import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.dao.StudentInfoDao
import io.github.wulkanowy.data.db.dao.SubjectDao
import io.github.wulkanowy.data.db.dao.TeacherDao
import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao
@ -53,6 +54,7 @@ import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.data.db.entities.School
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentInfo
import io.github.wulkanowy.data.db.entities.Subject
import io.github.wulkanowy.data.db.entities.Teacher
import io.github.wulkanowy.data.db.entities.Timetable
@ -80,6 +82,8 @@ import io.github.wulkanowy.data.db.migrations.Migration28
import io.github.wulkanowy.data.db.migrations.Migration29
import io.github.wulkanowy.data.db.migrations.Migration3
import io.github.wulkanowy.data.db.migrations.Migration30
import io.github.wulkanowy.data.db.migrations.Migration31
import io.github.wulkanowy.data.db.migrations.Migration32
import io.github.wulkanowy.data.db.migrations.Migration4
import io.github.wulkanowy.data.db.migrations.Migration5
import io.github.wulkanowy.data.db.migrations.Migration6
@ -116,6 +120,7 @@ import javax.inject.Singleton
School::class,
Conference::class,
TimetableAdditional::class,
StudentInfo::class,
],
version = AppDatabase.VERSION_SCHEMA,
exportSchema = true
@ -124,7 +129,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 30
const val VERSION_SCHEMA = 32
fun getMigrations(sharedPrefProvider: SharedPrefProvider): Array<Migration> {
return arrayOf(
@ -157,6 +162,8 @@ abstract class AppDatabase : RoomDatabase() {
Migration28(),
Migration29(),
Migration30(),
Migration31(),
Migration32()
)
}
@ -219,4 +226,6 @@ abstract class AppDatabase : RoomDatabase() {
abstract val conferenceDao: ConferenceDao
abstract val timetableAdditionalDao: TimetableAdditionalDao
abstract val studentInfoDao: StudentInfoDao
}

View File

@ -6,7 +6,9 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy.ABORT
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentNick
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import javax.inject.Singleton
@ -20,6 +22,9 @@ interface StudentDao {
@Delete
suspend fun delete(student: Student)
@Update(entity = Student::class)
suspend fun update(studentNick: StudentNick)
@Query("SELECT * FROM Students WHERE is_current = 1")
suspend fun loadCurrent(): Student?

View File

@ -0,0 +1,15 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.StudentInfo
import kotlinx.coroutines.flow.Flow
import javax.inject.Singleton
@Singleton
@Dao
interface StudentInfoDao : BaseDao<StudentInfo> {
@Query("SELECT * FROM StudentInfo WHERE student_id = :studentId")
fun loadStudentInfo(studentId: Int): Flow<StudentInfo?>
}

View File

@ -7,7 +7,13 @@ import androidx.room.PrimaryKey
import java.io.Serializable
import java.time.LocalDateTime
@Entity(tableName = "Students", indices = [Index(value = ["email", "symbol", "student_id", "school_id", "class_id"], unique = true)])
@Entity(
tableName = "Students",
indices = [Index(
value = ["email", "symbol", "student_id", "school_id", "class_id"],
unique = true
)]
)
data class Student(
@ColumnInfo(name = "scrapper_base_url")
@ -52,7 +58,7 @@ data class Student(
@ColumnInfo(name = "school_id")
val schoolSymbol: String,
@ColumnInfo(name ="school_short")
@ColumnInfo(name = "school_short")
val schoolShortName: String,
@ColumnInfo(name = "school_name")
@ -73,4 +79,6 @@ data class Student(
@PrimaryKey(autoGenerate = true)
var id: Long = 0
var nick = ""
}

View File

@ -0,0 +1,85 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.PrimaryKey
import io.github.wulkanowy.data.enums.Gender
import java.io.Serializable
import java.time.LocalDate
@Entity(tableName = "StudentInfo")
data class StudentInfo(
@ColumnInfo(name = "student_id")
val studentId: Int,
@ColumnInfo(name = "full_name")
val fullName: String,
@ColumnInfo(name = "first_name")
val firstName: String,
@ColumnInfo(name = "second_name")
val secondName: String,
val surname: String,
@ColumnInfo(name = "birth_date")
val birthDate: LocalDate,
@ColumnInfo(name = "birth_place")
val birthPlace: String,
val gender: Gender,
@ColumnInfo(name = "has_polish_citizenship")
val hasPolishCitizenship: Boolean,
@ColumnInfo(name = "family_name")
val familyName: String,
@ColumnInfo(name = "parents_names")
val parentsNames: String,
val address: String,
@ColumnInfo(name = "registered_address")
val registeredAddress: String,
@ColumnInfo(name = "correspondence_address")
val correspondenceAddress: String,
@ColumnInfo(name = "phone_number")
val phoneNumber: String,
@ColumnInfo(name = "cell_phone_number")
val cellPhoneNumber: String,
val email: String,
@Embedded(prefix = "first_guardian_")
val firstGuardian: StudentGuardian,
@Embedded(prefix = "second_guardian_")
val secondGuardian: StudentGuardian
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}
data class StudentGuardian(
@ColumnInfo(name = "full_name")
val fullName: String,
val kinship: String,
val address: String,
val phones: String,
val email: String
) : Serializable

View File

@ -0,0 +1,16 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable
@Entity
data class StudentNick(
val nick: String
) : Serializable {
@PrimaryKey
var id: Long = 0
}

View File

@ -0,0 +1,42 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration31 : Migration(30, 31) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""CREATE TABLE IF NOT EXISTS StudentInfo (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
student_id INTEGER NOT NULL,
full_name TEXT NOT NULL,
first_name TEXT NOT NULL,
second_name TEXT NOT NULL,
surname TEXT NOT NULL,
birth_date INTEGER NOT NULL,
birth_place TEXT NOT NULL,
gender TEXT NOT NULL,
has_polish_citizenship INTEGER NOT NULL,
family_name TEXT NOT NULL,
parents_names TEXT NOT NULL,
address TEXT NOT NULL,
registered_address TEXT NOT NULL,
correspondence_address TEXT NOT NULL,
phone_number TEXT NOT NULL,
cell_phone_number TEXT NOT NULL,
email TEXT NOT NULL,
first_guardian_full_name TEXT NOT NULL,
first_guardian_kinship TEXT NOT NULL,
first_guardian_address TEXT NOT NULL,
first_guardian_phones TEXT NOT NULL,
first_guardian_email TEXT NOT NULL,
second_guardian_full_name TEXT NOT NULL,
second_guardian_kinship TEXT NOT NULL,
second_guardian_address TEXT NOT NULL,
second_guardian_phones TEXT NOT NULL,
second_guardian_email TEXT NOT NULL)
"""
)
}
}

View File

@ -0,0 +1,12 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration32 : Migration(31, 32) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE Students ADD COLUMN nick TEXT NOT NULL DEFAULT \"\"")
}
}

View File

@ -0,0 +1,3 @@
package io.github.wulkanowy.data.enums
enum class Gender { MALE, FEMALE }

View File

@ -0,0 +1,38 @@
package io.github.wulkanowy.data.mappers
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.StudentGuardian
import io.github.wulkanowy.data.db.entities.StudentInfo
import io.github.wulkanowy.data.enums.Gender
import io.github.wulkanowy.sdk.pojo.StudentGuardian as SdkStudentGuardian
import io.github.wulkanowy.sdk.pojo.StudentInfo as SdkStudentInfo
fun SdkStudentInfo.mapToEntity(semester: Semester) = StudentInfo(
studentId = semester.studentId,
fullName = fullName,
firstName = firstName,
secondName = secondName,
surname = surname,
birthDate = birthDate,
birthPlace = birthPlace,
gender = Gender.valueOf(gender.name),
hasPolishCitizenship = hasPolishCitizenship,
familyName = familyName,
parentsNames = parentsNames,
address = address,
registeredAddress = registeredAddress,
correspondenceAddress = correspondenceAddress,
phoneNumber = phoneNumber,
cellPhoneNumber = phoneNumber,
email = email,
firstGuardian = guardians[0].mapToEntity(),
secondGuardian = guardians[1].mapToEntity()
)
fun SdkStudentGuardian.mapToEntity() = StudentGuardian(
fullName = fullName,
kinship = kinship,
address = address,
phones = phones,
email = email
)

View File

@ -16,16 +16,23 @@ class SchoolRepository @Inject constructor(
private val sdk: Sdk
) {
fun getSchoolInfo(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource(
shouldFetch = { it == null || forceRefresh },
query = { schoolDb.load(semester.studentId, semester.classId) },
fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).getSchool().mapToEntity(semester) },
saveFetchResult = { old, new ->
if (new != old && old != null) {
schoolDb.deleteAll(listOf(old))
schoolDb.insertAll(listOf(new))
fun getSchoolInfo(student: Student, semester: Semester, forceRefresh: Boolean) =
networkBoundResource(
shouldFetch = { it == null || forceRefresh },
query = { schoolDb.load(semester.studentId, semester.classId) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).getSchool()
.mapToEntity(semester)
},
saveFetchResult = { old, new ->
if (old != null && new != old) {
with(schoolDb) {
deleteAll(listOf(old))
insertAll(listOf(new))
}
} else if (old == null) {
schoolDb.insertAll(listOf(new))
}
}
schoolDb.insertAll(listOf(new))
}
)
)
}

View File

@ -0,0 +1,38 @@
package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.db.dao.StudentInfoDao
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.mappers.mapToEntity
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.init
import io.github.wulkanowy.utils.networkBoundResource
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class StudentInfoRepository @Inject constructor(
private val studentInfoDao: StudentInfoDao,
private val sdk: Sdk
) {
fun getStudentInfo(student: Student, semester: Semester, forceRefresh: Boolean) =
networkBoundResource(
shouldFetch = { it == null || forceRefresh },
query = { studentInfoDao.loadStudentInfo(student.studentId) },
fetch = {
sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear)
.getStudentInfo().mapToEntity(semester)
},
saveFetchResult = { old, new ->
if (old != null && new != old) {
with(studentInfoDao) {
deleteAll(listOf(old))
insertAll(listOf(new))
}
} else if (old == null) {
studentInfoDao.insertAll(listOf(new))
}
}
)
}

View File

@ -5,6 +5,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentNick
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.data.mappers.mapToEntities
@ -25,49 +26,70 @@ class StudentRepository @Inject constructor(
private val sdk: Sdk
) {
suspend fun isStudentSaved(): Boolean = getSavedStudents(false).isNotEmpty()
suspend fun isStudentSaved() = getSavedStudents(false).isNotEmpty()
suspend fun isCurrentStudentSet(): Boolean = studentDb.loadCurrent()?.isCurrent ?: false
suspend fun isCurrentStudentSet() = studentDb.loadCurrent()?.isCurrent ?: false
suspend fun getStudentsApi(pin: String, symbol: String, token: String): List<StudentWithSemesters> {
return sdk.getStudentsFromMobileApi(token, pin, symbol, "").mapToEntities()
}
suspend fun getStudentsApi(
pin: String,
symbol: String,
token: String
): List<StudentWithSemesters> =
sdk.getStudentsFromMobileApi(token, pin, symbol, "").mapToEntities()
suspend fun getStudentsScrapper(email: String, password: String, scrapperBaseUrl: String, symbol: String): List<StudentWithSemesters> {
return sdk.getStudentsFromScrapper(email, password, scrapperBaseUrl, symbol).mapToEntities(password)
}
suspend fun getStudentsScrapper(
email: String,
password: String,
scrapperBaseUrl: String,
symbol: String
): List<StudentWithSemesters> =
sdk.getStudentsFromScrapper(email, password, scrapperBaseUrl, symbol)
.mapToEntities(password)
suspend fun getStudentsHybrid(email: String, password: String, scrapperBaseUrl: String, symbol: String): List<StudentWithSemesters> {
return sdk.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol).mapToEntities(password)
}
suspend fun getStudentsHybrid(
email: String,
password: String,
scrapperBaseUrl: String,
symbol: String
): List<StudentWithSemesters> =
sdk.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol).mapToEntities(password)
suspend fun getSavedStudents(decryptPass: Boolean = true) = withContext(dispatchers.backgroundThread) {
studentDb.loadStudentsWithSemesters().map {
it.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) student.password = decrypt(student.password)
suspend fun getSavedStudents(decryptPass: Boolean = true) =
withContext(dispatchers.backgroundThread) {
studentDb.loadStudentsWithSemesters().map {
it.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
student.password = decrypt(student.password)
}
}
}
}
}
suspend fun getStudentById(id: Int) = withContext(dispatchers.backgroundThread) {
studentDb.loadById(id)?.apply {
if (Sdk.Mode.valueOf(loginMode) != Sdk.Mode.API) password = decrypt(password)
if (Sdk.Mode.valueOf(loginMode) != Sdk.Mode.API) {
password = decrypt(password)
}
}
} ?: throw NoCurrentStudentException()
suspend fun getCurrentStudent(decryptPass: Boolean = true) = withContext(dispatchers.backgroundThread) {
studentDb.loadCurrent()?.apply {
if (decryptPass && Sdk.Mode.valueOf(loginMode) != Sdk.Mode.API) password = decrypt(password)
}
} ?: throw NoCurrentStudentException()
suspend fun getCurrentStudent(decryptPass: Boolean = true) =
withContext(dispatchers.backgroundThread) {
studentDb.loadCurrent()?.apply {
if (decryptPass && Sdk.Mode.valueOf(loginMode) != Sdk.Mode.API) {
password = decrypt(password)
}
}
} ?: throw NoCurrentStudentException()
suspend fun saveStudents(studentsWithSemesters: List<StudentWithSemesters>): List<Long> {
semesterDb.insertSemesters(studentsWithSemesters.flatMap { it.semesters })
return withContext(dispatchers.backgroundThread) {
studentDb.insertAll(studentsWithSemesters.map { it.student }.map {
if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) it.copy(password = encrypt(it.password, context))
else it
if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) {
it.copy(password = encrypt(it.password, context))
} else it
})
}
}
@ -79,7 +101,7 @@ class StudentRepository @Inject constructor(
}
}
suspend fun logoutStudent(student: Student) {
studentDb.delete(student)
}
suspend fun logoutStudent(student: Student) = studentDb.delete(student)
suspend fun updateStudentNick(studentNick: StudentNick) = studentDb.update(studentNick)
}

View File

@ -27,6 +27,7 @@ import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companio
import io.github.wulkanowy.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_NAME
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.DispatchersProvider
import io.github.wulkanowy.utils.nickOrName
import io.github.wulkanowy.utils.toTimestamp
import kotlinx.coroutines.withContext
import timber.log.Timber
@ -41,17 +42,23 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
private val dispatchersProvider: DispatchersProvider,
) {
private fun getRequestCode(time: LocalDateTime, studentId: Int) = (time.toTimestamp() * studentId).toInt()
private fun getRequestCode(time: LocalDateTime, studentId: Int) =
(time.toTimestamp() * studentId).toInt()
private fun getUpcomingLessonTime(index: Int, day: List<Timetable>, lesson: Timetable): LocalDateTime {
return day.getOrNull(index - 1)?.end ?: lesson.start.minusMinutes(30)
}
private fun getUpcomingLessonTime(
index: Int,
day: List<Timetable>,
lesson: Timetable
) = day.getOrNull(index - 1)?.end ?: lesson.start.minusMinutes(30)
suspend fun cancelScheduled(lessons: List<Timetable>, studentId: Int = 1) {
withContext(dispatchersProvider.backgroundThread) {
lessons.sortedBy { it.start }.forEachIndexed { index, lesson ->
val upcomingTime = getUpcomingLessonTime(index, lessons, lesson)
cancelScheduledTo(upcomingTime..lesson.start, getRequestCode(upcomingTime, studentId))
cancelScheduledTo(
upcomingTime..lesson.start,
getRequestCode(upcomingTime, studentId)
)
cancelScheduledTo(lesson.start..lesson.end, getRequestCode(lesson.start, studentId))
Timber.d("TimetableNotification canceled: type 1 & 2, subject: ${lesson.subject}, start: ${lesson.start}, student: $studentId")
@ -61,13 +68,18 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
private fun cancelScheduledTo(range: ClosedRange<LocalDateTime>, requestCode: Int) {
if (now() in range) cancelNotification()
alarmManager.cancel(PendingIntent.getBroadcast(context, requestCode, Intent(), FLAG_UPDATE_CURRENT))
alarmManager.cancel(
PendingIntent.getBroadcast(context, requestCode, Intent(), FLAG_UPDATE_CURRENT)
)
}
fun cancelNotification() = NotificationManagerCompat.from(context).cancel(MainView.Section.TIMETABLE.id)
fun cancelNotification() =
NotificationManagerCompat.from(context).cancel(MainView.Section.TIMETABLE.id)
suspend fun scheduleNotifications(lessons: List<Timetable>, student: Student) {
if (!preferencesRepository.isUpcomingLessonsNotificationsEnable) return cancelScheduled(lessons, student.studentId)
if (!preferencesRepository.isUpcomingLessonsNotificationsEnable) {
return cancelScheduled(lessons, student.studentId)
}
withContext(dispatchersProvider.backgroundThread) {
lessons.groupBy { it.date }
@ -82,13 +94,28 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
val intent = createIntent(student, lesson, active.getOrNull(index + 1))
if (lesson.start > now()) {
scheduleBroadcast(intent, student.studentId, NOTIFICATION_TYPE_UPCOMING, getUpcomingLessonTime(index, active, lesson))
scheduleBroadcast(
intent,
student.studentId,
NOTIFICATION_TYPE_UPCOMING,
getUpcomingLessonTime(index, active, lesson)
)
}
if (lesson.end > now()) {
scheduleBroadcast(intent, student.studentId, NOTIFICATION_TYPE_CURRENT, lesson.start)
scheduleBroadcast(
intent,
student.studentId,
NOTIFICATION_TYPE_CURRENT,
lesson.start
)
if (active.lastIndex == index) {
scheduleBroadcast(intent, student.studentId, NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION, lesson.end)
scheduleBroadcast(
intent,
student.studentId,
NOTIFICATION_TYPE_LAST_LESSON_CANCELLATION,
lesson.end
)
}
}
}
@ -99,7 +126,7 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
private fun createIntent(student: Student, lesson: Timetable, nextLesson: Timetable?): Intent {
return Intent(context, TimetableNotificationReceiver::class.java).apply {
putExtra(STUDENT_ID, student.studentId)
putExtra(STUDENT_NAME, student.studentName)
putExtra(STUDENT_NAME, student.nickOrName)
putExtra(LESSON_ROOM, lesson.room)
putExtra(LESSON_START, lesson.start.toTimestamp())
putExtra(LESSON_END, lesson.end.toTimestamp())
@ -109,13 +136,23 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
}
}
private fun scheduleBroadcast(intent: Intent, studentId: Int, notificationType: Int, time: LocalDateTime) {
AlarmManagerCompat.setExactAndAllowWhileIdle(alarmManager, RTC_WAKEUP, time.toTimestamp(),
private fun scheduleBroadcast(
intent: Intent,
studentId: Int,
notificationType: Int,
time: LocalDateTime
) {
AlarmManagerCompat.setExactAndAllowWhileIdle(
alarmManager, RTC_WAKEUP, time.toTimestamp(),
PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also {
it.putExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id)
it.putExtra(LESSON_TYPE, notificationType)
}, FLAG_UPDATE_CURRENT)
)
Timber.d("TimetableNotification scheduled: type: $notificationType, subject: ${intent.getStringExtra(LESSON_TITLE)}, start: $time, student: $studentId")
Timber.d(
"TimetableNotification scheduled: type: $notificationType, subject: ${
intent.getStringExtra(LESSON_TITLE)
}, start: $time, student: $studentId"
)
}
}

View File

@ -5,11 +5,12 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.BigTextStyle
import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT
import androidx.core.app.NotificationManagerCompat
import androidx.hilt.Assisted
import androidx.hilt.work.WorkerInject
import androidx.hilt.work.HiltWorker
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.WorkerParameters
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
@ -23,7 +24,8 @@ import kotlinx.coroutines.coroutineScope
import timber.log.Timber
import kotlin.random.Random
class SyncWorker @WorkerInject constructor(
@HiltWorker
class SyncWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted workerParameters: WorkerParameters,
private val studentRepository: StudentRepository,
@ -58,9 +60,10 @@ class SyncWorker @WorkerInject constructor(
}
val result = when {
exceptions.isNotEmpty() && inputData.getBoolean("one_time", false) -> {
Result.failure(Data.Builder()
.putString("error", exceptions.map { it.stackTraceToString() }.toString())
.build()
Result.failure(
Data.Builder()
.putString("error", exceptions.map { it.stackTraceToString() }.toString())
.build()
)
}
exceptions.isNotEmpty() -> Result.retry()
@ -74,13 +77,16 @@ class SyncWorker @WorkerInject constructor(
}
private fun notify(result: Result) {
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), NotificationCompat.Builder(applicationContext, DebugChannel.CHANNEL_ID)
.setContentTitle("Debug notification")
.setSmallIcon(R.drawable.ic_stat_push)
.setAutoCancel(true)
.setColor(applicationContext.getCompatColor(R.color.colorPrimary))
.setStyle(BigTextStyle().bigText("${SyncWorker::class.java.simpleName} result: $result"))
.setPriority(PRIORITY_DEFAULT)
.build())
notificationManager.notify(
Random.nextInt(Int.MAX_VALUE),
NotificationCompat.Builder(applicationContext, DebugChannel.CHANNEL_ID)
.setContentTitle("Debug notification")
.setSmallIcon(R.drawable.ic_stat_push)
.setAutoCancel(true)
.setColor(applicationContext.getCompatColor(R.color.colorPrimary))
.setStyle(BigTextStyle().bigText("${SyncWorker::class.java.simpleName} result: $result"))
.setPriority(PRIORITY_DEFAULT)
.build()
)
}
}

View File

@ -57,7 +57,7 @@ class ErrorDialog : BaseDialogFragment<DialogErrorBinding>() {
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return DialogErrorBinding.inflate(inflater).apply { binding = this }.root
}
@ -114,11 +114,17 @@ class ErrorDialog : BaseDialogFragment<DialogErrorBinding>() {
chooserTitle = getString(R.string.about_feedback),
email = "wulkanowyinc@gmail.com",
subject = "Zgłoszenie błędu",
body = requireContext().getString(R.string.about_feedback_template,
"${appInfo.systemManufacturer} ${appInfo.systemModel}", appInfo.systemVersion.toString(), appInfo.versionName
body = requireContext().getString(
R.string.about_feedback_template,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
"${appInfo.versionName}-${appInfo.buildFlavor}"
) + "\n" + content,
onActivityNotFound = {
requireContext().openInternetBrowser("https://github.com/wulkanowy/wulkanowy/issues", ::showMessage)
requireContext().openInternetBrowser(
"https://github.com/wulkanowy/wulkanowy/issues",
::showMessage
)
}
)
}

View File

@ -9,6 +9,7 @@ import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.databinding.ItemAccountBinding
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.nickOrName
import javax.inject.Inject
class WidgetConfigureAdapter @Inject constructor() : RecyclerView.Adapter<WidgetConfigureAdapter.ItemViewHolder>() {
@ -28,7 +29,7 @@ class WidgetConfigureAdapter @Inject constructor() : RecyclerView.Adapter<Widget
val (student, isCurrent) = items[position]
with(holder.binding) {
accountItemName.text = "${student.studentName} ${student.className}"
accountItemName.text = "${student.nickOrName} ${student.className}"
accountItemSchool.text = student.schoolName
with(accountItemImage) {

View File

@ -18,6 +18,8 @@ import io.github.wulkanowy.utils.getCompatDrawable
import io.github.wulkanowy.utils.openAppInMarket
import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.toFormattedString
import io.github.wulkanowy.utils.toLocalDateTime
import javax.inject.Inject
@AndroidEntryPoint
@ -35,7 +37,9 @@ class AboutFragment : BaseFragment<FragmentAboutBinding>(R.layout.fragment_about
override val versionRes: Triple<String, String, Drawable?>?
get() = context?.run {
Triple(getString(R.string.about_version), "${appInfo.versionName} (${appInfo.versionCode})", getCompatDrawable(R.drawable.ic_all_about))
val buildTimestamp = appInfo.buildTimestamp.toLocalDateTime().toFormattedString("yyyy-MM-dd")
val versionSignature = "${appInfo.versionName}-${appInfo.buildFlavor} (${appInfo.versionCode}), $buildTimestamp"
Triple(getString(R.string.about_version), versionSignature, getCompatDrawable(R.drawable.ic_all_about))
}
override val creatorsRes: Triple<String, String, Drawable?>?
@ -65,7 +69,11 @@ class AboutFragment : BaseFragment<FragmentAboutBinding>(R.layout.fragment_about
override val homepageRes: Triple<String, String, Drawable?>?
get() = context?.run {
Triple(getString(R.string.about_homepage), getString(R.string.about_homepage_summary), getCompatDrawable(R.drawable.ic_about_homepage))
Triple(
getString(R.string.about_homepage),
getString(R.string.about_homepage_summary),
getCompatDrawable(R.drawable.ic_all_home)
)
}
override val licensesRes: Triple<String, String, Drawable?>?
@ -131,11 +139,17 @@ class AboutFragment : BaseFragment<FragmentAboutBinding>(R.layout.fragment_about
chooserTitle = getString(R.string.about_feedback),
email = "wulkanowyinc@gmail.com",
subject = "Zgłoszenie błędu",
body = getString(R.string.about_feedback_template,
"${appInfo.systemManufacturer} ${appInfo.systemModel}", appInfo.systemVersion.toString(), appInfo.versionName
body = getString(
R.string.about_feedback_template,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
"${appInfo.versionName}-${appInfo.buildFlavor}"
),
onActivityNotFound = {
requireContext().openInternetBrowser("https://github.com/wulkanowy/wulkanowy/issues", ::showMessage)
requireContext().openInternetBrowser(
"https://github.com/wulkanowy/wulkanowy/issues",
::showMessage
)
}
)
}

View File

@ -8,16 +8,17 @@ import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.databinding.HeaderAccountBinding
import io.github.wulkanowy.databinding.ItemAccountBinding
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.nickOrName
import javax.inject.Inject
class AccountAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var isAccountQuickDialogMode = false
var items = emptyList<AccountItem<*>>()
var onClickListener: (StudentWithSemesters) -> Unit = {}
@ -30,54 +31,69 @@ class AccountAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.V
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
AccountItem.ViewType.HEADER.id -> HeaderViewHolder(HeaderAccountBinding.inflate(inflater, parent, false))
AccountItem.ViewType.ITEM.id -> ItemViewHolder(ItemAccountBinding.inflate(inflater, parent, false))
AccountItem.ViewType.HEADER.id -> HeaderViewHolder(
HeaderAccountBinding.inflate(inflater, parent, false)
)
AccountItem.ViewType.ITEM.id -> ItemViewHolder(
ItemAccountBinding.inflate(inflater, parent, false)
)
else -> throw IllegalStateException()
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is HeaderViewHolder -> bindHeaderViewHolder(holder.binding, items[position].value as Account)
is ItemViewHolder -> bindItemViewHolder(holder.binding, items[position].value as StudentWithSemesters)
is HeaderViewHolder -> bindHeaderViewHolder(
holder.binding,
items[position].value as Account,
position
)
is ItemViewHolder -> bindItemViewHolder(
holder.binding,
items[position].value as StudentWithSemesters
)
}
}
private fun bindHeaderViewHolder(binding: HeaderAccountBinding, account: Account) {
private fun bindHeaderViewHolder(
binding: HeaderAccountBinding,
account: Account,
position: Int
) {
with(binding) {
accountHeaderDivider.visibility = if (position == 0) GONE else VISIBLE
accountHeaderEmail.text = account.email
accountHeaderType.setText(if (account.isParent) R.string.account_type_parent else R.string.account_type_student)
}
}
@SuppressLint("SetTextI18n")
private fun bindItemViewHolder(binding: ItemAccountBinding, studentWithSemesters: StudentWithSemesters) {
private fun bindItemViewHolder(
binding: ItemAccountBinding,
studentWithSemesters: StudentWithSemesters
) {
val student = studentWithSemesters.student
val semesters = studentWithSemesters.semesters
val diary = semesters.maxByOrNull { it.semesterId }
val isDuplicatedStudent = items.filter {
if (it.value !is StudentWithSemesters) return@filter false
val studentToCompare = it.value.student
studentToCompare.studentId == student.studentId
&& studentToCompare.schoolSymbol == student.schoolSymbol
&& studentToCompare.symbol == student.symbol
}.size > 1 && isAccountQuickDialogMode
with(binding) {
accountItemName.text = "${student.studentName} ${diary?.diaryName.orEmpty()}"
accountItemName.text = "${student.nickOrName} ${diary?.diaryName.orEmpty()}"
accountItemSchool.text = studentWithSemesters.student.schoolName
with(accountItemLoginMode) {
visibility = when (Sdk.Mode.valueOf(student.loginMode)) {
Sdk.Mode.API -> {
setText(R.string.account_login_mobile_api)
VISIBLE
}
Sdk.Mode.HYBRID -> {
setText(R.string.account_login_hybrid)
VISIBLE
}
Sdk.Mode.SCRAPPER -> {
GONE
}
}
}
accountItemAccountType.setText(if (student.isParent) R.string.account_type_parent else R.string.account_type_student)
accountItemAccountType.visibility = if (isDuplicatedStudent) VISIBLE else GONE
with(accountItemImage) {
val colorImage = if (student.isCurrent) context.getThemeAttrColor(R.attr.colorPrimary)
else context.getThemeAttrColor(R.attr.colorOnSurface, 153)
val colorImage =
if (student.isCurrent) context.getThemeAttrColor(R.attr.colorPrimary)
else context.getThemeAttrColor(R.attr.colorOnSurface, 153)
setColorFilter(colorImage, PorterDuff.Mode.SRC_IN)
}

View File

@ -1,102 +0,0 @@
package io.github.wulkanowy.ui.modules.account
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import android.widget.Toast.LENGTH_LONG
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.DialogAccountBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity
import javax.inject.Inject
@AndroidEntryPoint
class AccountDialog : BaseDialogFragment<DialogAccountBinding>(), AccountView {
@Inject
lateinit var presenter: AccountPresenter
@Inject
lateinit var accountAdapter: AccountAdapter
companion object {
fun newInstance() = AccountDialog()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return DialogAccountBinding.inflate(inflater).apply { binding = this }.root
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.onAttachView(this)
}
override fun initView() {
accountAdapter.onClickListener = presenter::onItemSelected
with(binding) {
accountDialogAdd.setOnClickListener { presenter.onAddSelected() }
accountDialogRemove.setOnClickListener { presenter.onRemoveSelected() }
accountDialogRecycler.apply {
layoutManager = LinearLayoutManager(context)
adapter = accountAdapter
}
}
}
override fun updateData(data: List<AccountItem<*>>) {
with(accountAdapter) {
items = data
notifyDataSetChanged()
}
}
override fun showError(text: String, error: Throwable) {
showMessage(text)
}
override fun showMessage(text: String) {
Toast.makeText(context, text, LENGTH_LONG).show()
}
override fun dismissView() {
dismiss()
}
override fun openLoginView() {
activity?.let {
startActivity(LoginActivity.getStartIntent(it))
}
}
override fun showConfirmDialog() {
context?.let {
AlertDialog.Builder(it)
.setTitle(R.string.account_logout_student)
.setMessage(R.string.account_confirm)
.setPositiveButton(R.string.account_logout) { _, _ -> presenter.onLogoutConfirm() }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
}
override fun recreateMainView() {
activity?.recreate()
}
override fun onDestroy() {
presenter.onDetachView()
super.onDestroy()
}
}

View File

@ -0,0 +1,111 @@
package io.github.wulkanowy.ui.modules.account
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import androidx.core.view.get
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.databinding.FragmentAccountBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsFragment
import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import javax.inject.Inject
@AndroidEntryPoint
class AccountFragment : BaseFragment<FragmentAccountBinding>(R.layout.fragment_account),
AccountView, MainView.TitledView {
@Inject
lateinit var presenter: AccountPresenter
@Inject
lateinit var accountAdapter: AccountAdapter
companion object {
fun newInstance() = AccountFragment()
}
override val titleStringId = R.string.account_title
override var subtitleString = ""
override val isViewEmpty get() = accountAdapter.items.isEmpty()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentAccountBinding.bind(view)
presenter.onAttachView(this)
}
override fun initView() {
binding.accountErrorRetry.setOnClickListener { presenter.onRetry() }
binding.accountErrorDetails.setOnClickListener { presenter.onDetailsClick() }
binding.accountRecycler.apply {
layoutManager = LinearLayoutManager(context)
adapter = accountAdapter
}
accountAdapter.onClickListener = presenter::onItemSelected
with(binding) {
accountAdd.setOnClickListener { presenter.onAddSelected() }
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
menu[0].isVisible = false
}
override fun updateData(data: List<AccountItem<*>>) {
with(accountAdapter) {
items = data
notifyDataSetChanged()
}
}
override fun openLoginView() {
activity?.let {
startActivity(LoginActivity.getStartIntent(it))
}
}
override fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters) {
(activity as? MainActivity)?.pushView(
AccountDetailsFragment.newInstance(
studentWithSemesters
)
)
}
override fun showErrorView(show: Boolean) {
binding.accountError.visibility = if (show) View.VISIBLE else View.GONE
}
override fun setErrorDetails(message: String) {
binding.accountErrorMessage.text = message
}
override fun showProgress(show: Boolean) {
binding.accountProgress.visibility = if (show) View.VISIBLE else View.GONE
}
override fun showContent(show: Boolean) {
with(binding) {
accountRecycler.visibility = if (show) View.VISIBLE else View.GONE
accountAdd.visibility = if (show) View.VISIBLE else View.GONE
}
}
}

View File

@ -3,7 +3,6 @@ package io.github.wulkanowy.ui.modules.account
import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.afterLoading
@ -15,101 +14,91 @@ import javax.inject.Inject
class AccountPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val syncManager: SyncManager
) : BasePresenter<AccountView>(errorHandler, studentRepository) {
private lateinit var lastError: Throwable
override fun onAttachView(view: AccountView) {
super.onAttachView(view)
view.initView()
Timber.i("Account dialog view was initialized")
Timber.i("Account view was initialized")
errorHandler.showErrorMessage = ::showErrorViewOnError
loadData()
}
fun onRetry() {
view?.run {
showErrorView(false)
showProgress(true)
}
loadData()
}
fun onDetailsClick() {
view?.showErrorDetailsDialog(lastError)
}
fun onAddSelected() {
Timber.i("Select add account")
view?.openLoginView()
}
fun onRemoveSelected() {
Timber.i("Select remove account")
view?.showConfirmDialog()
}
fun onLogoutConfirm() {
flowWithResource {
val student = studentRepository.getCurrentStudent(false)
studentRepository.logoutStudent(student)
val students = studentRepository.getSavedStudents(false)
if (students.isNotEmpty()) {
studentRepository.switchStudent(students[0])
}
students
}.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Attempt to logout current user ")
Status.SUCCESS -> view?.run {
if (it.data!!.isEmpty()) {
Timber.i("Logout result: Open login view")
syncManager.stopSyncWorker()
openClearLoginView()
} else {
Timber.i("Logout result: Switch to another student")
recreateMainView()
}
}
Status.ERROR -> {
Timber.i("Logout result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}.afterLoading {
view?.dismissView()
}.launch("logout")
}
fun onItemSelected(studentWithSemesters: StudentWithSemesters) {
Timber.i("Select student item ${studentWithSemesters.student.id}")
if (studentWithSemesters.student.isCurrent) {
view?.dismissView()
} else flowWithResource { studentRepository.switchStudent(studentWithSemesters) }.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Attempt to change a student")
Status.SUCCESS -> {
Timber.i("Change a student result: Success")
view?.recreateMainView()
}
Status.ERROR -> {
Timber.i("Change a student result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}.afterLoading {
view?.dismissView()
}.launch("switch")
view?.openAccountDetailsView(studentWithSemesters)
}
private fun createAccountItems(items: List<StudentWithSemesters>): List<AccountItem<*>> {
return items.groupBy { Account(it.student.email, it.student.isParent) }.map { (account, students) ->
listOf(AccountItem(account, AccountItem.ViewType.HEADER)) + students.map { student ->
AccountItem(student, AccountItem.ViewType.ITEM)
return items.groupBy {
Account("${it.student.userName} (${it.student.email})", it.student.isParent)
}
.map { (account, students) ->
listOf(
AccountItem(account, AccountItem.ViewType.HEADER)
) + students.map { student ->
AccountItem(student, AccountItem.ViewType.ITEM)
}
}
}.flatten()
.flatten()
}
private fun loadData() {
flowWithResource { studentRepository.getSavedStudents(false) }.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Loading account data started")
Status.SUCCESS -> {
Timber.i("Loading account result: Success")
view?.updateData(createAccountItems(it.data!!))
}
Status.ERROR -> {
Timber.i("Loading account result: An exception occurred")
errorHandler.dispatch(it.error!!)
flowWithResource { studentRepository.getSavedStudents(false) }
.onEach {
when (it.status) {
Status.LOADING -> {
Timber.i("Loading account data started")
view?.run {
showProgress(true)
showContent(false)
}
}
Status.SUCCESS -> {
Timber.i("Loading account result: Success")
view?.updateData(createAccountItems(it.data!!))
view?.run {
showContent(true)
showErrorView(false)
}
}
Status.ERROR -> {
Timber.i("Loading account result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}
}.launch()
.afterLoading { view?.showProgress(false) }
.launch()
}
private fun showErrorViewOnError(message: String, error: Throwable) {
view?.run {
if (isViewEmpty) {
lastError = error
setErrorDetails(message)
showErrorView(true)
showContent(false)
showProgress(false)
} else showError(message, error)
}
}
}

View File

@ -1,19 +1,26 @@
package io.github.wulkanowy.ui.modules.account
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.ui.base.BaseView
interface AccountView : BaseView {
val isViewEmpty: Boolean
fun initView()
fun updateData(data: List<AccountItem<*>>)
fun dismissView()
fun showConfirmDialog()
fun openLoginView()
fun recreateMainView()
fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters)
fun showErrorView(show: Boolean)
fun setErrorDetails(message: String)
fun showProgress(show: Boolean)
fun showContent(show: Boolean)
}

View File

@ -0,0 +1,157 @@
package io.github.wulkanowy.ui.modules.account.accountdetails
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.core.view.get
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.databinding.FragmentAccountDetailsBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.account.accountedit.AccountEditDialog
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoFragment
import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView
import io.github.wulkanowy.utils.nickOrName
import javax.inject.Inject
@AndroidEntryPoint
class AccountDetailsFragment :
BaseFragment<FragmentAccountDetailsBinding>(R.layout.fragment_account_details),
AccountDetailsView, MainView.TitledView {
@Inject
lateinit var presenter: AccountDetailsPresenter
override val titleStringId = R.string.account_details_title
override var subtitleString = ""
companion object {
private const val ARGUMENT_KEY = "Data"
fun newInstance(studentWithSemesters: StudentWithSemesters) =
AccountDetailsFragment().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, studentWithSemesters) }
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentAccountDetailsBinding.bind(view)
presenter.onAttachView(this, requireArguments()[ARGUMENT_KEY] as StudentWithSemesters)
}
override fun initView() {
binding.accountDetailsErrorRetry.setOnClickListener { presenter.onRetry() }
binding.accountDetailsErrorDetails.setOnClickListener { presenter.onDetailsClick() }
binding.accountDetailsLogout.setOnClickListener { presenter.onRemoveSelected() }
binding.accountDetailsSelect.setOnClickListener { presenter.onStudentSelect() }
binding.accountDetailsPersonalData.setOnClickListener {
presenter.onStudentInfoSelected(StudentInfoView.Type.PERSONAL)
}
binding.accountDetailsAddressData.setOnClickListener {
presenter.onStudentInfoSelected(StudentInfoView.Type.ADDRESS)
}
binding.accountDetailsContactData.setOnClickListener {
presenter.onStudentInfoSelected(StudentInfoView.Type.CONTACT)
}
binding.accountDetailsFamilyData.setOnClickListener {
presenter.onStudentInfoSelected(StudentInfoView.Type.FAMILY)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
menu[0].isVisible = false
inflater.inflate(R.menu.action_menu_account_details, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.accountDetailsMenuEdit) {
presenter.onAccountEditSelected()
true
} else false
}
override fun showAccountData(student: Student) {
with(binding) {
accountDetailsName.text = student.nickOrName
accountDetailsSchool.text = student.schoolName
}
}
override fun enableSelectStudentButton(enable: Boolean) {
binding.accountDetailsSelect.isEnabled = enable
}
override fun showAccountEditDetailsDialog(student: Student) {
(requireActivity() as MainActivity).showDialogFragment(
AccountEditDialog.newInstance(student)
)
}
override fun showLogoutConfirmDialog() {
context?.let {
AlertDialog.Builder(it)
.setTitle(R.string.account_logout_student)
.setMessage(R.string.account_confirm)
.setPositiveButton(R.string.account_logout) { _, _ -> presenter.onLogoutConfirm() }
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
}
override fun popView() {
(requireActivity() as MainActivity).popView(2)
}
override fun recreateMainView() {
requireActivity().recreate()
}
override fun openStudentInfoView(
infoType: StudentInfoView.Type,
studentWithSemesters: StudentWithSemesters
) {
(requireActivity() as MainActivity).pushView(
StudentInfoFragment.newInstance(
infoType,
studentWithSemesters
)
)
}
override fun showErrorView(show: Boolean) {
binding.accountDetailsError.visibility = if (show) View.VISIBLE else View.GONE
}
override fun setErrorDetails(message: String) {
binding.accountDetailsErrorMessage.text = message
}
override fun showProgress(show: Boolean) {
binding.accountDetailsProgress.visibility = if (show) View.VISIBLE else View.GONE
}
override fun showContent(show: Boolean) {
binding.accountDetailsContent.visibility = if (show) View.VISIBLE else View.GONE
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
}

View File

@ -0,0 +1,172 @@
package io.github.wulkanowy.ui.modules.account.accountdetails
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.services.sync.SyncManager
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView
import io.github.wulkanowy.utils.afterLoading
import io.github.wulkanowy.utils.flowWithResource
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import javax.inject.Inject
class AccountDetailsPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val syncManager: SyncManager
) : BasePresenter<AccountDetailsView>(errorHandler, studentRepository) {
private lateinit var studentWithSemesters: StudentWithSemesters
private lateinit var lastError: Throwable
private var studentId: Long? = null
fun onAttachView(view: AccountDetailsView, studentWithSemesters: StudentWithSemesters) {
super.onAttachView(view)
studentId = studentWithSemesters.student.id
view.initView()
errorHandler.showErrorMessage = ::showErrorViewOnError
Timber.i("Account details view was initialized")
loadData()
}
fun onRetry() {
view?.run {
showErrorView(false)
showProgress(true)
}
loadData()
}
fun onDetailsClick() {
view?.showErrorDetailsDialog(lastError)
}
private fun loadData() {
flowWithResource { studentRepository.getSavedStudents() }
.map { studentWithSemesters ->
Resource(
data = studentWithSemesters.data?.single { it.student.id == studentId },
status = studentWithSemesters.status,
error = studentWithSemesters.error
)
}
.onEach {
when (it.status) {
Status.LOADING -> {
view?.run {
showProgress(true)
showContent(false)
}
Timber.i("Loading account details view started")
}
Status.SUCCESS -> {
Timber.i("Loading account details view result: Success")
studentWithSemesters = it.data!!
view?.run {
showAccountData(studentWithSemesters.student)
enableSelectStudentButton(!studentWithSemesters.student.isCurrent)
showContent(true)
showErrorView(false)
}
}
Status.ERROR -> {
Timber.i("Loading account details view result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}
.afterLoading { view?.showProgress(false) }
.launch()
}
fun onAccountEditSelected() {
view?.showAccountEditDetailsDialog(studentWithSemesters.student)
}
fun onStudentInfoSelected(infoType: StudentInfoView.Type) {
view?.openStudentInfoView(infoType, studentWithSemesters)
}
fun onStudentSelect() {
Timber.i("Select student ${studentWithSemesters.student.id}")
flowWithResource { studentRepository.switchStudent(studentWithSemesters) }
.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Attempt to change a student")
Status.SUCCESS -> {
Timber.i("Change a student result: Success")
view?.recreateMainView()
}
Status.ERROR -> {
Timber.i("Change a student result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}.afterLoading {
view?.popView()
}.launch("switch")
}
fun onRemoveSelected() {
Timber.i("Select remove account")
view?.showLogoutConfirmDialog()
}
fun onLogoutConfirm() {
flowWithResource {
val studentToLogout = studentWithSemesters.student
studentRepository.logoutStudent(studentToLogout)
val students = studentRepository.getSavedStudents(false)
if (studentToLogout.isCurrent && students.isNotEmpty()) {
studentRepository.switchStudent(students[0])
}
return@flowWithResource students
}.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Attempt to logout user")
Status.SUCCESS -> view?.run {
when {
it.data!!.isEmpty() -> {
Timber.i("Logout result: Open login view")
syncManager.stopSyncWorker()
openClearLoginView()
}
studentWithSemesters.student.isCurrent -> {
Timber.i("Logout result: Logout student and switch to another")
recreateMainView()
}
else -> Timber.i("Logout result: Logout student")
}
}
Status.ERROR -> {
Timber.i("Logout result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}.afterLoading {
view?.popView()
}.launch("logout")
}
private fun showErrorViewOnError(message: String, error: Throwable) {
view?.run {
lastError = error
setErrorDetails(message)
showErrorView(true)
showContent(false)
showProgress(false)
}
}
}

View File

@ -0,0 +1,36 @@
package io.github.wulkanowy.ui.modules.account.accountdetails
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView
interface AccountDetailsView : BaseView {
fun initView()
fun showAccountData(student: Student)
fun showAccountEditDetailsDialog(student: Student)
fun showLogoutConfirmDialog()
fun popView()
fun recreateMainView()
fun enableSelectStudentButton(enable: Boolean)
fun openStudentInfoView(
infoType: StudentInfoView.Type,
studentWithSemesters: StudentWithSemesters
)
fun showErrorView(show: Boolean)
fun setErrorDetails(message: String)
fun showProgress(show: Boolean)
fun showContent(show: Boolean)
}

View File

@ -0,0 +1,72 @@
package io.github.wulkanowy.ui.modules.account.accountedit
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.databinding.DialogAccountEditBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment
import javax.inject.Inject
@AndroidEntryPoint
class AccountEditDialog : BaseDialogFragment<DialogAccountEditBinding>(), AccountEditView {
@Inject
lateinit var presenter: AccountEditPresenter
companion object {
private const val ARGUMENT_KEY = "student_with_semesters"
fun newInstance(student: Student) =
AccountEditDialog().apply {
arguments = Bundle().apply {
putSerializable(ARGUMENT_KEY, student)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = DialogAccountEditBinding.inflate(inflater).apply { binding = this }.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
presenter.onAttachView(this, requireArguments()[ARGUMENT_KEY] as Student)
}
override fun initView() {
with(binding) {
accountEditDetailsCancel.setOnClickListener { dismiss() }
accountEditDetailsSave.setOnClickListener {
presenter.changeStudentNick(binding.accountEditDetailsNickText.text.toString())
}
}
}
override fun showCurrentNick(nick: String) {
binding.accountEditDetailsNickText.setText(nick)
}
override fun popView() {
dismiss()
}
override fun recreateMainView() {
activity?.recreate()
}
override fun onDestroyView() {
super.onDestroyView()
presenter.onDetachView()
}
}

View File

@ -0,0 +1,54 @@
package io.github.wulkanowy.ui.modules.account.accountedit
import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentNick
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.afterLoading
import io.github.wulkanowy.utils.flowWithResource
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import javax.inject.Inject
class AccountEditPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository
) : BasePresenter<AccountEditView>(errorHandler, studentRepository) {
lateinit var student: Student
fun onAttachView(view: AccountEditView, student: Student) {
super.onAttachView(view)
this.student = student
with(view) {
initView()
showCurrentNick(student.nick.trim())
}
Timber.i("Account edit dialog view was initialized")
}
fun changeStudentNick(nick: String) {
flowWithResource {
val studentNick =
StudentNick(nick = nick.trim()).apply { id = student.id }
studentRepository.updateStudentNick(studentNick)
}.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Attempt to change a student nick")
Status.SUCCESS -> {
Timber.i("Change a student nick result: Success")
view?.recreateMainView()
}
Status.ERROR -> {
Timber.i("Change a student result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}
.afterLoading { view?.popView() }
.launch()
}
}

View File

@ -0,0 +1,14 @@
package io.github.wulkanowy.ui.modules.account.accountedit
import io.github.wulkanowy.ui.base.BaseView
interface AccountEditView : BaseView {
fun initView()
fun popView()
fun recreateMainView()
fun showCurrentNick(nick: String)
}

View File

@ -0,0 +1,82 @@
package io.github.wulkanowy.ui.modules.account.accountquick
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.databinding.DialogAccountQuickBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.ui.modules.account.AccountAdapter
import io.github.wulkanowy.ui.modules.account.AccountFragment
import io.github.wulkanowy.ui.modules.account.AccountItem
import io.github.wulkanowy.ui.modules.main.MainActivity
import javax.inject.Inject
@AndroidEntryPoint
class AccountQuickDialog : BaseDialogFragment<DialogAccountQuickBinding>(), AccountQuickView {
@Inject
lateinit var accountAdapter: AccountAdapter
@Inject
lateinit var presenter: AccountQuickPresenter
companion object {
fun newInstance() = AccountQuickDialog()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = DialogAccountQuickBinding.inflate(inflater).apply { binding = this }.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
presenter.onAttachView(this)
}
override fun initView() {
binding.accountQuickDialogManger.setOnClickListener { presenter.onManagerSelected() }
with(accountAdapter) {
isAccountQuickDialogMode = true
onClickListener = presenter::onStudentSelect
}
with(binding.accountQuickDialogRecycler) {
layoutManager = LinearLayoutManager(context)
adapter = accountAdapter
}
}
override fun updateData(data: List<AccountItem<*>>) {
with(accountAdapter) {
items = data
notifyDataSetChanged()
}
}
override fun popView() {
dismiss()
}
override fun recreateMainView() {
activity?.recreate()
}
override fun openAccountView() {
(requireActivity() as MainActivity).pushView(AccountFragment.newInstance())
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
}

View File

@ -0,0 +1,79 @@
package io.github.wulkanowy.ui.modules.account.accountquick
import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.account.AccountItem
import io.github.wulkanowy.utils.afterLoading
import io.github.wulkanowy.utils.flowWithResource
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import javax.inject.Inject
class AccountQuickPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository
) : BasePresenter<AccountQuickView>(errorHandler, studentRepository) {
override fun onAttachView(view: AccountQuickView) {
super.onAttachView(view)
view.initView()
Timber.i("Account quick dialog view was initialized")
loadData()
}
fun onManagerSelected() {
view?.run {
openAccountView()
popView()
}
}
fun onStudentSelect(studentWithSemesters: StudentWithSemesters) {
Timber.i("Select student ${studentWithSemesters.student.id}")
if (studentWithSemesters.student.isCurrent) {
view?.popView()
return
}
flowWithResource { studentRepository.switchStudent(studentWithSemesters) }
.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Attempt to change a student")
Status.SUCCESS -> {
Timber.i("Change a student result: Success")
view?.recreateMainView()
}
Status.ERROR -> {
Timber.i("Change a student result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}
.afterLoading { view?.popView() }
.launch("switch")
}
private fun loadData() {
flowWithResource { studentRepository.getSavedStudents(false) }.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Loading account data started")
Status.SUCCESS -> {
Timber.i("Loading account result: Success")
view?.updateData(createAccountItems(it.data!!))
}
Status.ERROR -> {
Timber.i("Loading account result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}.launch()
}
private fun createAccountItems(items: List<StudentWithSemesters>) = items.map {
AccountItem(it, AccountItem.ViewType.ITEM)
}
}

View File

@ -0,0 +1,17 @@
package io.github.wulkanowy.ui.modules.account.accountquick
import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.account.AccountItem
interface AccountQuickView : BaseView {
fun initView()
fun updateData(data: List<AccountItem<*>>)
fun recreateMainView()
fun popView()
fun openAccountView()
}

View File

@ -26,6 +26,7 @@ import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.SchooldaysRangeLimiter
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import java.time.LocalDate
import javax.inject.Inject
@ -60,6 +61,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
override val excuseActionMode: Boolean get() = attendanceAdapter.excuseActionMode
private var actionMode: ActionMode? = null
private val actionModeCallback = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
val inflater = mode.menuInflater
@ -111,6 +113,8 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
with(binding) {
attendanceSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
attendanceSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
attendanceSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
attendanceErrorRetry.setOnClickListener { presenter.onRetry() }
attendanceErrorDetails.setOnClickListener { presenter.onDetailsClick() }
@ -222,6 +226,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
setDateRangeLimiter(SchooldaysRangeLimiter())
version = DatePickerDialog.Version.VERSION_2
scrollOrientation = DatePickerDialog.ScrollOrientation.VERTICAL
vibrate(false)
show(this@AttendanceFragment.parentFragmentManager, null)
}
}

View File

@ -15,6 +15,7 @@ import io.github.wulkanowy.databinding.FragmentAttendanceSummaryBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.setOnItemSelectedListener
import javax.inject.Inject
@ -56,6 +57,8 @@ class AttendanceSummaryFragment :
with(binding) {
attendanceSummarySwipe.setOnRefreshListener(presenter::onSwipeRefresh)
attendanceSummarySwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
attendanceSummarySwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
attendanceSummaryErrorRetry.setOnClickListener { presenter.onRetry() }
attendanceSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}

View File

@ -10,6 +10,7 @@ import io.github.wulkanowy.databinding.FragmentConferenceBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject
@AndroidEntryPoint
@ -47,7 +48,9 @@ class ConferenceFragment : BaseFragment<FragmentConferenceBinding>(R.layout.frag
}
with(binding) {
conferenceSwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
conferenceSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
conferenceSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
conferenceSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
conferenceErrorRetry.setOnClickListener { presenter.onRetry() }
conferenceErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}

View File

@ -15,6 +15,7 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject
@AndroidEntryPoint
@ -55,6 +56,8 @@ class ExamFragment : BaseFragment<FragmentExamBinding>(R.layout.fragment_exam),
with(binding) {
examSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
examSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
examSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
examErrorRetry.setOnClickListener { presenter.onRetry() }
examErrorDetails.setOnClickListener { presenter.onDetailsClick() }

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.modules.grade
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester
@ -33,81 +34,162 @@ class GradeAverageProvider @Inject constructor(
private val minusModifier get() = preferencesRepository.gradeMinusModifier
fun getGradesDetailsWithAverage(student: Student, semesterId: Int, forceRefresh: Boolean) = flowWithResourceIn {
val semesters = semesterRepository.getSemesters(student)
fun getGradesDetailsWithAverage(student: Student, semesterId: Int, forceRefresh: Boolean) =
flowWithResourceIn {
val semesters = semesterRepository.getSemesters(student)
when (preferencesRepository.gradeAverageMode) {
ONE_SEMESTER -> getSemesterDetailsWithAverage(student, semesters.single { it.semesterId == semesterId }, forceRefresh)
BOTH_SEMESTERS -> calculateBothSemestersAverage(student, semesters, semesterId, forceRefresh)
ALL_YEAR -> calculateAllYearAverage(student, semesters, semesterId, forceRefresh)
}
}.distinctUntilChanged()
private fun calculateBothSemestersAverage(student: Student, semesters: List<Semester>, semesterId: Int, forceRefresh: Boolean): Flow<Resource<List<GradeDetailsWithAverage>>> {
val selectedSemester = semesters.single { it.semesterId == semesterId }
val firstSemester = semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 }
val selectedSemesterDetailsWithAverage = getSemesterDetailsWithAverage(student, selectedSemester, forceRefresh)
return if (selectedSemester == firstSemester) selectedSemesterDetailsWithAverage else {
val firstSemesterDetailsWithAverage = getSemesterDetailsWithAverage(student, firstSemester, forceRefresh)
selectedSemesterDetailsWithAverage.combine(firstSemesterDetailsWithAverage) { selectedDetails, secondDetails ->
val isAnyAverage = selectedDetails.data.orEmpty().any { it.average != .0 }
secondDetails.copy(data = selectedDetails.data?.map { selected ->
val second = secondDetails.data.orEmpty().singleOrNull { it.subject == selected.subject }
selected.copy(average = if (!isAnyAverage || preferencesRepository.gradeAverageForceCalc) {
val selectedGrades = selected.grades.updateModifiers(student).calcAverage()
(selectedGrades + (second?.grades?.updateModifiers(student)?.calcAverage() ?: selectedGrades)) / 2
} else (selected.average + (second?.average ?: selected.average)) / 2)
})
}
}
}
private fun calculateAllYearAverage(student: Student, semesters: List<Semester>, semesterId: Int, forceRefresh: Boolean): Flow<Resource<List<GradeDetailsWithAverage>>> {
val selectedSemester = semesters.single { it.semesterId == semesterId }
val firstSemester = semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 }
val selectedSemesterDetailsWithAverage = getSemesterDetailsWithAverage(student, selectedSemester, forceRefresh)
return if (selectedSemester == firstSemester) selectedSemesterDetailsWithAverage else {
val firstSemesterDetailsWithAverage = getSemesterDetailsWithAverage(student, firstSemester, forceRefresh)
selectedSemesterDetailsWithAverage.combine(firstSemesterDetailsWithAverage) { selectedDetails, secondDetails ->
val isAnyAverage = selectedDetails.data.orEmpty().any { it.average != .0 }
secondDetails.copy(data = selectedDetails.data?.map { selected ->
val second = secondDetails.data.orEmpty().singleOrNull { it.subject == selected.subject }
selected.copy(average = if (!isAnyAverage || preferencesRepository.gradeAverageForceCalc) {
(selected.grades.updateModifiers(student) + second?.grades?.updateModifiers(student).orEmpty()).calcAverage()
} else selected.average)
})
}
}
}
private fun getSemesterDetailsWithAverage(student: Student, semester: Semester, forceRefresh: Boolean): Flow<Resource<List<GradeDetailsWithAverage>>> {
return gradeRepository.getGrades(student, semester, forceRefresh = forceRefresh).map { res ->
val (details, summaries) = res.data ?: null to null
val isAnyAverage = summaries.orEmpty().any { it.average != .0 }
val allGrades = details.orEmpty().groupBy { it.subject }
val items = summaries?.emulateEmptySummaries(student, semester, allGrades.toList(), isAnyAverage)?.map { summary ->
val grades = allGrades[summary.subject].orEmpty()
GradeDetailsWithAverage(
subject = summary.subject,
average = if (!isAnyAverage || preferencesRepository.gradeAverageForceCalc) {
grades.updateModifiers(student).calcAverage()
} else summary.average,
points = summary.pointsSum,
summary = summary,
grades = grades
when (preferencesRepository.gradeAverageMode) {
ONE_SEMESTER -> getGradeSubjects(
student = student,
semester = semesters.single { it.semesterId == semesterId },
forceRefresh = forceRefresh
)
BOTH_SEMESTERS -> calculateCombinedAverage(
student = student,
semesters = semesters,
semesterId = semesterId,
forceRefresh = forceRefresh,
averageMode = BOTH_SEMESTERS
)
ALL_YEAR -> calculateCombinedAverage(
student = student,
semesters = semesters,
semesterId = semesterId,
forceRefresh = forceRefresh,
averageMode = ALL_YEAR
)
}
}.distinctUntilChanged()
Resource(res.status, items, res.error)
private fun calculateCombinedAverage(
student: Student,
semesters: List<Semester>,
semesterId: Int,
forceRefresh: Boolean,
averageMode: GradeAverageMode
): Flow<Resource<List<GradeSubject>>> {
val gradeAverageForceCalc = preferencesRepository.gradeAverageForceCalc
val selectedSemester = semesters.single { it.semesterId == semesterId }
val firstSemester =
semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 }
val selectedSemesterGradeSubjects =
getGradeSubjects(student, selectedSemester, forceRefresh)
if (selectedSemester == firstSemester) return selectedSemesterGradeSubjects
val firstSemesterGradeSubjects =
getGradeSubjects(student, firstSemester, forceRefresh)
return selectedSemesterGradeSubjects.combine(firstSemesterGradeSubjects) { secondSemesterGradeSubject, firstSemesterGradeSubject ->
if (firstSemesterGradeSubject.status == Status.ERROR) {
return@combine firstSemesterGradeSubject
}
val isAnyAverage = secondSemesterGradeSubject.data.orEmpty().any { it.average != .0 }
val updatedData = secondSemesterGradeSubject.data?.map { secondSemesterSubject ->
val firstSemesterSubject = firstSemesterGradeSubject.data.orEmpty()
.singleOrNull { it.subject == secondSemesterSubject.subject }
val updatedAverage = if (averageMode == ALL_YEAR) {
calculateAllYearAverage(
student = student,
isAnyAverage = isAnyAverage,
gradeAverageForceCalc = gradeAverageForceCalc,
secondSemesterSubject = secondSemesterSubject,
firstSemesterSubject = firstSemesterSubject
)
} else {
calculateBothSemestersAverage(
student = student,
isAnyAverage = isAnyAverage,
gradeAverageForceCalc = gradeAverageForceCalc,
secondSemesterSubject = secondSemesterSubject,
firstSemesterSubject = firstSemesterSubject
)
}
secondSemesterSubject.copy(average = updatedAverage)
}
secondSemesterGradeSubject.copy(data = updatedData)
}
}
private fun List<GradeSummary>.emulateEmptySummaries(student: Student, semester: Semester, grades: List<Pair<String, List<Grade>>>, calcAverage: Boolean): List<GradeSummary> {
private fun calculateAllYearAverage(
student: Student,
isAnyAverage: Boolean,
gradeAverageForceCalc: Boolean,
secondSemesterSubject: GradeSubject,
firstSemesterSubject: GradeSubject?
) = if (!isAnyAverage || gradeAverageForceCalc) {
val updatedSecondSemesterGrades =
secondSemesterSubject.grades.updateModifiers(student)
val updatedFirstSemesterGrades =
firstSemesterSubject?.grades?.updateModifiers(student).orEmpty()
(updatedSecondSemesterGrades + updatedFirstSemesterGrades).calcAverage()
} else {
secondSemesterSubject.average
}
private fun calculateBothSemestersAverage(
student: Student,
isAnyAverage: Boolean,
gradeAverageForceCalc: Boolean,
secondSemesterSubject: GradeSubject,
firstSemesterSubject: GradeSubject?
) = if (!isAnyAverage || gradeAverageForceCalc) {
val secondSemesterAverage =
secondSemesterSubject.grades.updateModifiers(student).calcAverage()
val firstSemesterAverage = firstSemesterSubject?.grades?.updateModifiers(student)
?.calcAverage() ?: secondSemesterAverage
val divider = if (secondSemesterSubject.grades.any { it.weightValue > .0 }) 2 else 1
(secondSemesterAverage + firstSemesterAverage) / divider
} else {
(secondSemesterSubject.average + (firstSemesterSubject?.average ?: secondSemesterSubject.average)) / 2
}
private fun getGradeSubjects(
student: Student,
semester: Semester,
forceRefresh: Boolean
): Flow<Resource<List<GradeSubject>>> {
val gradeAverageForceCalc = preferencesRepository.gradeAverageForceCalc
return gradeRepository.getGrades(student, semester, forceRefresh = forceRefresh)
.map { res ->
val (details, summaries) = res.data ?: null to null
val isAnyAverage = summaries.orEmpty().any { it.average != .0 }
val allGrades = details.orEmpty().groupBy { it.subject }
val items = summaries?.emulateEmptySummaries(
student,
semester,
allGrades.toList(),
isAnyAverage
)?.map { summary ->
val grades = allGrades[summary.subject].orEmpty()
GradeSubject(
subject = summary.subject,
average = if (!isAnyAverage || gradeAverageForceCalc) {
grades.updateModifiers(student).calcAverage()
} else summary.average,
points = summary.pointsSum,
summary = summary,
grades = grades
)
}
Resource(res.status, items, res.error)
}
}
private fun List<GradeSummary>.emulateEmptySummaries(
student: Student,
semester: Semester,
grades: List<Pair<String, List<Grade>>>,
calcAverage: Boolean
): List<GradeSummary> {
if (isNotEmpty() && size > grades.size) return this
return grades.mapIndexed { i, (subject, details) ->

View File

@ -33,7 +33,6 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
private var semesterSwitchMenu: MenuItem? = null
companion object {
private const val SAVED_SEMESTER_KEY = "CURRENT_SEMESTER"
fun newInstance() = GradeFragment()
}
@ -52,7 +51,7 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentGradeBinding.bind(view)
presenter.onAttachView(this, savedInstanceState?.getInt(SAVED_SEMESTER_KEY))
presenter.onAttachView(this)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -161,11 +160,6 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
(pagerAdapter.getFragmentInstance(index) as? GradeView.GradeChildView)?.onParentChangeSemester()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(SAVED_SEMESTER_KEY, presenter.selectedIndex)
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()

View File

@ -21,8 +21,7 @@ class GradePresenter @Inject constructor(
private val analytics: AnalyticsHelper
) : BasePresenter<GradeView>(errorHandler, studentRepository) {
var selectedIndex = 0
private set
private var selectedIndex = 0
private var schoolYear = 0
@ -32,9 +31,8 @@ class GradePresenter @Inject constructor(
private lateinit var lastError: Throwable
fun onAttachView(view: GradeView, savedIndex: Int?) {
override fun onAttachView(view: GradeView) {
super.onAttachView(view)
selectedIndex = savedIndex ?: 0
view.initView()
Timber.i("Grade view was initialized with $selectedIndex index")
errorHandler.showErrorMessage = ::showErrorViewOnError

View File

@ -3,7 +3,7 @@ package io.github.wulkanowy.ui.modules.grade
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary
data class GradeDetailsWithAverage(
data class GradeSubject(
val subject: String,
val average: Double,
val points: String,

View File

@ -17,6 +17,7 @@ import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.grade.GradeView
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject
@AndroidEntryPoint
@ -65,7 +66,9 @@ class GradeDetailsFragment :
layoutManager = LinearLayoutManager(context)
adapter = gradeDetailsAdapter
}
gradeDetailsSwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
gradeDetailsSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
gradeDetailsSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
gradeDetailsSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
gradeDetailsErrorRetry.setOnClickListener { presenter.onRetry() }
gradeDetailsErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}

View File

@ -10,9 +10,9 @@ import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider
import io.github.wulkanowy.ui.modules.grade.GradeDetailsWithAverage
import io.github.wulkanowy.ui.modules.grade.GradeSortingMode.ALPHABETIC
import io.github.wulkanowy.ui.modules.grade.GradeSortingMode.DATE
import io.github.wulkanowy.ui.modules.grade.GradeSubject
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.afterLoading
import io.github.wulkanowy.utils.flowWithResource
@ -201,8 +201,9 @@ class GradeDetailsPresenter @Inject constructor(
}.launch()
}
private fun updateNewGradesAmount(grades: List<GradeDetailsWithAverage>) {
newGradesAmount = grades.sumBy { item -> item.grades.sumBy { grade -> if (!grade.isRead) 1 else 0 } }
private fun updateNewGradesAmount(grades: List<GradeSubject>) {
newGradesAmount =
grades.sumBy { item -> item.grades.sumBy { grade -> if (!grade.isRead) 1 else 0 } }
}
private fun showErrorViewOnError(message: String, error: Throwable) {
@ -217,7 +218,7 @@ class GradeDetailsPresenter @Inject constructor(
}
@SuppressLint("DefaultLocale")
private fun createGradeItems(items: List<GradeDetailsWithAverage>): List<GradeDetailsItem> {
private fun createGradeItems(items: List<GradeSubject>): List<GradeDetailsItem> {
return items
.let { gradesWithAverages ->
if (!preferencesRepository.showSubjectsWithoutGrades) {

View File

@ -78,18 +78,18 @@ class GradeStatisticsAdapter @Inject constructor() :
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is PartialViewHolder -> bindPartialChart(holder, items[position].partial!!)
is SemesterViewHolder -> bindSemesterChart(holder, items[position].semester!!)
is PointsViewHolder -> bindBarChart(holder, items[position].points!!)
is PartialViewHolder -> bindPartialChart(holder.binding, items[position].partial!!)
is SemesterViewHolder -> bindSemesterChart(holder.binding, items[position].semester!!)
is PointsViewHolder -> bindBarChart(holder.binding, items[position].points!!)
}
}
private fun bindPartialChart(holder: PartialViewHolder, partials: GradePartialStatistics) {
bindPieChart(holder.binding, partials.subject, partials.classAverage, partials.classAmounts)
private fun bindPartialChart(binding: ItemGradeStatisticsPieBinding, partials: GradePartialStatistics) {
bindPieChart(binding, partials.subject, partials.classAverage, partials.classAmounts)
}
private fun bindSemesterChart(holder: SemesterViewHolder, semester: GradeSemesterStatistics) {
bindPieChart(holder.binding, semester.subject, semester.average, semester.amounts)
private fun bindSemesterChart(binding: ItemGradeStatisticsPieBinding, semester: GradeSemesterStatistics) {
bindPieChart(binding, semester.subject, semester.average, semester.amounts)
}
private fun bindPieChart(binding: ItemGradeStatisticsPieBinding, subject: String, average: String, amounts: List<Int>) {
@ -103,9 +103,12 @@ class GradeStatisticsAdapter @Inject constructor() :
else -> materialGradeColors
}
val dataset = PieDataSet(amounts.mapIndexed { grade, amount ->
PieEntry(amount.toFloat(), (grade + 1).toString())
}.reversed().filterNot { it.value == 0f }, "Legenda")
val dataset = PieDataSet(
amounts.mapIndexed { grade, amount ->
PieEntry(amount.toFloat(), (grade + 1).toString())
}.reversed().filterNot { it.value == 0f },
binding.root.context.getString(R.string.grade_statistics_legend)
)
with(dataset) {
valueTextSize = 12f
@ -138,11 +141,13 @@ class GradeStatisticsAdapter @Inject constructor() :
})
}
val numberOfGradesString = amounts.fold(0) { acc, it -> acc + it }
.let { resources.getQuantityString(R.plurals.grade_number_item, it, it) }
val averageString = binding.root.context.getString(R.string.grade_statistics_average, average)
minAngleForSlices = 25f
description.isEnabled = false
centerText = amounts.fold(0) { acc, it -> acc + it }
.let { resources.getQuantityString(R.plurals.grade_number_item, it, it) } +
("\n\nŚrednia: $average").takeIf { average.isNotBlank() }.orEmpty()
centerText = numberOfGradesString + ("\n\n" + averageString).takeIf { average.isNotBlank() }.orEmpty()
setHoleColor(context.getThemeAttrColor(android.R.attr.windowBackground))
setCenterTextColor(context.getThemeAttrColor(android.R.attr.textColorPrimary))
@ -150,8 +155,8 @@ class GradeStatisticsAdapter @Inject constructor() :
}
}
private fun bindBarChart(holder: PointsViewHolder, points: GradePointsStatistics) {
with(holder.binding.gradeStatisticsBarTitle) {
private fun bindBarChart(binding: ItemGradeStatisticsBarBinding, points: GradePointsStatistics) {
with(binding.gradeStatisticsBarTitle) {
text = points.subject
visibility = if (items.size == 1) GONE else VISIBLE
}
@ -159,18 +164,18 @@ class GradeStatisticsAdapter @Inject constructor() :
val dataset = BarDataSet(listOf(
BarEntry(1f, points.others.toFloat()),
BarEntry(2f, points.student.toFloat())
), "Legenda")
), binding.root.context.getString(R.string.grade_statistics_legend))
with(dataset) {
valueTextSize = 12f
valueTextColor = holder.binding.root.context.getThemeAttrColor(android.R.attr.textColorPrimary)
valueTextColor = binding.root.context.getThemeAttrColor(android.R.attr.textColorPrimary)
valueFormatter = object : ValueFormatter() {
override fun getBarLabel(barEntry: BarEntry) = "${barEntry.y}%"
}
colors = gradePointsColors
}
with(holder.binding.gradeStatisticsBar) {
with(binding.gradeStatisticsBar) {
setTouchEnabled(false)
if (items.size == 1) animateXY(1000, 1000)
data = BarData(dataset).apply {
@ -179,12 +184,12 @@ class GradeStatisticsAdapter @Inject constructor() :
}
legend.setCustom(listOf(
LegendEntry().apply {
label = "Średnia klasy"
label = binding.root.context.getString(R.string.grade_statistics_average_class)
formColor = gradePointsColors[0]
form = Legend.LegendForm.SQUARE
},
LegendEntry().apply {
label = "Uczeń"
label = binding.root.context.getString(R.string.grade_statistics_average_student)
formColor = gradePointsColors[1]
form = Legend.LegendForm.SQUARE
}
@ -193,7 +198,7 @@ class GradeStatisticsAdapter @Inject constructor() :
description.isEnabled = false
holder.binding.root.context.getThemeAttrColor(android.R.attr.textColorPrimary).let {
binding.root.context.getThemeAttrColor(android.R.attr.textColorPrimary).let {
axisLeft.textColor = it
axisRight.textColor = it
}

View File

@ -13,6 +13,7 @@ import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.grade.GradeView
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.setOnItemSelectedListener
import javax.inject.Inject
@ -69,6 +70,8 @@ class GradeStatisticsFragment :
gradeStatisticsSubjectsContainer.setElevationCompat(requireContext().dpToPx(1f))
gradeStatisticsSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
gradeStatisticsSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
gradeStatisticsSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
gradeStatisticsErrorRetry.setOnClickListener { presenter.onRetry() }
gradeStatisticsErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}

View File

@ -172,6 +172,7 @@ class GradeStatisticsPresenter @Inject constructor(
showErrorView(false)
enableSwipe(true)
showRefresh(true)
showProgress(false)
updateData(it.data!!, preferencesRepository.gradeColorTheme, preferencesRepository.showAllSubjectsOnStatisticsList)
showSubjects(!preferencesRepository.showAllSubjectsOnStatisticsList)
}

View File

@ -13,6 +13,7 @@ import io.github.wulkanowy.databinding.FragmentGradeSummaryBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.grade.GradeView
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject
@AndroidEntryPoint
@ -52,7 +53,9 @@ class GradeSummaryFragment :
adapter = gradeSummaryAdapter
}
with(binding) {
gradeSummarySwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
gradeSummarySwipe.setOnRefreshListener(presenter::onSwipeRefresh)
gradeSummarySwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
gradeSummarySwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
gradeSummaryErrorRetry.setOnClickListener { presenter.onRetry() }
gradeSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}

View File

@ -6,7 +6,7 @@ import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider
import io.github.wulkanowy.ui.modules.grade.GradeDetailsWithAverage
import io.github.wulkanowy.ui.modules.grade.GradeSubject
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.afterLoading
import io.github.wulkanowy.utils.flowWithResourceIn
@ -135,14 +135,14 @@ class GradeSummaryPresenter @Inject constructor(
cancelJobs("load")
}
private fun createGradeSummaryItems(items: List<GradeDetailsWithAverage>): List<GradeSummary> {
private fun createGradeSummaryItems(items: List<GradeSubject>): List<GradeSummary> {
return items
.filter { !checkEmpty(it) }
.sortedBy { it.subject }
.map { it.summary.copy(average = it.average) }
}
private fun checkEmpty(gradeSummary: GradeDetailsWithAverage): Boolean {
private fun checkEmpty(gradeSummary: GradeSubject): Boolean {
return gradeSummary.run {
summary.finalGrade.isBlank()
&& summary.predictedGrade.isBlank()

View File

@ -15,6 +15,7 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject
@AndroidEntryPoint
@ -55,6 +56,8 @@ class HomeworkFragment : BaseFragment<FragmentHomeworkBinding>(R.layout.fragment
with(binding) {
homeworkSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
homeworkSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
homeworkSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
homeworkErrorRetry.setOnClickListener { presenter.onRetry() }
homeworkErrorDetails.setOnClickListener { presenter.onDetailsClick() }

View File

@ -10,6 +10,7 @@ import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.databinding.FragmentLuckyNumberBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject
@AndroidEntryPoint
@ -38,7 +39,9 @@ class LuckyNumberFragment :
override fun initView() {
with(binding) {
luckyNumberSwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
luckyNumberSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
luckyNumberSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
luckyNumberSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
luckyNumberErrorRetry.setOnClickListener { presenter.onRetry() }
luckyNumberErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}

View File

@ -14,6 +14,7 @@ import android.os.Build.VERSION_CODES.LOLLIPOP
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService
import androidx.core.view.ViewCompat
@ -28,7 +29,7 @@ import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.ActivityMainBinding
import io.github.wulkanowy.ui.base.BaseActivity
import io.github.wulkanowy.ui.modules.account.AccountDialog
import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog
import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment
import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment
@ -65,17 +66,19 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
private val overlayProvider by lazy { ElevationOverlayProvider(this) }
private val navController = FragNavController(supportFragmentManager, R.id.mainFragmentContainer)
private val navController =
FragNavController(supportFragmentManager, R.id.mainFragmentContainer)
companion object {
const val EXTRA_START_MENU = "extraStartMenu"
fun getStartIntent(context: Context, startMenu: MainView.Section? = null, clear: Boolean = false): Intent {
return Intent(context, MainActivity::class.java)
.apply {
if (clear) flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK
startMenu?.let { putExtra(EXTRA_START_MENU, it.id) }
}
fun getStartIntent(
context: Context,
startMenu: MainView.Section? = null,
clear: Boolean = false
) = Intent(context, MainActivity::class.java).apply {
if (clear) flags = FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK
startMenu?.let { putExtra(EXTRA_START_MENU, it.id) }
}
}
@ -83,7 +86,10 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
override val currentStackSize get() = navController.currentStack?.size
override val currentViewTitle get() = (navController.currentFrag as? MainView.TitledView)?.titleStringId?.let { getString(it) }
override val currentViewTitle
get() = (navController.currentFrag as? MainView.TitledView)?.titleStringId?.let {
getString(it)
}
override val currentViewSubtitle get() = (navController.currentFrag as? MainView.TitledView)?.subtitleString
@ -106,7 +112,10 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
messageContainer = binding.mainFragmentContainer
updateHelper.messageContainer = binding.mainFragmentContainer
presenter.onAttachView(this, MainView.Section.values().singleOrNull { it.id == intent.getIntExtra(EXTRA_START_MENU, -1) })
val section = MainView.Section.values()
.singleOrNull { it.id == intent.getIntExtra(EXTRA_START_MENU, -1) }
presenter.onAttachView(this, section)
with(navController) {
initialize(startMenuIndex, savedInstanceState)
@ -132,21 +141,49 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
val shortcutsList = mutableListOf<ShortcutInfo>()
listOf(
Triple(getString(R.string.grade_title), R.drawable.ic_shortcut_grade, MainView.Section.GRADE),
Triple(getString(R.string.attendance_title), R.drawable.ic_shortcut_attendance, MainView.Section.ATTENDANCE),
Triple(getString(R.string.exam_title), R.drawable.ic_shortcut_exam, MainView.Section.EXAM),
Triple(getString(R.string.timetable_title), R.drawable.ic_shortcut_timetable, MainView.Section.TIMETABLE),
Triple(getString(R.string.message_title), R.drawable.ic_shortcut_message, MainView.Section.MESSAGE)
Triple(
getString(R.string.grade_title),
R.drawable.ic_shortcut_grade,
MainView.Section.GRADE
),
Triple(
getString(R.string.attendance_title),
R.drawable.ic_shortcut_attendance,
MainView.Section.ATTENDANCE
),
Triple(
getString(R.string.exam_title),
R.drawable.ic_shortcut_exam,
MainView.Section.EXAM
),
Triple(
getString(R.string.timetable_title),
R.drawable.ic_shortcut_timetable,
MainView.Section.TIMETABLE
),
Triple(
getString(R.string.message_title),
R.drawable.ic_shortcut_message,
MainView.Section.MESSAGE
)
).forEach { (title, icon, enum) ->
shortcutsList.add(ShortcutInfo.Builder(applicationContext, title)
.setShortLabel(title)
.setLongLabel(title)
.setIcon(Icon.createWithResource(applicationContext, icon))
.setIntents(arrayOf(
Intent(applicationContext, MainActivity::class.java).setAction(Intent.ACTION_VIEW),
Intent(applicationContext, MainActivity::class.java).putExtra(EXTRA_START_MENU, enum.id)
.setAction(Intent.ACTION_VIEW).addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK)))
.build())
shortcutsList.add(
ShortcutInfo.Builder(applicationContext, title)
.setShortLabel(title)
.setLongLabel(title)
.setIcon(Icon.createWithResource(applicationContext, icon))
.setIntents(
arrayOf(
Intent(applicationContext, MainActivity::class.java)
.setAction(Intent.ACTION_VIEW),
Intent(applicationContext, MainActivity::class.java)
.putExtra(EXTRA_START_MENU, enum.id)
.setAction(Intent.ACTION_VIEW)
.addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK)
)
)
.build()
)
}
getSystemService<ShortcutManager>()?.dynamicShortcuts = shortcutsList
@ -160,20 +197,33 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
override fun initView() {
with(binding.mainToolbar) {
if (SDK_INT >= LOLLIPOP) stateListAnimator = null
setBackgroundColor(overlayProvider.compositeOverlayWithThemeSurfaceColorIfNeeded(dpToPx(4f)))
setBackgroundColor(
overlayProvider.compositeOverlayWithThemeSurfaceColorIfNeeded(dpToPx(4f))
)
}
with(binding.mainBottomNav) {
addItems(listOf(
AHBottomNavigationItem(R.string.grade_title, R.drawable.ic_main_grade, 0),
AHBottomNavigationItem(R.string.attendance_title, R.drawable.ic_main_attendance, 0),
AHBottomNavigationItem(R.string.exam_title, R.drawable.ic_main_exam, 0),
AHBottomNavigationItem(R.string.timetable_title, R.drawable.ic_main_timetable, 0),
AHBottomNavigationItem(R.string.more_title, R.drawable.ic_main_more, 0)
))
addItems(
listOf(
AHBottomNavigationItem(R.string.grade_title, R.drawable.ic_main_grade, 0),
AHBottomNavigationItem(
R.string.attendance_title,
R.drawable.ic_main_attendance,
0
),
AHBottomNavigationItem(R.string.exam_title, R.drawable.ic_main_exam, 0),
AHBottomNavigationItem(
R.string.timetable_title,
R.drawable.ic_main_timetable,
0
),
AHBottomNavigationItem(R.string.more_title, R.drawable.ic_main_more, 0)
)
)
accentColor = getThemeAttrColor(R.attr.colorPrimary)
inactiveColor = getThemeAttrColor(R.attr.colorOnSurface, 153)
defaultBackgroundColor = overlayProvider.compositeOverlayWithThemeSurfaceColorIfNeeded(dpToPx(8f))
defaultBackgroundColor =
overlayProvider.compositeOverlayWithThemeSurfaceColorIfNeeded(dpToPx(8f))
titleState = ALWAYS_SHOW
currentItem = startMenuIndex
isBehaviorTranslationEnabled = false
@ -183,6 +233,13 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
with(navController) {
setOnViewChangeListener { section, name ->
binding.mainBottomNav.visibility =
if (section == MainView.Section.ACCOUNT || section == MainView.Section.STUDENT_INFO) {
View.GONE
} else {
View.VISIBLE
}
analytics.setCurrentScreen(this@MainActivity, name)
presenter.onViewChange(section)
}
@ -224,7 +281,7 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
}
override fun showAccountPicker() {
navController.showDialogFragment(AccountDialog.newInstance())
navController.showDialogFragment(AccountQuickDialog.newInstance())
}
override fun showActionBarElevation(show: Boolean) {

View File

@ -64,6 +64,8 @@ interface MainView : BaseView {
LUCKY_NUMBER(8),
SETTINGS(9),
ABOUT(10),
SCHOOL(11)
SCHOOL(11),
ACCOUNT(12),
STUDENT_INFO(13)
}
}

View File

@ -70,6 +70,7 @@ class MessagePreviewPresenter @Inject constructor(
this@MessagePreviewPresenter.attachments = it.data.attachments
view?.apply {
setMessageWithAttachment(it.data)
showContent(true)
initOptions()
}
analytics.logEvent(

View File

@ -19,6 +19,7 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.getThemeAttrColor
import kotlinx.coroutines.FlowPreview
import javax.inject.Inject
@ -74,7 +75,9 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
addItemDecoration(DividerItemDecoration(context))
}
with(binding) {
messageTabSwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
messageTabSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
messageTabSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
messageTabSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
messageTabErrorRetry.setOnClickListener { presenter.onRetry() }
messageTabErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}

View File

@ -16,6 +16,7 @@ import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.mobiledevice.token.MobileDeviceTokenDialog
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject
@AndroidEntryPoint
@ -56,7 +57,9 @@ class MobileDeviceFragment :
}
with(binding) {
mobileDevicesSwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
mobileDevicesSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
mobileDevicesSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
mobileDevicesSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
mobileDevicesErrorRetry.setOnClickListener { presenter.onRetry() }
mobileDevicesErrorDetails.setOnClickListener { presenter.onDetailsClick() }
mobileDeviceAddButton.setOnClickListener { presenter.onRegisterDevice() }

View File

@ -13,6 +13,7 @@ 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.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject
@AndroidEntryPoint
@ -50,7 +51,9 @@ class NoteFragment : BaseFragment<FragmentNoteBinding>(R.layout.fragment_note),
addItemDecoration(DividerItemDecoration(context))
}
with(binding) {
noteSwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
noteSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
noteSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
noteSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
noteErrorRetry.setOnClickListener { presenter.onRetry() }
noteErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}

View File

@ -12,6 +12,7 @@ import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersChildView
import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersFragment
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.openDialer
import io.github.wulkanowy.utils.openNavigation
import javax.inject.Inject
@ -39,7 +40,9 @@ class SchoolFragment : BaseFragment<FragmentSchoolBinding>(R.layout.fragment_sch
override fun initView() {
with(binding) {
schoolSwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
schoolSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
schoolSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
schoolSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
schoolErrorRetry.setOnClickListener { presenter.onRetry() }
schoolErrorDetails.setOnClickListener { presenter.onDetailsClick() }

View File

@ -81,10 +81,7 @@ class SchoolPresenter @Inject constructor(
showEmpty(false)
showErrorView(false)
}
analytics.logEvent(
"load_item",
"type" to "school"
)
analytics.logEvent("load_item", "type" to "school")
} else view?.run {
Timber.i("Loading school result: No school info found")
showContent(!isViewEmpty)

View File

@ -14,6 +14,7 @@ import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersChildView
import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersFragment
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject
@AndroidEntryPoint
@ -51,7 +52,9 @@ class TeacherFragment : BaseFragment<FragmentTeacherBinding>(R.layout.fragment_t
addItemDecoration(DividerItemDecoration(context))
}
with(binding) {
teacherSwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
teacherSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
teacherSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
teacherSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
teacherErrorRetry.setOnClickListener { presenter.onRetry() }
teacherErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}

View File

@ -79,6 +79,10 @@ class SettingsFragment : PreferenceFragmentCompat(),
lingver.setLocale(requireContext(), langCode)
}
override fun updateLanguageToFollowSystem() {
lingver.setFollowSystemLocale(requireContext())
}
override fun setServicesSuspended(serviceEnablesKey: String, isHolidays: Boolean) {
findPreference<Preference>(serviceEnablesKey)?.run {
summary = if (isHolidays) getString(R.string.pref_services_suspended) else ""

View File

@ -42,14 +42,18 @@ class SettingsPresenter @Inject constructor(
when (key) {
serviceEnableKey -> with(syncManager) { if (isServiceEnabled) startPeriodicSyncWorker() else stopSyncWorker() }
servicesIntervalKey, servicesOnlyWifiKey -> syncManager.startPeriodicSyncWorker(true)
isDebugNotificationEnableKey -> chuckerCollector.showNotification = isDebugNotificationEnable
isDebugNotificationEnableKey -> chuckerCollector.showNotification =
isDebugNotificationEnable
appThemeKey -> view?.recreateView()
isUpcomingLessonsNotificationsEnableKey -> if (!isUpcomingLessonsNotificationsEnable) timetableNotificationHelper.cancelNotification()
appLanguageKey -> view?.run {
val newLang = if (appLanguage == "system") appInfo.systemLanguage else appLanguage
analytics.logEvent("language", "setting_changed" to newLang)
updateLanguage(newLang)
if (appLanguage == "system") {
updateLanguageToFollowSystem()
analytics.logEvent("language", "setting_changed" to appInfo.systemLanguage)
} else {
updateLanguage(appLanguage)
analytics.logEvent("language", "setting_changed" to appLanguage)
}
recreateView()
}
}
@ -71,7 +75,10 @@ class SettingsPresenter @Inject constructor(
analytics.logEvent("sync_now", "status" to "success")
}
WorkInfo.State.FAILED -> {
showError(syncFailedString, Throwable(workInfo.outputData.getString("error")))
showError(
syncFailedString,
Throwable(workInfo.outputData.getString("error"))
)
analytics.logEvent("sync_now", "status" to "failed")
}
else -> Timber.d("Sync now state: ${workInfo.state}")

View File

@ -14,6 +14,8 @@ interface SettingsView : BaseView {
fun updateLanguage(langCode: String)
fun updateLanguageToFollowSystem()
fun setServicesSuspended(serviceEnablesKey: String, isHolidays: Boolean)
fun setSyncInProgress(inProgress: Boolean)

View File

@ -0,0 +1,42 @@
package io.github.wulkanowy.ui.modules.studentinfo
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.databinding.ItemStudentInfoBinding
import javax.inject.Inject
class StudentInfoAdapter @Inject constructor() :
RecyclerView.Adapter<StudentInfoAdapter.ViewHolder>() {
var items = listOf<Pair<String, String>>()
var onItemClickListener: (position: Int) -> Unit = {}
var onItemLongClickListener: (text: String) -> Unit = {}
override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemStudentInfoBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
with(holder.binding) {
studentInfoItemTitle.text = item.first
studentInfoItemSubtitle.text = item.second
with(root) {
setOnClickListener { onItemClickListener(position) }
setOnLongClickListener {
onItemLongClickListener(studentInfoItemSubtitle.text.toString())
true
}
}
}
}
class ViewHolder(val binding: ItemStudentInfoBinding) : RecyclerView.ViewHolder(binding.root)
}

View File

@ -0,0 +1,232 @@
package io.github.wulkanowy.ui.modules.studentinfo
import android.annotation.SuppressLint
import android.content.ClipData
import android.content.ClipboardManager
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.View
import android.widget.Toast
import androidx.core.content.getSystemService
import androidx.core.view.get
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.StudentInfo
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.enums.Gender
import io.github.wulkanowy.databinding.FragmentStudentInfoBinding
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.utils.getThemeAttrColor
import javax.inject.Inject
@AndroidEntryPoint
class StudentInfoFragment :
BaseFragment<FragmentStudentInfoBinding>(R.layout.fragment_student_info), StudentInfoView,
MainView.TitledView {
@Inject
lateinit var presenter: StudentInfoPresenter
@Inject
lateinit var studentInfoAdapter: StudentInfoAdapter
override val titleStringId: Int
get() = R.string.student_info_title
override val isViewEmpty get() = studentInfoAdapter.items.isEmpty()
companion object {
private const val INFO_TYPE_ARGUMENT_KEY = "info_type"
private const val STUDENT_ARGUMENT_KEY = "student_with_semesters"
fun newInstance(type: StudentInfoView.Type, studentWithSemesters: StudentWithSemesters) =
StudentInfoFragment().apply {
arguments = Bundle().apply {
putSerializable(INFO_TYPE_ARGUMENT_KEY, type)
putSerializable(STUDENT_ARGUMENT_KEY, studentWithSemesters)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentStudentInfoBinding.bind(view)
presenter.onAttachView(
this,
requireArguments().getSerializable(INFO_TYPE_ARGUMENT_KEY) as StudentInfoView.Type,
requireArguments().getSerializable(STUDENT_ARGUMENT_KEY) as StudentWithSemesters
)
}
override fun initView() {
with(binding) {
studentInfoSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
studentInfoSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
studentInfoSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
studentInfoErrorRetry.setOnClickListener { presenter.onRetry() }
studentInfoErrorDetails.setOnClickListener { presenter.onDetailsClick() }
}
with(studentInfoAdapter) {
onItemClickListener = presenter::onItemSelected
onItemLongClickListener = presenter::onItemLongClick
}
with(binding.studentInfoRecycler) {
layoutManager = LinearLayoutManager(context)
addItemDecoration(DividerItemDecoration(context, RecyclerView.VERTICAL))
setHasFixedSize(true)
adapter = studentInfoAdapter
}
}
override fun updateData(data: List<Pair<String, String>>) {
with(studentInfoAdapter) {
items = data
notifyDataSetChanged()
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
menu[0].isVisible = false
}
override fun showPersonalTypeData(studentInfo: StudentInfo) {
updateData(
listOf(
getString(R.string.student_info_first_name) to studentInfo.firstName,
getString(R.string.student_info_second_name) to studentInfo.secondName,
getString(R.string.student_info_last_name) to studentInfo.surname,
getString(R.string.student_info_gender) to getString(if (studentInfo.gender == Gender.MALE) R.string.student_info_male else R.string.student_info_female),
getString(R.string.student_info_polish_citizenship) to getString(if (studentInfo.hasPolishCitizenship) R.string.all_yes else R.string.all_no),
getString(R.string.student_info_family_name) to studentInfo.familyName,
getString(R.string.student_info_parents_name) to studentInfo.parentsNames
).map {
if (it.second.isBlank()) it.copy(second = getString(R.string.all_no_data)) else it
}
)
}
override fun showContactTypeData(studentInfo: StudentInfo) {
updateData(
listOf(
getString(R.string.student_info_phone) to studentInfo.phoneNumber,
getString(R.string.student_info_cellphone) to studentInfo.cellPhoneNumber,
getString(R.string.student_info_email) to studentInfo.email
).map {
if (it.second.isBlank()) it.copy(second = getString(R.string.all_no_data)) else it
}
)
}
@SuppressLint("DefaultLocale")
override fun showFamilyTypeData(studentInfo: StudentInfo) {
updateData(
listOf(
studentInfo.firstGuardian.kinship.capitalize() to studentInfo.firstGuardian.fullName,
studentInfo.secondGuardian.kinship.capitalize() to studentInfo.secondGuardian.fullName
).map {
if (it.second.isBlank()) it.copy(second = getString(R.string.all_no_data)) else it
}
)
}
override fun showAddressTypeData(studentInfo: StudentInfo) {
updateData(
listOf(
getString(R.string.student_info_address) to studentInfo.address,
getString(R.string.student_info_registered_address) to studentInfo.registeredAddress,
getString(R.string.student_info_correspondence_address) to studentInfo.correspondenceAddress
).map {
if (it.second.isBlank()) it.copy(second = getString(R.string.all_no_data)) else it
}
)
}
override fun showFirstGuardianTypeData(studentInfo: StudentInfo) {
updateData(
listOf(
getString(R.string.student_info_full_name) to studentInfo.firstGuardian.fullName,
getString(R.string.student_info_kinship) to studentInfo.firstGuardian.kinship,
getString(R.string.student_info_guardian_address) to studentInfo.firstGuardian.address,
getString(R.string.student_info_phones) to studentInfo.firstGuardian.phones,
getString(R.string.student_info_email) to studentInfo.firstGuardian.email
).map {
if (it.second.isBlank()) it.copy(second = getString(R.string.all_no_data)) else it
}
)
}
override fun showSecondGuardianTypeData(studentInfo: StudentInfo) {
updateData(
listOf(
getString(R.string.student_info_full_name) to studentInfo.secondGuardian.fullName,
getString(R.string.student_info_kinship) to studentInfo.secondGuardian.kinship,
getString(R.string.student_info_guardian_address) to studentInfo.secondGuardian.address,
getString(R.string.student_info_phones) to studentInfo.secondGuardian.phones,
getString(R.string.student_info_email) to studentInfo.secondGuardian.email
).map {
if (it.second.isBlank()) it.copy(second = getString(R.string.all_no_data)) else it
}
)
}
override fun openStudentInfoView(
infoType: StudentInfoView.Type,
studentWithSemesters: StudentWithSemesters
) {
(requireActivity() as MainActivity).pushView(newInstance(infoType, studentWithSemesters))
}
override fun showEmpty(show: Boolean) {
binding.studentInfoEmpty.visibility = if (show) View.VISIBLE else View.GONE
}
override fun showErrorView(show: Boolean) {
binding.studentInfoError.visibility = if (show) View.VISIBLE else View.GONE
}
override fun setErrorDetails(message: String) {
binding.studentInfoErrorMessage.text = message
}
override fun showProgress(show: Boolean) {
binding.studentInfoProgress.visibility = if (show) View.VISIBLE else View.GONE
}
override fun enableSwipe(enable: Boolean) {
binding.studentInfoSwipe.isEnabled = enable
}
override fun showContent(show: Boolean) {
binding.studentInfoRecycler.visibility = if (show) View.VISIBLE else View.GONE
}
override fun hideRefresh() {
binding.studentInfoSwipe.isRefreshing = false
}
override fun copyToClipboard(text: String) {
val clipData = ClipData.newPlainText("student_info_wulkanowy", text)
requireActivity().getSystemService<ClipboardManager>()?.setPrimaryClip(clipData)
Toast.makeText(context, R.string.all_copied, Toast.LENGTH_SHORT).show()
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
}

View File

@ -0,0 +1,142 @@
package io.github.wulkanowy.ui.modules.studentinfo
import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.StudentInfo
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.repositories.StudentInfoRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.afterLoading
import io.github.wulkanowy.utils.flowWithResourceIn
import io.github.wulkanowy.utils.getCurrentOrLast
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import javax.inject.Inject
class StudentInfoPresenter @Inject constructor(
errorHandler: ErrorHandler,
studentRepository: StudentRepository,
private val studentInfoRepository: StudentInfoRepository,
private val analytics: AnalyticsHelper
) : BasePresenter<StudentInfoView>(errorHandler, studentRepository) {
private lateinit var infoType: StudentInfoView.Type
private lateinit var studentWithSemesters: StudentWithSemesters
private lateinit var lastError: Throwable
fun onAttachView(
view: StudentInfoView,
type: StudentInfoView.Type,
studentWithSemesters: StudentWithSemesters
) {
super.onAttachView(view)
infoType = type
this.studentWithSemesters = studentWithSemesters
view.initView()
Timber.i("Student info $infoType view was initialized")
errorHandler.showErrorMessage = ::showErrorViewOnError
loadData()
}
fun onSwipeRefresh() {
loadData(true)
}
fun onRetry() {
view?.run {
showErrorView(false)
showProgress(true)
}
loadData(true)
}
fun onDetailsClick() {
view?.showErrorDetailsDialog(lastError)
}
fun onItemSelected(position: Int) {
if (infoType != StudentInfoView.Type.FAMILY) return
if (position == 0) {
view?.openStudentInfoView(StudentInfoView.Type.FIRST_GUARDIAN, studentWithSemesters)
} else {
view?.openStudentInfoView(StudentInfoView.Type.SECOND_GUARDIAN, studentWithSemesters)
}
}
fun onItemLongClick(text: String) {
view?.copyToClipboard(text)
}
private fun loadData(forceRefresh: Boolean = false) {
flowWithResourceIn {
val semester = studentWithSemesters.semesters.getCurrentOrLast()
studentInfoRepository.getStudentInfo(
studentWithSemesters.student,
semester,
forceRefresh
)
}.onEach {
when (it.status) {
Status.LOADING -> Timber.i("Loading student info $infoType started")
Status.SUCCESS -> {
if (it.data != null) {
Timber.i("Loading student info $infoType result: Success")
showCorrectData(it.data)
view?.run {
showContent(true)
showEmpty(false)
showErrorView(false)
}
analytics.logEvent("load_item", "type" to "student_info")
} else {
Timber.i("Loading student info $infoType result: No school info found")
view?.run {
showContent(!isViewEmpty)
showEmpty(isViewEmpty)
showErrorView(false)
}
}
}
Status.ERROR -> {
Timber.i("Loading student info $infoType result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}.afterLoading {
view?.run {
hideRefresh()
showProgress(false)
enableSwipe(true)
}
}.launch()
}
private fun showCorrectData(studentInfo: StudentInfo) {
when (infoType) {
StudentInfoView.Type.PERSONAL -> view?.showPersonalTypeData(studentInfo)
StudentInfoView.Type.CONTACT -> view?.showContactTypeData(studentInfo)
StudentInfoView.Type.ADDRESS -> view?.showAddressTypeData(studentInfo)
StudentInfoView.Type.FAMILY -> view?.showFamilyTypeData(studentInfo)
StudentInfoView.Type.SECOND_GUARDIAN -> view?.showSecondGuardianTypeData(studentInfo)
StudentInfoView.Type.FIRST_GUARDIAN -> view?.showFirstGuardianTypeData(studentInfo)
}
}
private fun showErrorViewOnError(message: String, error: Throwable) {
view?.run {
if (isViewEmpty) {
lastError = error
setErrorDetails(message)
showErrorView(true)
showEmpty(false)
showContent(false)
showProgress(false)
} else showError(message, error)
}
}
}

View File

@ -0,0 +1,48 @@
package io.github.wulkanowy.ui.modules.studentinfo
import io.github.wulkanowy.data.db.entities.StudentInfo
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.ui.base.BaseView
interface StudentInfoView : BaseView {
enum class Type {
PERSONAL, ADDRESS, CONTACT, FAMILY, FIRST_GUARDIAN, SECOND_GUARDIAN
}
val isViewEmpty: Boolean
fun initView()
fun updateData(data: List<Pair<String, String>>)
fun showPersonalTypeData(studentInfo: StudentInfo)
fun showContactTypeData(studentInfo: StudentInfo)
fun showAddressTypeData(studentInfo: StudentInfo)
fun showFamilyTypeData(studentInfo: StudentInfo)
fun showFirstGuardianTypeData(studentInfo: StudentInfo)
fun showSecondGuardianTypeData(studentInfo: StudentInfo)
fun openStudentInfoView(infoType: Type, studentWithSemesters: StudentWithSemesters)
fun showEmpty(show: Boolean)
fun showErrorView(show: Boolean)
fun setErrorDetails(message: String)
fun showProgress(show: Boolean)
fun enableSwipe(enable: Boolean)
fun showContent(show: Boolean)
fun hideRefresh()
fun copyToClipboard(text: String)
}

View File

@ -21,6 +21,7 @@ import io.github.wulkanowy.ui.modules.timetable.completed.CompletedLessonsFragme
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.SchooldaysRangeLimiter
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import java.time.LocalDate
import javax.inject.Inject
@ -69,6 +70,8 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
with(binding) {
timetableSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
timetableSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
timetableSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
timetableErrorRetry.setOnClickListener { presenter.onRetry() }
timetableErrorDetails.setOnClickListener { presenter.onDetailsClick() }
@ -176,6 +179,7 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
setDateRangeLimiter(SchooldaysRangeLimiter())
version = DatePickerDialog.Version.VERSION_2
scrollOrientation = DatePickerDialog.ScrollOrientation.VERTICAL
vibrate(false)
show(this@TimetableFragment.parentFragmentManager, null)
}
}

View File

@ -13,6 +13,7 @@ import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.SchooldaysRangeLimiter
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import java.time.LocalDate
import javax.inject.Inject
@ -53,6 +54,8 @@ class AdditionalLessonsFragment :
with(binding) {
additionalLessonsSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
additionalLessonsSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
additionalLessonsSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
additionalLessonsErrorRetry.setOnClickListener { presenter.onRetry() }
additionalLessonsPreviousButton.setOnClickListener { presenter.onPreviousDay() }
@ -128,6 +131,7 @@ class AdditionalLessonsFragment :
setDateRangeLimiter(SchooldaysRangeLimiter())
version = DatePickerDialog.Version.VERSION_2
scrollOrientation = DatePickerDialog.ScrollOrientation.VERTICAL
vibrate(false)
show(this@AdditionalLessonsFragment.parentFragmentManager, null)
}
}

View File

@ -18,6 +18,7 @@ import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.SchooldaysRangeLimiter
import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getCompatDrawable
import io.github.wulkanowy.utils.getThemeAttrColor
import java.time.LocalDate
import javax.inject.Inject
@ -60,6 +61,8 @@ class CompletedLessonsFragment :
with(binding) {
completedLessonsSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
completedLessonsSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
completedLessonsSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
completedLessonErrorRetry.setOnClickListener { presenter.onRetry() }
completedLessonErrorDetails.setOnClickListener { presenter.onDetailsClick() }
@ -147,6 +150,7 @@ class CompletedLessonsFragment :
setDateRangeLimiter(SchooldaysRangeLimiter())
version = DatePickerDialog.Version.VERSION_2
scrollOrientation = DatePickerDialog.ScrollOrientation.VERTICAL
vibrate(false)
show(this@CompletedLessonsFragment.parentFragmentManager, null)
}
}

View File

@ -27,6 +27,7 @@ import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.nextOrSameSchoolDay
import io.github.wulkanowy.utils.nextSchoolDay
import io.github.wulkanowy.utils.nickOrName
import io.github.wulkanowy.utils.previousSchoolDay
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.coroutines.GlobalScope
@ -151,8 +152,14 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() {
val remoteView = RemoteViews(context.packageName, layoutId).apply {
setEmptyView(R.id.timetableWidgetList, R.id.timetableWidgetEmpty)
setTextViewText(R.id.timetableWidgetDate, date.toFormattedString("EEEE, dd.MM").capitalize())
setTextViewText(R.id.timetableWidgetName, student?.studentName ?: context.getString(R.string.all_no_data))
setTextViewText(
R.id.timetableWidgetDate,
date.toFormattedString("EEEE, dd.MM").capitalize()
)
setTextViewText(
R.id.timetableWidgetName,
student?.nickOrName ?: context.getString(R.string.all_no_data)
)
setRemoteAdapter(R.id.timetableWidgetList, adapterIntent)
setOnClickPendingIntent(R.id.timetableWidgetNext, nextNavIntent)
setOnClickPendingIntent(R.id.timetableWidgetPrev, prevNavIntent)

View File

@ -4,7 +4,9 @@ import android.content.res.Resources
import android.os.Build.MANUFACTURER
import android.os.Build.MODEL
import android.os.Build.VERSION.SDK_INT
import io.github.wulkanowy.BuildConfig.BUILD_TIMESTAMP
import io.github.wulkanowy.BuildConfig.DEBUG
import io.github.wulkanowy.BuildConfig.FLAVOR
import io.github.wulkanowy.BuildConfig.VERSION_CODE
import io.github.wulkanowy.BuildConfig.VERSION_NAME
import javax.inject.Inject
@ -17,6 +19,10 @@ open class AppInfo @Inject constructor() {
open val versionCode get() = VERSION_CODE
open val buildTimestamp get() = BUILD_TIMESTAMP
open val buildFlavor get() = FLAVOR
open val versionName get() = VERSION_NAME
open val systemVersion get() = SDK_INT
@ -28,4 +34,9 @@ open class AppInfo @Inject constructor() {
@Suppress("DEPRECATION")
open val systemLanguage: String
get() = Resources.getSystem().configuration.locale.language
open val defaultColorsForAvatar = listOf(
0xe57373, 0xf06292, 0xba68c8, 0x9575cd, 0x7986cb, 0x64b5f6, 0x4fc3f7, 0x4dd0e1, 0x4db6ac,
0x81c784, 0xaed581, 0xff8a65, 0xd4e157, 0xffd54f, 0xffb74d, 0xa1887f, 0x90a4ae
)
}

View File

@ -2,6 +2,7 @@ package io.github.wulkanowy.utils
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.Status
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
@ -71,24 +72,14 @@ inline fun <ResultType, RequestType, T> networkBoundResource(
fun <T> flowWithResource(block: suspend () -> T) = flow {
emit(Resource.loading())
emit(try {
Resource.success(block())
} catch (e: Throwable) {
Resource.error(e)
})
}
emit(Resource.success(block()))
}.catch { emit(Resource.error(it)) }
@OptIn(FlowPreview::class)
fun <T> flowWithResourceIn(block: suspend () -> Flow<Resource<T>>) = flow {
emit(Resource.loading())
block()
.catch { emit(Resource.error(it)) }
.collect {
if (it.status != Status.LOADING || (it.status == Status.LOADING && it.data != null)) { // LOADING without data is already emitted
emit(it)
}
}
}
emitAll(block().filter { it.status != Status.LOADING || (it.status == Status.LOADING && it.data != null) })
}.catch { emit(Resource.error(it)) }
fun <T> Flow<Resource<T>>.afterLoading(callback: () -> Unit) = onEach {
if (it.status != Status.LOADING) callback()
@ -96,4 +87,5 @@ fun <T> Flow<Resource<T>>.afterLoading(callback: () -> Unit) = onEach {
suspend fun <T> Flow<Resource<T>>.toFirstResult() = filter { it.status != Status.LOADING }.first()
suspend fun <T> Flow<Resource<T>>.waitForResult() = takeWhile { it.status == Status.LOADING }.collect()
suspend fun <T> Flow<Resource<T>>.waitForResult() =
takeWhile { it.status == Status.LOADING }.collect()

View File

@ -2,6 +2,8 @@ package io.github.wulkanowy.utils
import androidx.fragment.app.Fragment
import io.github.wulkanowy.ui.modules.about.AboutFragment
import io.github.wulkanowy.ui.modules.account.AccountFragment
import io.github.wulkanowy.ui.modules.account.accountdetails.AccountDetailsFragment
import io.github.wulkanowy.ui.modules.attendance.AttendanceFragment
import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment
@ -13,6 +15,7 @@ import io.github.wulkanowy.ui.modules.more.MoreFragment
import io.github.wulkanowy.ui.modules.note.NoteFragment
import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersFragment
import io.github.wulkanowy.ui.modules.settings.SettingsFragment
import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
fun Fragment.toSection(): MainView.Section? {
@ -29,6 +32,9 @@ fun Fragment.toSection(): MainView.Section? {
is SettingsFragment -> MainView.Section.SETTINGS
is AboutFragment -> MainView.Section.ABOUT
is SchoolAndTeachersFragment -> MainView.Section.SCHOOL
is AccountFragment -> MainView.Section.ACCOUNT
is AccountDetailsFragment -> MainView.Section.ACCOUNT
is StudentInfoFragment -> MainView.Section.STUDENT_INFO
else -> null
}
}

View File

@ -16,63 +16,58 @@ fun List<Grade>.calcAverage(): Double {
}
@JvmName("calcSummaryAverage")
fun List<GradeSummary>.calcAverage(): Double {
return asSequence().mapNotNull {
if (it.finalGrade.matches("[0-6]".toRegex())) it.finalGrade.toDouble() else null
}.average().let { if (it.isNaN()) 0.0 else it }
}
fun Grade.getBackgroundColor(theme: String): Int {
return when (theme) {
"grade_color" -> getGradeColor()
"material" -> when (value.toInt()) {
6 -> R.color.grade_material_six
5 -> R.color.grade_material_five
4 -> R.color.grade_material_four
3 -> R.color.grade_material_three
2 -> R.color.grade_material_two
1 -> R.color.grade_material_one
else -> R.color.grade_material_default
}
else -> when (value.toInt()) {
6 -> R.color.grade_vulcan_six
5 -> R.color.grade_vulcan_five
4 -> R.color.grade_vulcan_four
3 -> R.color.grade_vulcan_three
2 -> R.color.grade_vulcan_two
1 -> R.color.grade_vulcan_one
else -> R.color.grade_vulcan_default
}
fun List<GradeSummary>.calcAverage() = asSequence()
.mapNotNull {
if (it.finalGrade.matches("[0-6]".toRegex())) {
it.finalGrade.toDouble()
} else null
}
}
.average()
.let { if (it.isNaN()) 0.0 else it }
fun Grade.getGradeColor(): Int {
return when (color) {
"000000" -> R.color.grade_black
"F04C4C" -> R.color.grade_red
"20A4F7" -> R.color.grade_blue
"6ECD07" -> R.color.grade_green
"B16CF1" -> R.color.grade_purple
fun Grade.getBackgroundColor(theme: String) = when (theme) {
"grade_color" -> getGradeColor()
"material" -> when (value.toInt()) {
6 -> R.color.grade_material_six
5 -> R.color.grade_material_five
4 -> R.color.grade_material_four
3 -> R.color.grade_material_three
2 -> R.color.grade_material_two
1 -> R.color.grade_material_one
else -> R.color.grade_material_default
}
else -> when (value.toInt()) {
6 -> R.color.grade_vulcan_six
5 -> R.color.grade_vulcan_five
4 -> R.color.grade_vulcan_four
3 -> R.color.grade_vulcan_three
2 -> R.color.grade_vulcan_two
1 -> R.color.grade_vulcan_one
else -> R.color.grade_vulcan_default
}
}
fun Grade.getGradeColor() = when (color) {
"000000" -> R.color.grade_black
"F04C4C" -> R.color.grade_red
"20A4F7" -> R.color.grade_blue
"6ECD07" -> R.color.grade_green
"B16CF1" -> R.color.grade_purple
else -> R.color.grade_material_default
}
inline val Grade.colorStringId: Int
get() {
return when (color) {
"000000" -> R.string.all_black
"F04C4C" -> R.string.all_red
"20A4F7" -> R.string.all_blue
"6ECD07" -> R.string.all_green
"B16CF1" -> R.string.all_purple
else -> R.string.all_empty_color
}
get() = when (color) {
"000000" -> R.string.all_black
"F04C4C" -> R.string.all_red
"20A4F7" -> R.string.all_blue
"6ECD07" -> R.string.all_green
"B16CF1" -> R.string.all_purple
else -> R.string.all_empty_color
}
fun Grade.changeModifier(plusModifier: Double, minusModifier: Double): Grade {
return when {
modifier > 0 -> copy(modifier = plusModifier)
modifier < 0 -> copy(modifier = -minusModifier)
else -> this
}
fun Grade.changeModifier(plusModifier: Double, minusModifier: Double) = when {
modifier > 0 -> copy(modifier = plusModifier)
modifier < 0 -> copy(modifier = -minusModifier)
else -> this
}

View File

@ -0,0 +1,5 @@
package io.github.wulkanowy.utils
import io.github.wulkanowy.data.db.entities.Student
inline val Student.nickOrName get() = if (nick.isBlank()) studentName else nick

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.utils
import android.annotation.SuppressLint
import java.text.SimpleDateFormat
import java.time.DayOfWeek.FRIDAY
import java.time.DayOfWeek.MONDAY
import java.time.DayOfWeek.SATURDAY
@ -8,12 +9,12 @@ import java.time.DayOfWeek.SUNDAY
import java.time.Instant.ofEpochMilli
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalDateTime.now
import java.time.LocalDateTime.ofInstant
import java.time.Month
import java.time.ZoneId
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter.ofPattern
import java.time.format.TextStyle.FULL
import java.time.temporal.TemporalAdjusters.firstInMonth
import java.time.temporal.TemporalAdjusters.next
import java.time.temporal.TemporalAdjusters.previous
@ -33,24 +34,10 @@ fun LocalDateTime.toFormattedString(format: String = DATE_PATTERN): String = for
@SuppressLint("DefaultLocale")
fun Month.getFormattedName(): String {
return getDisplayName(FULL, Locale.getDefault())
.let {
when (it) {
"stycznia" -> "Styczeń"
"lutego" -> "Luty"
"marca" -> "Marzec"
"kwietnia" -> "Kwiecień"
"maja" -> "Maj"
"czerwca" -> "Czerwiec"
"lipca" -> "Lipiec"
"sierpnia" -> "Sierpień"
"września" -> "Wrzesień"
"października" -> "Październik"
"listopada" -> "Listopad"
"grudnia" -> "Grudzień"
else -> it
}
}.capitalize()
val formatter = SimpleDateFormat("LLLL", Locale.getDefault())
val date = now().withMonth(value)
return formatter.format(date.toInstant(ZoneOffset.UTC).toEpochMilli()).capitalize()
}
inline val LocalDate.nextSchoolDay: LocalDate

View File

@ -1,9 +1,7 @@
Wersja 0.24.3
- naprawiliśmy odczytywanie wiadomości
- naprawiliśmy niekończące się ładowanie w ocenach na drugim semestrze
- naprawiliśmy ciemny motyw na MIUI 12
- dodaliśmy automatyczne odświeżanie danych w aplikacji
- naprawiliśmy wysyłanie wiadomości kiedy uczeń zalogowany był/jest przez konto ucznia i rodzica
- dodaliśmy zakładkę lekcji dodatkowych (na górnym pasku w planie lekcji)
Wersja 0.25.0
- naprawiliśmy przełączanie semestrów przy przełączaniu uczniów
- naprawiliśmy błąd przy odświeżaniu ocen gdy włączony był inny niż domyślny tryb liczenia średniej
- dodaliśmy menadżer kont, gdzie można podejrzeć informacje o uczniu oraz zmienić pseudonim
- zmieniliśmy ikonę planu lekcji oraz kolor animacji odświeżania w ciemnym motywie
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<group
android:scaleX="0.92"
android:scaleY="0.92"
android:translateX="0.96"
android:translateY="0.96">
<path
android:fillColor="#FFF"
android:pathData="M14,12H15.5V14.82L17.94,16.23L17.19,17.53L14,15.69V12M4,2H18A2,2 0 0,1 20,4V10.1C21.24,11.36 22,13.09 22,15A7,7 0 0,1 15,22C13.09,22 11.36,21.24 10.1,20H4A2,2 0 0,1 2,18V4A2,2 0 0,1 4,2M4,15V18H8.67C8.24,17.09 8,16.07 8,15H4M4,8H10V5H4V8M18,8V5H12V8H18M4,13H8.29C8.63,11.85 9.26,10.82 10.1,10H4V13M15,10.15A4.85,4.85 0 0,0 10.15,15C10.15,17.68 12.32,19.85 15,19.85A4.85,4.85 0 0,0 19.85,15C19.85,12.32 17.68,10.15 15,10.15Z" />
</group>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 312 B

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 B

After

Width:  |  Height:  |  Size: 315 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

After

Width:  |  Height:  |  Size: 499 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 459 B

After

Width:  |  Height:  |  Size: 671 B

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M16,4c0,-1.11 0.89,-2 2,-2s2,0.89 2,2s-0.89,2 -2,2S16,5.11 16,4zM20,22v-6h2.5l-2.54,-7.63C19.68,7.55 18.92,7 18.06,7h-0.12c-0.86,0 -1.63,0.55 -1.9,1.37l-0.86,2.58C16.26,11.55 17,12.68 17,14v8H20zM12.5,11.5c0.83,0 1.5,-0.67 1.5,-1.5s-0.67,-1.5 -1.5,-1.5S11,9.17 11,10S11.67,11.5 12.5,11.5zM5.5,6c1.11,0 2,-0.89 2,-2s-0.89,-2 -2,-2s-2,0.89 -2,2S4.39,6 5.5,6zM7.5,22v-7H9V9c0,-1.1 -0.9,-2 -2,-2H4C2.9,7 2,7.9 2,9v6h1.5v7H7.5zM14,22v-4h1v-4c0,-0.82 -0.68,-1.5 -1.5,-1.5h-2c-0.82,0 -1.5,0.68 -1.5,1.5v4h1v4H14z" />
</vector>

View File

@ -5,5 +5,5 @@
android:viewportHeight="24">
<path
android:fillColor="#FFF"
android:pathData="M19,3h-1L18,1h-2v2L8,3L8,1L6,1v2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM19,19L5,19L5,9h14v10zM5,7L5,5h14v2L5,7zM7,11h10v2L7,13zM7,15h7v2L7,17z" />
android:pathData="M14,12H15.5V14.82L17.94,16.23L17.19,17.53L14,15.69V12M4,2H18A2,2 0 0,1 20,4V10.1C21.24,11.36 22,13.09 22,15A7,7 0 0,1 15,22C13.09,22 11.36,21.24 10.1,20H4A2,2 0 0,1 2,18V4A2,2 0 0,1 4,2M4,15V18H8.67C8.24,17.09 8,16.07 8,15H4M4,8H10V5H4V8M18,8V5H12V8H18M4,13H8.29C8.63,11.85 9.26,10.82 10.1,10H4V13M15,10.15A4.85,4.85 0 0,0 10.15,15C10.15,17.68 12.32,19.85 15,19.85A4.85,4.85 0 0,0 19.85,15C19.85,12.32 17.68,10.15 15,10.15Z" />
</vector>

View File

@ -5,5 +5,5 @@
android:viewportHeight="24">
<path
android:fillColor="#FFF"
android:pathData="M19,19V8H5V19H19M16,1H18V3H19C20.11,3 21,3.9 21,5V19C21,20.11 20.11,21 19,21H5C3.89,21 3,20.1 3,19V5C3,3.89 3.89,3 5,3H6V1H8V3H16V1M11,9.5H13V12.5H16V14.5H13V17.5H11V14.5H8V12.5H11V9.5Z" />
android:pathData="M6,1L6,3L5,3C3.89,3 3,3.89 3,5v14c0,1.1 0.89,2 2,2h14c1.11,0 2,-0.89 2,-2L21,5C21,3.9 20.11,3 19,3L18,3L18,1L16,1L16,3L8,3L8,1ZM5,5L19,5L19,7L5,7ZM5,9L19,9L19,19L5,19ZM11,10v3L8,13v2h3v3h2v-3h3v-2h-3v-3z" />
</vector>

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