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>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" /> <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" 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="WRAP_ELVIS_EXPRESSIONS" value="0" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings> </JetCodeStyleSettings>
<MarkdownNavigatorCodeStyleSettings>
<option name="RIGHT_MARGIN" value="72" />
</MarkdownNavigatorCodeStyleSettings>
<codeStyleSettings language="XML"> <codeStyleSettings language="XML">
<indentOptions> <indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" /> <option name="CONTINUATION_INDENT_SIZE" value="4" />
@ -143,13 +134,11 @@
</arrangement> </arrangement>
</codeStyleSettings> </codeStyleSettings>
<codeStyleSettings language="kotlin"> <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_DECLARATIONS" value="1" />
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" /> <option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" /> <option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" /> <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> <indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" /> <option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions> </indentOptions>

View File

@ -11,19 +11,22 @@ apply from: 'hooks.gradle'
android { android {
compileSdkVersion 30 compileSdkVersion 30
buildToolsVersion '30.0.2' buildToolsVersion '30.0.3'
defaultConfig { defaultConfig {
applicationId "io.github.wulkanowy" applicationId "io.github.wulkanowy"
testApplicationId "io.github.tests.wulkanowy" testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 17 minSdkVersion 17
targetSdkVersion 30 targetSdkVersion 30
versionCode 81 versionCode 82
versionName "0.24.3" versionName "0.25.0"
multiDexEnabled true multiDexEnabled true
resValue "string", "app_name", "Wulkanowy"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
resValue "string", "app_name", "Wulkanowy"
buildConfigField "long", "BUILD_TIMESTAMP", String.valueOf(System.currentTimeMillis())
manifestPlaceholders = [ manifestPlaceholders = [
firebase_enabled: project.hasProperty("enableFirebase") firebase_enabled: project.hasProperty("enableFirebase")
] ]
@ -126,19 +129,20 @@ play {
serviceAccountCredentials = file('key.p12') serviceAccountCredentials = file('key.p12')
defaultToAppBundles = false defaultToAppBundles = false
track = 'alpha' track = 'alpha'
updatePriority = 5 updatePriority = 3
} }
ext { ext {
work_manager = "2.4.0" work_manager = "2.5.0"
room = "2.2.6" work_hilt = "1.0.0-alpha03"
room = "2.3.0-beta01"
chucker = "3.4.0" chucker = "3.4.0"
mockk = "1.10.5" mockk = "1.10.5"
moshi = "1.11.0" moshi = "1.11.0"
} }
dependencies { 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' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
@ -159,10 +163,9 @@ dependencies {
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.0.4" implementation "androidx.constraintlayout:constraintlayout:2.0.4"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0" 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.wulkanowy:material-chips-input:2.1.1"
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0" 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" implementation "androidx.work:work-runtime-ktx:$work_manager"
playImplementation "androidx.work:work-gcm:$work_manager" playImplementation "androidx.work:work-gcm:$work_manager"
@ -175,12 +178,12 @@ dependencies {
implementation "com.google.dagger:hilt-android:$hilt_version" implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version" kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
implementation 'androidx.hilt:hilt-work:1.0.0-alpha02' implementation "androidx.hilt:hilt-work:$work_hilt"
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02' kapt "androidx.hilt:hilt-compiler:$work_hilt"
implementation "com.aurelhubert:ahbottomnavigation:2.3.4" implementation "com.aurelhubert:ahbottomnavigation:2.3.4"
implementation "com.ncapdevi:frag-nav:3.3.0" 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:$moshi"
implementation "com.squareup.moshi:moshi-adapters:$moshi" implementation "com.squareup.moshi:moshi-adapters:$moshi"
@ -194,7 +197,7 @@ dependencies {
implementation "io.github.wulkanowy:AppKillerManager:3.0.0" implementation "io.github.wulkanowy:AppKillerManager:3.0.0"
implementation 'me.xdrop:fuzzywuzzy:1.3.1' 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-analytics-ktx'
playImplementation 'com.google.firebase:firebase-inappmessaging-display-ktx' playImplementation 'com.google.firebase:firebase-inappmessaging-display-ktx'
playImplementation "com.google.firebase:firebase-inappmessaging-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' 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.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" releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"
@ -214,6 +217,7 @@ dependencies {
testImplementation "junit:junit:4.13.1" testImplementation "junit:junit:4.13.1"
testImplementation "io.mockk:mockk:$mockk" testImplementation "io.mockk:mockk:$mockk"
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2' 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:core:1.3.0"
androidTestImplementation "androidx.test:runner: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" /> <action android:name="android.intent.action.VIEW" />
<data android:scheme="https" /> <data android:scheme="https" />
</intent> </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> </queries>
<application <application

View File

@ -5,6 +5,7 @@ import android.content.Context
import android.util.Log.DEBUG import android.util.Log.DEBUG
import android.util.Log.INFO import android.util.Log.INFO
import android.util.Log.VERBOSE import android.util.Log.VERBOSE
import android.webkit.WebView
import androidx.hilt.work.HiltWorkerFactory import androidx.hilt.work.HiltWorkerFactory
import androidx.multidex.MultiDex import androidx.multidex.MultiDex
import androidx.work.Configuration import androidx.work.Configuration
@ -47,22 +48,23 @@ class WulkanowyApp : Application(), Configuration.Provider {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Lingver.init(this)
themeManager.applyDefaultTheme()
initializeAppLanguage()
themeManager.applyDefaultTheme()
initLogging() initLogging()
logCurrentLanguage() fixWebViewLocale()
} }
private fun initLogging() { private fun initLogging() {
if (appInfo.isDebug) { if (appInfo.isDebug) {
Timber.plant(DebugLogTree()) Timber.plant(DebugLogTree())
Timber.plant(FileLoggerTree.Builder() Timber.plant(
.withFileName("wulkanowy.%g.log") FileLoggerTree.Builder()
.withDirName(applicationContext.filesDir.absolutePath) .withFileName("wulkanowy.%g.log")
.withFileLimit(10) .withDirName(applicationContext.filesDir.absolutePath)
.withMinPriority(DEBUG) .withFileLimit(10)
.build() .withMinPriority(DEBUG)
.build()
) )
} else { } else {
Timber.plant(CrashLogExceptionTree()) Timber.plant(CrashLogExceptionTree())
@ -71,14 +73,20 @@ class WulkanowyApp : Application(), Configuration.Provider {
registerActivityLifecycleCallbacks(ActivityLifecycleLogger()) registerActivityLifecycleCallbacks(ActivityLifecycleLogger())
} }
private fun logCurrentLanguage() { private fun initializeAppLanguage() {
val newLang = if (preferencesRepository.appLanguage == "system") { Lingver.init(this)
appInfo.systemLanguage
} else {
preferencesRepository.appLanguage
}
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() override fun getWorkManagerConfiguration() = Configuration.Builder()

View File

@ -33,17 +33,21 @@ internal class RepositoryModule {
setSimpleHttpLogger { Timber.d(it) } setSimpleHttpLogger { Timber.d(it) }
// for debug only // for debug only
addInterceptor(ChuckerInterceptor.Builder(context) addInterceptor(
.collector(chuckerCollector) ChuckerInterceptor.Builder(context)
.alwaysReadResponseBody(true) .collector(chuckerCollector)
.build(), network = true .alwaysReadResponseBody(true)
.build(), network = true
) )
} }
} }
@Singleton @Singleton
@Provides @Provides
fun provideChuckerCollector(@ApplicationContext context: Context, prefRepository: PreferencesRepository): ChuckerCollector { fun provideChuckerCollector(
@ApplicationContext context: Context,
prefRepository: PreferencesRepository
): ChuckerCollector {
return ChuckerCollector( return ChuckerCollector(
context = context, context = context,
showNotification = prefRepository.isDebugNotificationEnable, showNotification = prefRepository.isDebugNotificationEnable,
@ -53,7 +57,10 @@ internal class RepositoryModule {
@Singleton @Singleton
@Provides @Provides
fun provideDatabase(@ApplicationContext context: Context, sharedPrefProvider: SharedPrefProvider) = AppDatabase.newInstance(context, sharedPrefProvider) fun provideDatabase(
@ApplicationContext context: Context,
sharedPrefProvider: SharedPrefProvider,
) = AppDatabase.newInstance(context, sharedPrefProvider)
@Singleton @Singleton
@Provides @Provides
@ -65,7 +72,8 @@ internal class RepositoryModule {
@Singleton @Singleton
@Provides @Provides
fun provideSharedPref(@ApplicationContext context: Context): SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) fun provideSharedPref(@ApplicationContext context: Context): SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context)
@Singleton @Singleton
@Provides @Provides
@ -89,7 +97,8 @@ internal class RepositoryModule {
@Singleton @Singleton
@Provides @Provides
fun provideGradeSemesterStatisticsDao(database: AppDatabase) = database.gradeSemesterStatisticsDao fun provideGradeSemesterStatisticsDao(database: AppDatabase) =
database.gradeSemesterStatisticsDao
@Singleton @Singleton
@Provides @Provides
@ -166,4 +175,8 @@ internal class RepositoryModule {
@Singleton @Singleton
@Provides @Provides
fun provideTimetableAdditionalDao(database: AppDatabase) = database.timetableAdditionalDao 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.SchoolDao
import io.github.wulkanowy.data.db.dao.SemesterDao import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao 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.SubjectDao
import io.github.wulkanowy.data.db.dao.TeacherDao import io.github.wulkanowy.data.db.dao.TeacherDao
import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao 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.School
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentInfo
import io.github.wulkanowy.data.db.entities.Subject import io.github.wulkanowy.data.db.entities.Subject
import io.github.wulkanowy.data.db.entities.Teacher import io.github.wulkanowy.data.db.entities.Teacher
import io.github.wulkanowy.data.db.entities.Timetable 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.Migration29
import io.github.wulkanowy.data.db.migrations.Migration3 import io.github.wulkanowy.data.db.migrations.Migration3
import io.github.wulkanowy.data.db.migrations.Migration30 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.Migration4
import io.github.wulkanowy.data.db.migrations.Migration5 import io.github.wulkanowy.data.db.migrations.Migration5
import io.github.wulkanowy.data.db.migrations.Migration6 import io.github.wulkanowy.data.db.migrations.Migration6
@ -116,6 +120,7 @@ import javax.inject.Singleton
School::class, School::class,
Conference::class, Conference::class,
TimetableAdditional::class, TimetableAdditional::class,
StudentInfo::class,
], ],
version = AppDatabase.VERSION_SCHEMA, version = AppDatabase.VERSION_SCHEMA,
exportSchema = true exportSchema = true
@ -124,7 +129,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
companion object { companion object {
const val VERSION_SCHEMA = 30 const val VERSION_SCHEMA = 32
fun getMigrations(sharedPrefProvider: SharedPrefProvider): Array<Migration> { fun getMigrations(sharedPrefProvider: SharedPrefProvider): Array<Migration> {
return arrayOf( return arrayOf(
@ -157,6 +162,8 @@ abstract class AppDatabase : RoomDatabase() {
Migration28(), Migration28(),
Migration29(), Migration29(),
Migration30(), Migration30(),
Migration31(),
Migration32()
) )
} }
@ -219,4 +226,6 @@ abstract class AppDatabase : RoomDatabase() {
abstract val conferenceDao: ConferenceDao abstract val conferenceDao: ConferenceDao
abstract val timetableAdditionalDao: TimetableAdditionalDao 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.OnConflictStrategy.ABORT
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction import androidx.room.Transaction
import androidx.room.Update
import io.github.wulkanowy.data.db.entities.Student 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.db.entities.StudentWithSemesters
import javax.inject.Singleton import javax.inject.Singleton
@ -20,6 +22,9 @@ interface StudentDao {
@Delete @Delete
suspend fun delete(student: Student) suspend fun delete(student: Student)
@Update(entity = Student::class)
suspend fun update(studentNick: StudentNick)
@Query("SELECT * FROM Students WHERE is_current = 1") @Query("SELECT * FROM Students WHERE is_current = 1")
suspend fun loadCurrent(): Student? 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.io.Serializable
import java.time.LocalDateTime 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( data class Student(
@ColumnInfo(name = "scrapper_base_url") @ColumnInfo(name = "scrapper_base_url")
@ -52,7 +58,7 @@ data class Student(
@ColumnInfo(name = "school_id") @ColumnInfo(name = "school_id")
val schoolSymbol: String, val schoolSymbol: String,
@ColumnInfo(name ="school_short") @ColumnInfo(name = "school_short")
val schoolShortName: String, val schoolShortName: String,
@ColumnInfo(name = "school_name") @ColumnInfo(name = "school_name")
@ -73,4 +79,6 @@ data class Student(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
var id: Long = 0 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 private val sdk: Sdk
) { ) {
fun getSchoolInfo(student: Student, semester: Semester, forceRefresh: Boolean) = networkBoundResource( fun getSchoolInfo(student: Student, semester: Semester, forceRefresh: Boolean) =
shouldFetch = { it == null || forceRefresh }, networkBoundResource(
query = { schoolDb.load(semester.studentId, semester.classId) }, shouldFetch = { it == null || forceRefresh },
fetch = { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).getSchool().mapToEntity(semester) }, query = { schoolDb.load(semester.studentId, semester.classId) },
saveFetchResult = { old, new -> fetch = {
if (new != old && old != null) { sdk.init(student).switchDiary(semester.diaryId, semester.schoolYear).getSchool()
schoolDb.deleteAll(listOf(old)) .mapToEntity(semester)
schoolDb.insertAll(listOf(new)) },
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.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.entities.Student 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.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.data.mappers.mapToEntities import io.github.wulkanowy.data.mappers.mapToEntities
@ -25,49 +26,70 @@ class StudentRepository @Inject constructor(
private val sdk: Sdk 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> { suspend fun getStudentsApi(
return sdk.getStudentsFromMobileApi(token, pin, symbol, "").mapToEntities() 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> { suspend fun getStudentsScrapper(
return sdk.getStudentsFromScrapper(email, password, scrapperBaseUrl, symbol).mapToEntities(password) 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> { suspend fun getStudentsHybrid(
return sdk.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol).mapToEntities(password) 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) { suspend fun getSavedStudents(decryptPass: Boolean = true) =
studentDb.loadStudentsWithSemesters().map { withContext(dispatchers.backgroundThread) {
it.apply { studentDb.loadStudentsWithSemesters().map {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) student.password = decrypt(student.password) 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) { suspend fun getStudentById(id: Int) = withContext(dispatchers.backgroundThread) {
studentDb.loadById(id)?.apply { 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() } ?: throw NoCurrentStudentException()
suspend fun getCurrentStudent(decryptPass: Boolean = true) = withContext(dispatchers.backgroundThread) { suspend fun getCurrentStudent(decryptPass: Boolean = true) =
studentDb.loadCurrent()?.apply { withContext(dispatchers.backgroundThread) {
if (decryptPass && Sdk.Mode.valueOf(loginMode) != Sdk.Mode.API) password = decrypt(password) studentDb.loadCurrent()?.apply {
} if (decryptPass && Sdk.Mode.valueOf(loginMode) != Sdk.Mode.API) {
} ?: throw NoCurrentStudentException() password = decrypt(password)
}
}
} ?: throw NoCurrentStudentException()
suspend fun saveStudents(studentsWithSemesters: List<StudentWithSemesters>): List<Long> { suspend fun saveStudents(studentsWithSemesters: List<StudentWithSemesters>): List<Long> {
semesterDb.insertSemesters(studentsWithSemesters.flatMap { it.semesters }) semesterDb.insertSemesters(studentsWithSemesters.flatMap { it.semesters })
return withContext(dispatchers.backgroundThread) { return withContext(dispatchers.backgroundThread) {
studentDb.insertAll(studentsWithSemesters.map { it.student }.map { studentDb.insertAll(studentsWithSemesters.map { it.student }.map {
if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) it.copy(password = encrypt(it.password, context)) if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) {
else it it.copy(password = encrypt(it.password, context))
} else it
}) })
} }
} }
@ -79,7 +101,7 @@ class StudentRepository @Inject constructor(
} }
} }
suspend fun logoutStudent(student: Student) { suspend fun logoutStudent(student: Student) = studentDb.delete(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.services.alarm.TimetableNotificationReceiver.Companion.STUDENT_NAME
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.DispatchersProvider import io.github.wulkanowy.utils.DispatchersProvider
import io.github.wulkanowy.utils.nickOrName
import io.github.wulkanowy.utils.toTimestamp import io.github.wulkanowy.utils.toTimestamp
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
@ -41,17 +42,23 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
private val dispatchersProvider: DispatchersProvider, 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 { private fun getUpcomingLessonTime(
return day.getOrNull(index - 1)?.end ?: lesson.start.minusMinutes(30) 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) { suspend fun cancelScheduled(lessons: List<Timetable>, studentId: Int = 1) {
withContext(dispatchersProvider.backgroundThread) { withContext(dispatchersProvider.backgroundThread) {
lessons.sortedBy { it.start }.forEachIndexed { index, lesson -> lessons.sortedBy { it.start }.forEachIndexed { index, lesson ->
val upcomingTime = getUpcomingLessonTime(index, lessons, 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)) cancelScheduledTo(lesson.start..lesson.end, getRequestCode(lesson.start, studentId))
Timber.d("TimetableNotification canceled: type 1 & 2, subject: ${lesson.subject}, start: ${lesson.start}, student: $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) { private fun cancelScheduledTo(range: ClosedRange<LocalDateTime>, requestCode: Int) {
if (now() in range) cancelNotification() 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) { 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) { withContext(dispatchersProvider.backgroundThread) {
lessons.groupBy { it.date } lessons.groupBy { it.date }
@ -82,13 +94,28 @@ class TimetableNotificationSchedulerHelper @Inject constructor(
val intent = createIntent(student, lesson, active.getOrNull(index + 1)) val intent = createIntent(student, lesson, active.getOrNull(index + 1))
if (lesson.start > now()) { 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()) { 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) { 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 { private fun createIntent(student: Student, lesson: Timetable, nextLesson: Timetable?): Intent {
return Intent(context, TimetableNotificationReceiver::class.java).apply { return Intent(context, TimetableNotificationReceiver::class.java).apply {
putExtra(STUDENT_ID, student.studentId) putExtra(STUDENT_ID, student.studentId)
putExtra(STUDENT_NAME, student.studentName) putExtra(STUDENT_NAME, student.nickOrName)
putExtra(LESSON_ROOM, lesson.room) putExtra(LESSON_ROOM, lesson.room)
putExtra(LESSON_START, lesson.start.toTimestamp()) putExtra(LESSON_START, lesson.start.toTimestamp())
putExtra(LESSON_END, lesson.end.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) { private fun scheduleBroadcast(
AlarmManagerCompat.setExactAndAllowWhileIdle(alarmManager, RTC_WAKEUP, time.toTimestamp(), intent: Intent,
studentId: Int,
notificationType: Int,
time: LocalDateTime
) {
AlarmManagerCompat.setExactAndAllowWhileIdle(
alarmManager, RTC_WAKEUP, time.toTimestamp(),
PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also { PendingIntent.getBroadcast(context, getRequestCode(time, studentId), intent.also {
it.putExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id) it.putExtra(NOTIFICATION_ID, MainView.Section.TIMETABLE.id)
it.putExtra(LESSON_TYPE, notificationType) it.putExtra(LESSON_TYPE, notificationType)
}, FLAG_UPDATE_CURRENT) }, 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.BigTextStyle
import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT import androidx.core.app.NotificationCompat.PRIORITY_DEFAULT
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.hilt.Assisted import androidx.hilt.work.HiltWorker
import androidx.hilt.work.WorkerInject
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.Data import androidx.work.Data
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository import io.github.wulkanowy.data.repositories.SemesterRepository
@ -23,7 +24,8 @@ import kotlinx.coroutines.coroutineScope
import timber.log.Timber import timber.log.Timber
import kotlin.random.Random import kotlin.random.Random
class SyncWorker @WorkerInject constructor( @HiltWorker
class SyncWorker @AssistedInject constructor(
@Assisted appContext: Context, @Assisted appContext: Context,
@Assisted workerParameters: WorkerParameters, @Assisted workerParameters: WorkerParameters,
private val studentRepository: StudentRepository, private val studentRepository: StudentRepository,
@ -58,9 +60,10 @@ class SyncWorker @WorkerInject constructor(
} }
val result = when { val result = when {
exceptions.isNotEmpty() && inputData.getBoolean("one_time", false) -> { exceptions.isNotEmpty() && inputData.getBoolean("one_time", false) -> {
Result.failure(Data.Builder() Result.failure(
.putString("error", exceptions.map { it.stackTraceToString() }.toString()) Data.Builder()
.build() .putString("error", exceptions.map { it.stackTraceToString() }.toString())
.build()
) )
} }
exceptions.isNotEmpty() -> Result.retry() exceptions.isNotEmpty() -> Result.retry()
@ -74,13 +77,16 @@ class SyncWorker @WorkerInject constructor(
} }
private fun notify(result: Result) { private fun notify(result: Result) {
notificationManager.notify(Random.nextInt(Int.MAX_VALUE), NotificationCompat.Builder(applicationContext, DebugChannel.CHANNEL_ID) notificationManager.notify(
.setContentTitle("Debug notification") Random.nextInt(Int.MAX_VALUE),
.setSmallIcon(R.drawable.ic_stat_push) NotificationCompat.Builder(applicationContext, DebugChannel.CHANNEL_ID)
.setAutoCancel(true) .setContentTitle("Debug notification")
.setColor(applicationContext.getCompatColor(R.color.colorPrimary)) .setSmallIcon(R.drawable.ic_stat_push)
.setStyle(BigTextStyle().bigText("${SyncWorker::class.java.simpleName} result: $result")) .setAutoCancel(true)
.setPriority(PRIORITY_DEFAULT) .setColor(applicationContext.getCompatColor(R.color.colorPrimary))
.build()) .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 return DialogErrorBinding.inflate(inflater).apply { binding = this }.root
} }
@ -114,11 +114,17 @@ class ErrorDialog : BaseDialogFragment<DialogErrorBinding>() {
chooserTitle = getString(R.string.about_feedback), chooserTitle = getString(R.string.about_feedback),
email = "wulkanowyinc@gmail.com", email = "wulkanowyinc@gmail.com",
subject = "Zgłoszenie błędu", subject = "Zgłoszenie błędu",
body = requireContext().getString(R.string.about_feedback_template, body = requireContext().getString(
"${appInfo.systemManufacturer} ${appInfo.systemModel}", appInfo.systemVersion.toString(), appInfo.versionName R.string.about_feedback_template,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
"${appInfo.versionName}-${appInfo.buildFlavor}"
) + "\n" + content, ) + "\n" + content,
onActivityNotFound = { 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.data.db.entities.Student
import io.github.wulkanowy.databinding.ItemAccountBinding import io.github.wulkanowy.databinding.ItemAccountBinding
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.nickOrName
import javax.inject.Inject import javax.inject.Inject
class WidgetConfigureAdapter @Inject constructor() : RecyclerView.Adapter<WidgetConfigureAdapter.ItemViewHolder>() { class WidgetConfigureAdapter @Inject constructor() : RecyclerView.Adapter<WidgetConfigureAdapter.ItemViewHolder>() {
@ -28,7 +29,7 @@ class WidgetConfigureAdapter @Inject constructor() : RecyclerView.Adapter<Widget
val (student, isCurrent) = items[position] val (student, isCurrent) = items[position]
with(holder.binding) { with(holder.binding) {
accountItemName.text = "${student.studentName} ${student.className}" accountItemName.text = "${student.nickOrName} ${student.className}"
accountItemSchool.text = student.schoolName accountItemSchool.text = student.schoolName
with(accountItemImage) { 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.openAppInMarket
import io.github.wulkanowy.utils.openEmailClient import io.github.wulkanowy.utils.openEmailClient
import io.github.wulkanowy.utils.openInternetBrowser import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.toFormattedString
import io.github.wulkanowy.utils.toLocalDateTime
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -35,7 +37,9 @@ class AboutFragment : BaseFragment<FragmentAboutBinding>(R.layout.fragment_about
override val versionRes: Triple<String, String, Drawable?>? override val versionRes: Triple<String, String, Drawable?>?
get() = context?.run { 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?>? 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?>? override val homepageRes: Triple<String, String, Drawable?>?
get() = context?.run { 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?>? 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), chooserTitle = getString(R.string.about_feedback),
email = "wulkanowyinc@gmail.com", email = "wulkanowyinc@gmail.com",
subject = "Zgłoszenie błędu", subject = "Zgłoszenie błędu",
body = getString(R.string.about_feedback_template, body = getString(
"${appInfo.systemManufacturer} ${appInfo.systemModel}", appInfo.systemVersion.toString(), appInfo.versionName R.string.about_feedback_template,
"${appInfo.systemManufacturer} ${appInfo.systemModel}",
appInfo.systemVersion.toString(),
"${appInfo.versionName}-${appInfo.buildFlavor}"
), ),
onActivityNotFound = { 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 android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.databinding.HeaderAccountBinding import io.github.wulkanowy.databinding.HeaderAccountBinding
import io.github.wulkanowy.databinding.ItemAccountBinding import io.github.wulkanowy.databinding.ItemAccountBinding
import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.nickOrName
import javax.inject.Inject import javax.inject.Inject
class AccountAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() { class AccountAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var isAccountQuickDialogMode = false
var items = emptyList<AccountItem<*>>() var items = emptyList<AccountItem<*>>()
var onClickListener: (StudentWithSemesters) -> Unit = {} var onClickListener: (StudentWithSemesters) -> Unit = {}
@ -30,54 +31,69 @@ class AccountAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.V
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
return when (viewType) { return when (viewType) {
AccountItem.ViewType.HEADER.id -> HeaderViewHolder(HeaderAccountBinding.inflate(inflater, parent, false)) AccountItem.ViewType.HEADER.id -> HeaderViewHolder(
AccountItem.ViewType.ITEM.id -> ItemViewHolder(ItemAccountBinding.inflate(inflater, parent, false)) HeaderAccountBinding.inflate(inflater, parent, false)
)
AccountItem.ViewType.ITEM.id -> ItemViewHolder(
ItemAccountBinding.inflate(inflater, parent, false)
)
else -> throw IllegalStateException() else -> throw IllegalStateException()
} }
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) { when (holder) {
is HeaderViewHolder -> bindHeaderViewHolder(holder.binding, items[position].value as Account) is HeaderViewHolder -> bindHeaderViewHolder(
is ItemViewHolder -> bindItemViewHolder(holder.binding, items[position].value as StudentWithSemesters) 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) { with(binding) {
accountHeaderDivider.visibility = if (position == 0) GONE else VISIBLE
accountHeaderEmail.text = account.email accountHeaderEmail.text = account.email
accountHeaderType.setText(if (account.isParent) R.string.account_type_parent else R.string.account_type_student) accountHeaderType.setText(if (account.isParent) R.string.account_type_parent else R.string.account_type_student)
} }
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
private fun bindItemViewHolder(binding: ItemAccountBinding, studentWithSemesters: StudentWithSemesters) { private fun bindItemViewHolder(
binding: ItemAccountBinding,
studentWithSemesters: StudentWithSemesters
) {
val student = studentWithSemesters.student val student = studentWithSemesters.student
val semesters = studentWithSemesters.semesters val semesters = studentWithSemesters.semesters
val diary = semesters.maxByOrNull { it.semesterId } 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) { with(binding) {
accountItemName.text = "${student.studentName} ${diary?.diaryName.orEmpty()}" accountItemName.text = "${student.nickOrName} ${diary?.diaryName.orEmpty()}"
accountItemSchool.text = studentWithSemesters.student.schoolName accountItemSchool.text = studentWithSemesters.student.schoolName
with(accountItemLoginMode) { accountItemAccountType.setText(if (student.isParent) R.string.account_type_parent else R.string.account_type_student)
visibility = when (Sdk.Mode.valueOf(student.loginMode)) { accountItemAccountType.visibility = if (isDuplicatedStudent) VISIBLE else GONE
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
}
}
}
with(accountItemImage) { with(accountItemImage) {
val colorImage = if (student.isCurrent) context.getThemeAttrColor(R.attr.colorPrimary) val colorImage =
else context.getThemeAttrColor(R.attr.colorOnSurface, 153) if (student.isCurrent) context.getThemeAttrColor(R.attr.colorPrimary)
else context.getThemeAttrColor(R.attr.colorOnSurface, 153)
setColorFilter(colorImage, PorterDuff.Mode.SRC_IN) 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.Status
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.repositories.StudentRepository 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.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.afterLoading
@ -15,101 +14,91 @@ import javax.inject.Inject
class AccountPresenter @Inject constructor( class AccountPresenter @Inject constructor(
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val syncManager: SyncManager
) : BasePresenter<AccountView>(errorHandler, studentRepository) { ) : BasePresenter<AccountView>(errorHandler, studentRepository) {
private lateinit var lastError: Throwable
override fun onAttachView(view: AccountView) { override fun onAttachView(view: AccountView) {
super.onAttachView(view) super.onAttachView(view)
view.initView() view.initView()
Timber.i("Account dialog view was initialized") Timber.i("Account view was initialized")
errorHandler.showErrorMessage = ::showErrorViewOnError
loadData() loadData()
} }
fun onRetry() {
view?.run {
showErrorView(false)
showProgress(true)
}
loadData()
}
fun onDetailsClick() {
view?.showErrorDetailsDialog(lastError)
}
fun onAddSelected() { fun onAddSelected() {
Timber.i("Select add account") Timber.i("Select add account")
view?.openLoginView() 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) { fun onItemSelected(studentWithSemesters: StudentWithSemesters) {
Timber.i("Select student item ${studentWithSemesters.student.id}") view?.openAccountDetailsView(studentWithSemesters)
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")
} }
private fun createAccountItems(items: List<StudentWithSemesters>): List<AccountItem<*>> { private fun createAccountItems(items: List<StudentWithSemesters>): List<AccountItem<*>> {
return items.groupBy { Account(it.student.email, it.student.isParent) }.map { (account, students) -> return items.groupBy {
listOf(AccountItem(account, AccountItem.ViewType.HEADER)) + students.map { student -> Account("${it.student.userName} (${it.student.email})", it.student.isParent)
AccountItem(student, AccountItem.ViewType.ITEM) }
.map { (account, students) ->
listOf(
AccountItem(account, AccountItem.ViewType.HEADER)
) + students.map { student ->
AccountItem(student, AccountItem.ViewType.ITEM)
}
} }
}.flatten() .flatten()
} }
private fun loadData() { private fun loadData() {
flowWithResource { studentRepository.getSavedStudents(false) }.onEach { flowWithResource { studentRepository.getSavedStudents(false) }
when (it.status) { .onEach {
Status.LOADING -> Timber.i("Loading account data started") when (it.status) {
Status.SUCCESS -> { Status.LOADING -> {
Timber.i("Loading account result: Success") Timber.i("Loading account data started")
view?.updateData(createAccountItems(it.data!!)) view?.run {
} showProgress(true)
Status.ERROR -> { showContent(false)
Timber.i("Loading account result: An exception occurred") }
errorHandler.dispatch(it.error!!) }
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 package io.github.wulkanowy.ui.modules.account
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
interface AccountView : BaseView { interface AccountView : BaseView {
val isViewEmpty: Boolean
fun initView() fun initView()
fun updateData(data: List<AccountItem<*>>) fun updateData(data: List<AccountItem<*>>)
fun dismissView()
fun showConfirmDialog()
fun openLoginView() 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.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.SchooldaysRangeLimiter import io.github.wulkanowy.utils.SchooldaysRangeLimiter
import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
@ -60,6 +61,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
override val excuseActionMode: Boolean get() = attendanceAdapter.excuseActionMode override val excuseActionMode: Boolean get() = attendanceAdapter.excuseActionMode
private var actionMode: ActionMode? = null private var actionMode: ActionMode? = null
private val actionModeCallback = object : ActionMode.Callback { private val actionModeCallback = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
val inflater = mode.menuInflater val inflater = mode.menuInflater
@ -111,6 +113,8 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
with(binding) { with(binding) {
attendanceSwipe.setOnRefreshListener(presenter::onSwipeRefresh) attendanceSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
attendanceSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
attendanceSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
attendanceErrorRetry.setOnClickListener { presenter.onRetry() } attendanceErrorRetry.setOnClickListener { presenter.onRetry() }
attendanceErrorDetails.setOnClickListener { presenter.onDetailsClick() } attendanceErrorDetails.setOnClickListener { presenter.onDetailsClick() }
@ -222,6 +226,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
setDateRangeLimiter(SchooldaysRangeLimiter()) setDateRangeLimiter(SchooldaysRangeLimiter())
version = DatePickerDialog.Version.VERSION_2 version = DatePickerDialog.Version.VERSION_2
scrollOrientation = DatePickerDialog.ScrollOrientation.VERTICAL scrollOrientation = DatePickerDialog.ScrollOrientation.VERTICAL
vibrate(false)
show(this@AttendanceFragment.parentFragmentManager, null) 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.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.setOnItemSelectedListener import io.github.wulkanowy.utils.setOnItemSelectedListener
import javax.inject.Inject import javax.inject.Inject
@ -56,6 +57,8 @@ class AttendanceSummaryFragment :
with(binding) { with(binding) {
attendanceSummarySwipe.setOnRefreshListener(presenter::onSwipeRefresh) attendanceSummarySwipe.setOnRefreshListener(presenter::onSwipeRefresh)
attendanceSummarySwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
attendanceSummarySwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
attendanceSummaryErrorRetry.setOnClickListener { presenter.onRetry() } attendanceSummaryErrorRetry.setOnClickListener { presenter.onRetry() }
attendanceSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() } 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.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -47,7 +48,9 @@ class ConferenceFragment : BaseFragment<FragmentConferenceBinding>(R.layout.frag
} }
with(binding) { 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() } conferenceErrorRetry.setOnClickListener { presenter.onRetry() }
conferenceErrorDetails.setOnClickListener { presenter.onDetailsClick() } 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.modules.main.MainView
import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -55,6 +56,8 @@ class ExamFragment : BaseFragment<FragmentExamBinding>(R.layout.fragment_exam),
with(binding) { with(binding) {
examSwipe.setOnRefreshListener(presenter::onSwipeRefresh) examSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
examSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
examSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
examErrorRetry.setOnClickListener { presenter.onRetry() } examErrorRetry.setOnClickListener { presenter.onRetry() }
examErrorDetails.setOnClickListener { presenter.onDetailsClick() } examErrorDetails.setOnClickListener { presenter.onDetailsClick() }

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.modules.grade package io.github.wulkanowy.ui.modules.grade
import io.github.wulkanowy.data.Resource 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.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Semester import io.github.wulkanowy.data.db.entities.Semester
@ -33,81 +34,162 @@ class GradeAverageProvider @Inject constructor(
private val minusModifier get() = preferencesRepository.gradeMinusModifier private val minusModifier get() = preferencesRepository.gradeMinusModifier
fun getGradesDetailsWithAverage(student: Student, semesterId: Int, forceRefresh: Boolean) = flowWithResourceIn { fun getGradesDetailsWithAverage(student: Student, semesterId: Int, forceRefresh: Boolean) =
val semesters = semesterRepository.getSemesters(student) flowWithResourceIn {
val semesters = semesterRepository.getSemesters(student)
when (preferencesRepository.gradeAverageMode) { when (preferencesRepository.gradeAverageMode) {
ONE_SEMESTER -> getSemesterDetailsWithAverage(student, semesters.single { it.semesterId == semesterId }, forceRefresh) ONE_SEMESTER -> getGradeSubjects(
BOTH_SEMESTERS -> calculateBothSemestersAverage(student, semesters, semesterId, forceRefresh) student = student,
ALL_YEAR -> calculateAllYearAverage(student, semesters, semesterId, forceRefresh) semester = semesters.single { it.semesterId == semesterId },
} forceRefresh = forceRefresh
}.distinctUntilChanged() )
BOTH_SEMESTERS -> calculateCombinedAverage(
private fun calculateBothSemestersAverage(student: Student, semesters: List<Semester>, semesterId: Int, forceRefresh: Boolean): Flow<Resource<List<GradeDetailsWithAverage>>> { student = student,
val selectedSemester = semesters.single { it.semesterId == semesterId } semesters = semesters,
val firstSemester = semesters.single { it.diaryId == selectedSemester.diaryId && it.semesterName == 1 } semesterId = semesterId,
forceRefresh = forceRefresh,
val selectedSemesterDetailsWithAverage = getSemesterDetailsWithAverage(student, selectedSemester, forceRefresh) averageMode = BOTH_SEMESTERS
)
return if (selectedSemester == firstSemester) selectedSemesterDetailsWithAverage else { ALL_YEAR -> calculateCombinedAverage(
val firstSemesterDetailsWithAverage = getSemesterDetailsWithAverage(student, firstSemester, forceRefresh) student = student,
selectedSemesterDetailsWithAverage.combine(firstSemesterDetailsWithAverage) { selectedDetails, secondDetails -> semesters = semesters,
val isAnyAverage = selectedDetails.data.orEmpty().any { it.average != .0 } semesterId = semesterId,
secondDetails.copy(data = selectedDetails.data?.map { selected -> forceRefresh = forceRefresh,
val second = secondDetails.data.orEmpty().singleOrNull { it.subject == selected.subject } averageMode = ALL_YEAR
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
) )
} }
}.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 if (isNotEmpty() && size > grades.size) return this
return grades.mapIndexed { i, (subject, details) -> 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 private var semesterSwitchMenu: MenuItem? = null
companion object { companion object {
private const val SAVED_SEMESTER_KEY = "CURRENT_SEMESTER"
fun newInstance() = GradeFragment() fun newInstance() = GradeFragment()
} }
@ -52,7 +51,7 @@ class GradeFragment : BaseFragment<FragmentGradeBinding>(R.layout.fragment_grade
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding = FragmentGradeBinding.bind(view) binding = FragmentGradeBinding.bind(view)
presenter.onAttachView(this, savedInstanceState?.getInt(SAVED_SEMESTER_KEY)) presenter.onAttachView(this)
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { 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() (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() { override fun onDestroyView() {
presenter.onDetachView() presenter.onDetachView()
super.onDestroyView() super.onDestroyView()

View File

@ -21,8 +21,7 @@ class GradePresenter @Inject constructor(
private val analytics: AnalyticsHelper private val analytics: AnalyticsHelper
) : BasePresenter<GradeView>(errorHandler, studentRepository) { ) : BasePresenter<GradeView>(errorHandler, studentRepository) {
var selectedIndex = 0 private var selectedIndex = 0
private set
private var schoolYear = 0 private var schoolYear = 0
@ -32,9 +31,8 @@ class GradePresenter @Inject constructor(
private lateinit var lastError: Throwable private lateinit var lastError: Throwable
fun onAttachView(view: GradeView, savedIndex: Int?) { override fun onAttachView(view: GradeView) {
super.onAttachView(view) super.onAttachView(view)
selectedIndex = savedIndex ?: 0
view.initView() view.initView()
Timber.i("Grade view was initialized with $selectedIndex index") Timber.i("Grade view was initialized with $selectedIndex index")
errorHandler.showErrorMessage = ::showErrorViewOnError 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.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary import io.github.wulkanowy.data.db.entities.GradeSummary
data class GradeDetailsWithAverage( data class GradeSubject(
val subject: String, val subject: String,
val average: Double, val average: Double,
val points: String, 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.GradeFragment
import io.github.wulkanowy.ui.modules.grade.GradeView import io.github.wulkanowy.ui.modules.grade.GradeView
import io.github.wulkanowy.ui.modules.main.MainActivity import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -65,7 +66,9 @@ class GradeDetailsFragment :
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
adapter = gradeDetailsAdapter 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() } gradeDetailsErrorRetry.setOnClickListener { presenter.onRetry() }
gradeDetailsErrorDetails.setOnClickListener { presenter.onDetailsClick() } 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.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider 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.ALPHABETIC
import io.github.wulkanowy.ui.modules.grade.GradeSortingMode.DATE 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.AnalyticsHelper
import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.afterLoading
import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.flowWithResource
@ -201,8 +201,9 @@ class GradeDetailsPresenter @Inject constructor(
}.launch() }.launch()
} }
private fun updateNewGradesAmount(grades: List<GradeDetailsWithAverage>) { private fun updateNewGradesAmount(grades: List<GradeSubject>) {
newGradesAmount = grades.sumBy { item -> item.grades.sumBy { grade -> if (!grade.isRead) 1 else 0 } } newGradesAmount =
grades.sumBy { item -> item.grades.sumBy { grade -> if (!grade.isRead) 1 else 0 } }
} }
private fun showErrorViewOnError(message: String, error: Throwable) { private fun showErrorViewOnError(message: String, error: Throwable) {
@ -217,7 +218,7 @@ class GradeDetailsPresenter @Inject constructor(
} }
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
private fun createGradeItems(items: List<GradeDetailsWithAverage>): List<GradeDetailsItem> { private fun createGradeItems(items: List<GradeSubject>): List<GradeDetailsItem> {
return items return items
.let { gradesWithAverages -> .let { gradesWithAverages ->
if (!preferencesRepository.showSubjectsWithoutGrades) { if (!preferencesRepository.showSubjectsWithoutGrades) {

View File

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

View File

@ -172,6 +172,7 @@ class GradeStatisticsPresenter @Inject constructor(
showErrorView(false) showErrorView(false)
enableSwipe(true) enableSwipe(true)
showRefresh(true) showRefresh(true)
showProgress(false)
updateData(it.data!!, preferencesRepository.gradeColorTheme, preferencesRepository.showAllSubjectsOnStatisticsList) updateData(it.data!!, preferencesRepository.gradeColorTheme, preferencesRepository.showAllSubjectsOnStatisticsList)
showSubjects(!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.base.BaseFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.grade.GradeView import io.github.wulkanowy.ui.modules.grade.GradeView
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -52,7 +53,9 @@ class GradeSummaryFragment :
adapter = gradeSummaryAdapter adapter = gradeSummaryAdapter
} }
with(binding) { 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() } gradeSummaryErrorRetry.setOnClickListener { presenter.onRetry() }
gradeSummaryErrorDetails.setOnClickListener { presenter.onDetailsClick() } 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.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.ui.modules.grade.GradeAverageProvider 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.AnalyticsHelper
import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.afterLoading
import io.github.wulkanowy.utils.flowWithResourceIn import io.github.wulkanowy.utils.flowWithResourceIn
@ -135,14 +135,14 @@ class GradeSummaryPresenter @Inject constructor(
cancelJobs("load") cancelJobs("load")
} }
private fun createGradeSummaryItems(items: List<GradeDetailsWithAverage>): List<GradeSummary> { private fun createGradeSummaryItems(items: List<GradeSubject>): List<GradeSummary> {
return items return items
.filter { !checkEmpty(it) } .filter { !checkEmpty(it) }
.sortedBy { it.subject } .sortedBy { it.subject }
.map { it.summary.copy(average = it.average) } .map { it.summary.copy(average = it.average) }
} }
private fun checkEmpty(gradeSummary: GradeDetailsWithAverage): Boolean { private fun checkEmpty(gradeSummary: GradeSubject): Boolean {
return gradeSummary.run { return gradeSummary.run {
summary.finalGrade.isBlank() summary.finalGrade.isBlank()
&& summary.predictedGrade.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.modules.main.MainView
import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -55,6 +56,8 @@ class HomeworkFragment : BaseFragment<FragmentHomeworkBinding>(R.layout.fragment
with(binding) { with(binding) {
homeworkSwipe.setOnRefreshListener(presenter::onSwipeRefresh) homeworkSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
homeworkSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
homeworkSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
homeworkErrorRetry.setOnClickListener { presenter.onRetry() } homeworkErrorRetry.setOnClickListener { presenter.onRetry() }
homeworkErrorDetails.setOnClickListener { presenter.onDetailsClick() } 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.databinding.FragmentLuckyNumberBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -38,7 +39,9 @@ class LuckyNumberFragment :
override fun initView() { override fun initView() {
with(binding) { 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() } luckyNumberErrorRetry.setOnClickListener { presenter.onRetry() }
luckyNumberErrorDetails.setOnClickListener { presenter.onDetailsClick() } luckyNumberErrorDetails.setOnClickListener { presenter.onDetailsClick() }
} }

View File

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

View File

@ -64,6 +64,8 @@ interface MainView : BaseView {
LUCKY_NUMBER(8), LUCKY_NUMBER(8),
SETTINGS(9), SETTINGS(9),
ABOUT(10), 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 this@MessagePreviewPresenter.attachments = it.data.attachments
view?.apply { view?.apply {
setMessageWithAttachment(it.data) setMessageWithAttachment(it.data)
showContent(true)
initOptions() initOptions()
} }
analytics.logEvent( 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.MessageFragment
import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment
import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.getThemeAttrColor
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import javax.inject.Inject import javax.inject.Inject
@ -74,7 +75,9 @@ class MessageTabFragment : BaseFragment<FragmentMessageTabBinding>(R.layout.frag
addItemDecoration(DividerItemDecoration(context)) addItemDecoration(DividerItemDecoration(context))
} }
with(binding) { 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() } messageTabErrorRetry.setOnClickListener { presenter.onRetry() }
messageTabErrorDetails.setOnClickListener { presenter.onDetailsClick() } 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.main.MainView
import io.github.wulkanowy.ui.modules.mobiledevice.token.MobileDeviceTokenDialog import io.github.wulkanowy.ui.modules.mobiledevice.token.MobileDeviceTokenDialog
import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -56,7 +57,9 @@ class MobileDeviceFragment :
} }
with(binding) { 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() } mobileDevicesErrorRetry.setOnClickListener { presenter.onRetry() }
mobileDevicesErrorDetails.setOnClickListener { presenter.onDetailsClick() } mobileDevicesErrorDetails.setOnClickListener { presenter.onDetailsClick() }
mobileDeviceAddButton.setOnClickListener { presenter.onRegisterDevice() } 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.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -50,7 +51,9 @@ class NoteFragment : BaseFragment<FragmentNoteBinding>(R.layout.fragment_note),
addItemDecoration(DividerItemDecoration(context)) addItemDecoration(DividerItemDecoration(context))
} }
with(binding) { 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() } noteErrorRetry.setOnClickListener { presenter.onRetry() }
noteErrorDetails.setOnClickListener { presenter.onDetailsClick() } 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.main.MainView
import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersChildView import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersChildView
import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersFragment 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.openDialer
import io.github.wulkanowy.utils.openNavigation import io.github.wulkanowy.utils.openNavigation
import javax.inject.Inject import javax.inject.Inject
@ -39,7 +40,9 @@ class SchoolFragment : BaseFragment<FragmentSchoolBinding>(R.layout.fragment_sch
override fun initView() { override fun initView() {
with(binding) { 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() } schoolErrorRetry.setOnClickListener { presenter.onRetry() }
schoolErrorDetails.setOnClickListener { presenter.onDetailsClick() } schoolErrorDetails.setOnClickListener { presenter.onDetailsClick() }

View File

@ -81,10 +81,7 @@ class SchoolPresenter @Inject constructor(
showEmpty(false) showEmpty(false)
showErrorView(false) showErrorView(false)
} }
analytics.logEvent( analytics.logEvent("load_item", "type" to "school")
"load_item",
"type" to "school"
)
} else view?.run { } else view?.run {
Timber.i("Loading school result: No school info found") Timber.i("Loading school result: No school info found")
showContent(!isViewEmpty) 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.SchoolAndTeachersChildView
import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersFragment import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersFragment
import io.github.wulkanowy.ui.widgets.DividerItemDecoration import io.github.wulkanowy.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.getThemeAttrColor
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -51,7 +52,9 @@ class TeacherFragment : BaseFragment<FragmentTeacherBinding>(R.layout.fragment_t
addItemDecoration(DividerItemDecoration(context)) addItemDecoration(DividerItemDecoration(context))
} }
with(binding) { 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() } teacherErrorRetry.setOnClickListener { presenter.onRetry() }
teacherErrorDetails.setOnClickListener { presenter.onDetailsClick() } teacherErrorDetails.setOnClickListener { presenter.onDetailsClick() }
} }

View File

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

View File

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

View File

@ -14,6 +14,8 @@ interface SettingsView : BaseView {
fun updateLanguage(langCode: String) fun updateLanguage(langCode: String)
fun updateLanguageToFollowSystem()
fun setServicesSuspended(serviceEnablesKey: String, isHolidays: Boolean) fun setServicesSuspended(serviceEnablesKey: String, isHolidays: Boolean)
fun setSyncInProgress(inProgress: 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.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.SchooldaysRangeLimiter import io.github.wulkanowy.utils.SchooldaysRangeLimiter
import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
@ -69,6 +70,8 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
with(binding) { with(binding) {
timetableSwipe.setOnRefreshListener(presenter::onSwipeRefresh) timetableSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
timetableSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
timetableSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
timetableErrorRetry.setOnClickListener { presenter.onRetry() } timetableErrorRetry.setOnClickListener { presenter.onRetry() }
timetableErrorDetails.setOnClickListener { presenter.onDetailsClick() } timetableErrorDetails.setOnClickListener { presenter.onDetailsClick() }
@ -176,6 +179,7 @@ class TimetableFragment : BaseFragment<FragmentTimetableBinding>(R.layout.fragme
setDateRangeLimiter(SchooldaysRangeLimiter()) setDateRangeLimiter(SchooldaysRangeLimiter())
version = DatePickerDialog.Version.VERSION_2 version = DatePickerDialog.Version.VERSION_2
scrollOrientation = DatePickerDialog.ScrollOrientation.VERTICAL scrollOrientation = DatePickerDialog.ScrollOrientation.VERTICAL
vibrate(false)
show(this@TimetableFragment.parentFragmentManager, null) 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.ui.widgets.DividerItemDecoration
import io.github.wulkanowy.utils.SchooldaysRangeLimiter import io.github.wulkanowy.utils.SchooldaysRangeLimiter
import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
@ -53,6 +54,8 @@ class AdditionalLessonsFragment :
with(binding) { with(binding) {
additionalLessonsSwipe.setOnRefreshListener(presenter::onSwipeRefresh) additionalLessonsSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
additionalLessonsSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
additionalLessonsSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
additionalLessonsErrorRetry.setOnClickListener { presenter.onRetry() } additionalLessonsErrorRetry.setOnClickListener { presenter.onRetry() }
additionalLessonsPreviousButton.setOnClickListener { presenter.onPreviousDay() } additionalLessonsPreviousButton.setOnClickListener { presenter.onPreviousDay() }
@ -128,6 +131,7 @@ class AdditionalLessonsFragment :
setDateRangeLimiter(SchooldaysRangeLimiter()) setDateRangeLimiter(SchooldaysRangeLimiter())
version = DatePickerDialog.Version.VERSION_2 version = DatePickerDialog.Version.VERSION_2
scrollOrientation = DatePickerDialog.ScrollOrientation.VERTICAL scrollOrientation = DatePickerDialog.ScrollOrientation.VERTICAL
vibrate(false)
show(this@AdditionalLessonsFragment.parentFragmentManager, null) 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.SchooldaysRangeLimiter
import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getCompatDrawable import io.github.wulkanowy.utils.getCompatDrawable
import io.github.wulkanowy.utils.getThemeAttrColor
import java.time.LocalDate import java.time.LocalDate
import javax.inject.Inject import javax.inject.Inject
@ -60,6 +61,8 @@ class CompletedLessonsFragment :
with(binding) { with(binding) {
completedLessonsSwipe.setOnRefreshListener(presenter::onSwipeRefresh) completedLessonsSwipe.setOnRefreshListener(presenter::onSwipeRefresh)
completedLessonsSwipe.setColorSchemeColors(requireContext().getThemeAttrColor(R.attr.colorPrimary))
completedLessonsSwipe.setProgressBackgroundColorSchemeColor(requireContext().getThemeAttrColor(R.attr.colorSwipeRefresh))
completedLessonErrorRetry.setOnClickListener { presenter.onRetry() } completedLessonErrorRetry.setOnClickListener { presenter.onRetry() }
completedLessonErrorDetails.setOnClickListener { presenter.onDetailsClick() } completedLessonErrorDetails.setOnClickListener { presenter.onDetailsClick() }
@ -147,6 +150,7 @@ class CompletedLessonsFragment :
setDateRangeLimiter(SchooldaysRangeLimiter()) setDateRangeLimiter(SchooldaysRangeLimiter())
version = DatePickerDialog.Version.VERSION_2 version = DatePickerDialog.Version.VERSION_2
scrollOrientation = DatePickerDialog.ScrollOrientation.VERTICAL scrollOrientation = DatePickerDialog.ScrollOrientation.VERTICAL
vibrate(false)
show(this@CompletedLessonsFragment.parentFragmentManager, null) 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.AnalyticsHelper
import io.github.wulkanowy.utils.nextOrSameSchoolDay import io.github.wulkanowy.utils.nextOrSameSchoolDay
import io.github.wulkanowy.utils.nextSchoolDay import io.github.wulkanowy.utils.nextSchoolDay
import io.github.wulkanowy.utils.nickOrName
import io.github.wulkanowy.utils.previousSchoolDay import io.github.wulkanowy.utils.previousSchoolDay
import io.github.wulkanowy.utils.toFormattedString import io.github.wulkanowy.utils.toFormattedString
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -151,8 +152,14 @@ class TimetableWidgetProvider : HiltBroadcastReceiver() {
val remoteView = RemoteViews(context.packageName, layoutId).apply { val remoteView = RemoteViews(context.packageName, layoutId).apply {
setEmptyView(R.id.timetableWidgetList, R.id.timetableWidgetEmpty) setEmptyView(R.id.timetableWidgetList, R.id.timetableWidgetEmpty)
setTextViewText(R.id.timetableWidgetDate, date.toFormattedString("EEEE, dd.MM").capitalize()) setTextViewText(
setTextViewText(R.id.timetableWidgetName, student?.studentName ?: context.getString(R.string.all_no_data)) 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) setRemoteAdapter(R.id.timetableWidgetList, adapterIntent)
setOnClickPendingIntent(R.id.timetableWidgetNext, nextNavIntent) setOnClickPendingIntent(R.id.timetableWidgetNext, nextNavIntent)
setOnClickPendingIntent(R.id.timetableWidgetPrev, prevNavIntent) 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.MANUFACTURER
import android.os.Build.MODEL import android.os.Build.MODEL
import android.os.Build.VERSION.SDK_INT 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.DEBUG
import io.github.wulkanowy.BuildConfig.FLAVOR
import io.github.wulkanowy.BuildConfig.VERSION_CODE import io.github.wulkanowy.BuildConfig.VERSION_CODE
import io.github.wulkanowy.BuildConfig.VERSION_NAME import io.github.wulkanowy.BuildConfig.VERSION_NAME
import javax.inject.Inject import javax.inject.Inject
@ -17,6 +19,10 @@ open class AppInfo @Inject constructor() {
open val versionCode get() = VERSION_CODE 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 versionName get() = VERSION_NAME
open val systemVersion get() = SDK_INT open val systemVersion get() = SDK_INT
@ -28,4 +34,9 @@ open class AppInfo @Inject constructor() {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
open val systemLanguage: String open val systemLanguage: String
get() = Resources.getSystem().configuration.locale.language 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.Resource
import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.Status
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
@ -71,24 +72,14 @@ inline fun <ResultType, RequestType, T> networkBoundResource(
fun <T> flowWithResource(block: suspend () -> T) = flow { fun <T> flowWithResource(block: suspend () -> T) = flow {
emit(Resource.loading()) emit(Resource.loading())
emit(try { emit(Resource.success(block()))
Resource.success(block()) }.catch { emit(Resource.error(it)) }
} catch (e: Throwable) {
Resource.error(e)
})
}
@OptIn(FlowPreview::class)
fun <T> flowWithResourceIn(block: suspend () -> Flow<Resource<T>>) = flow { fun <T> flowWithResourceIn(block: suspend () -> Flow<Resource<T>>) = flow {
emit(Resource.loading()) emit(Resource.loading())
emitAll(block().filter { it.status != Status.LOADING || (it.status == Status.LOADING && it.data != null) })
block() }.catch { emit(Resource.error(it)) }
.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)
}
}
}
fun <T> Flow<Resource<T>>.afterLoading(callback: () -> Unit) = onEach { fun <T> Flow<Resource<T>>.afterLoading(callback: () -> Unit) = onEach {
if (it.status != Status.LOADING) callback() 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>>.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 androidx.fragment.app.Fragment
import io.github.wulkanowy.ui.modules.about.AboutFragment 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.attendance.AttendanceFragment
import io.github.wulkanowy.ui.modules.exam.ExamFragment import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment import io.github.wulkanowy.ui.modules.grade.GradeFragment
@ -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.note.NoteFragment
import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersFragment import io.github.wulkanowy.ui.modules.schoolandteachers.SchoolAndTeachersFragment
import io.github.wulkanowy.ui.modules.settings.SettingsFragment import io.github.wulkanowy.ui.modules.settings.SettingsFragment
import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
fun Fragment.toSection(): MainView.Section? { fun Fragment.toSection(): MainView.Section? {
@ -29,6 +32,9 @@ fun Fragment.toSection(): MainView.Section? {
is SettingsFragment -> MainView.Section.SETTINGS is SettingsFragment -> MainView.Section.SETTINGS
is AboutFragment -> MainView.Section.ABOUT is AboutFragment -> MainView.Section.ABOUT
is SchoolAndTeachersFragment -> MainView.Section.SCHOOL is SchoolAndTeachersFragment -> MainView.Section.SCHOOL
is AccountFragment -> MainView.Section.ACCOUNT
is AccountDetailsFragment -> MainView.Section.ACCOUNT
is StudentInfoFragment -> MainView.Section.STUDENT_INFO
else -> null else -> null
} }
} }

View File

@ -16,63 +16,58 @@ fun List<Grade>.calcAverage(): Double {
} }
@JvmName("calcSummaryAverage") @JvmName("calcSummaryAverage")
fun List<GradeSummary>.calcAverage(): Double { fun List<GradeSummary>.calcAverage() = asSequence()
return asSequence().mapNotNull { .mapNotNull {
if (it.finalGrade.matches("[0-6]".toRegex())) it.finalGrade.toDouble() else null if (it.finalGrade.matches("[0-6]".toRegex())) {
}.average().let { if (it.isNaN()) 0.0 else it } it.finalGrade.toDouble()
} } else null
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
}
} }
} .average()
.let { if (it.isNaN()) 0.0 else it }
fun Grade.getGradeColor(): Int { fun Grade.getBackgroundColor(theme: String) = when (theme) {
return when (color) { "grade_color" -> getGradeColor()
"000000" -> R.color.grade_black "material" -> when (value.toInt()) {
"F04C4C" -> R.color.grade_red 6 -> R.color.grade_material_six
"20A4F7" -> R.color.grade_blue 5 -> R.color.grade_material_five
"6ECD07" -> R.color.grade_green 4 -> R.color.grade_material_four
"B16CF1" -> R.color.grade_purple 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 -> 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 inline val Grade.colorStringId: Int
get() { get() = when (color) {
return when (color) { "000000" -> R.string.all_black
"000000" -> R.string.all_black "F04C4C" -> R.string.all_red
"F04C4C" -> R.string.all_red "20A4F7" -> R.string.all_blue
"20A4F7" -> R.string.all_blue "6ECD07" -> R.string.all_green
"6ECD07" -> R.string.all_green "B16CF1" -> R.string.all_purple
"B16CF1" -> R.string.all_purple else -> R.string.all_empty_color
else -> R.string.all_empty_color
}
} }
fun Grade.changeModifier(plusModifier: Double, minusModifier: Double): Grade { fun Grade.changeModifier(plusModifier: Double, minusModifier: Double) = when {
return when { modifier > 0 -> copy(modifier = plusModifier)
modifier > 0 -> copy(modifier = plusModifier) modifier < 0 -> copy(modifier = -minusModifier)
modifier < 0 -> copy(modifier = -minusModifier) else -> this
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 package io.github.wulkanowy.utils
import android.annotation.SuppressLint import android.annotation.SuppressLint
import java.text.SimpleDateFormat
import java.time.DayOfWeek.FRIDAY import java.time.DayOfWeek.FRIDAY
import java.time.DayOfWeek.MONDAY import java.time.DayOfWeek.MONDAY
import java.time.DayOfWeek.SATURDAY import java.time.DayOfWeek.SATURDAY
@ -8,12 +9,12 @@ import java.time.DayOfWeek.SUNDAY
import java.time.Instant.ofEpochMilli import java.time.Instant.ofEpochMilli
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.LocalDateTime.now
import java.time.LocalDateTime.ofInstant import java.time.LocalDateTime.ofInstant
import java.time.Month import java.time.Month
import java.time.ZoneId import java.time.ZoneId
import java.time.ZoneOffset import java.time.ZoneOffset
import java.time.format.DateTimeFormatter.ofPattern import java.time.format.DateTimeFormatter.ofPattern
import java.time.format.TextStyle.FULL
import java.time.temporal.TemporalAdjusters.firstInMonth import java.time.temporal.TemporalAdjusters.firstInMonth
import java.time.temporal.TemporalAdjusters.next import java.time.temporal.TemporalAdjusters.next
import java.time.temporal.TemporalAdjusters.previous import java.time.temporal.TemporalAdjusters.previous
@ -33,24 +34,10 @@ fun LocalDateTime.toFormattedString(format: String = DATE_PATTERN): String = for
@SuppressLint("DefaultLocale") @SuppressLint("DefaultLocale")
fun Month.getFormattedName(): String { fun Month.getFormattedName(): String {
return getDisplayName(FULL, Locale.getDefault()) val formatter = SimpleDateFormat("LLLL", Locale.getDefault())
.let {
when (it) { val date = now().withMonth(value)
"stycznia" -> "Styczeń" return formatter.format(date.toInstant(ZoneOffset.UTC).toEpochMilli()).capitalize()
"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()
} }
inline val LocalDate.nextSchoolDay: LocalDate inline val LocalDate.nextSchoolDay: LocalDate

View File

@ -1,9 +1,7 @@
Wersja 0.24.3 Wersja 0.25.0
- naprawiliśmy odczytywanie wiadomości - naprawiliśmy przełączanie semestrów przy przełączaniu uczniów
- naprawiliśmy niekończące się ładowanie w ocenach na drugim semestrze - naprawiliśmy błąd przy odświeżaniu ocen gdy włączony był inny niż domyślny tryb liczenia średniej
- naprawiliśmy ciemny motyw na MIUI 12 - dodaliśmy menadżer kont, gdzie można podejrzeć informacje o uczniu oraz zmienić pseudonim
- dodaliśmy automatyczne odświeżanie danych w aplikacji - zmieniliśmy ikonę planu lekcji oraz kolor animacji odświeżania w ciemnym motywie
- 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)
Pełna lista zmian: https://github.com/wulkanowy/wulkanowy/releases 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"> android:viewportHeight="24">
<path <path
android:fillColor="#FFF" 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> </vector>

View File

@ -5,5 +5,5 @@
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:fillColor="#FFF" 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> </vector>

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