Add avatars (#1146)

This commit is contained in:
Rafał Borcz 2021-03-03 00:34:25 +01:00 committed by GitHub
parent 412057b512
commit 9e2985864a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 2929 additions and 406 deletions

View File

@ -166,6 +166,7 @@ dependencies {
implementation "com.google.android.material:material:1.3.0" implementation "com.google.android.material:material:1.3.0"
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 'com.mikhaellopez:circularimageview:4.2.0'
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"

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@ import androidx.test.core.app.ApplicationProvider
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import io.github.wulkanowy.data.db.AppDatabase import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.utils.AppInfo
import org.junit.Rule import org.junit.Rule
abstract class AbstractMigrationTest { abstract class AbstractMigrationTest {
@ -24,12 +25,16 @@ abstract class AbstractMigrationTest {
fun getMigratedRoomDatabase(): AppDatabase { fun getMigratedRoomDatabase(): AppDatabase {
val context = ApplicationProvider.getApplicationContext<Context>() val context = ApplicationProvider.getApplicationContext<Context>()
val database = Room.databaseBuilder(ApplicationProvider.getApplicationContext(), val database = Room.databaseBuilder(
AppDatabase::class.java, dbName) ApplicationProvider.getApplicationContext(),
.addMigrations(*AppDatabase.getMigrations(SharedPrefProvider(PreferenceManager AppDatabase::class.java,
.getDefaultSharedPreferences(context))) dbName
).addMigrations(
*AppDatabase.getMigrations(
SharedPrefProvider(PreferenceManager.getDefaultSharedPreferences(context)),
AppInfo()
) )
.build() ).build()
// close the database and release any stream resources when the test finishes // close the database and release any stream resources when the test finishes
helper.closeWhenFinished(database) helper.closeWhenFinished(database)
return database return database

View File

@ -0,0 +1,60 @@
package io.github.wulkanowy.data.db.migrations
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import io.github.wulkanowy.utils.AppInfo
import kotlinx.coroutines.runBlocking
import org.junit.Test
import kotlin.random.Random
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class Migration35Test : AbstractMigrationTest() {
@Test
fun addRandomAvatarColorsForStudents() {
with(helper.createDatabase(dbName, 34)) {
createStudent(this, 1)
createStudent(this, 2)
}
helper.runMigrationsAndValidate(dbName, 35, true, Migration35(AppInfo()))
val db = getMigratedRoomDatabase()
val students = runBlocking { db.studentDao.loadAll() }
assertEquals(2, students.size)
assertTrue { students[0].avatarColor in AppInfo().defaultColorsForAvatar }
assertTrue { students[1].avatarColor in AppInfo().defaultColorsForAvatar }
}
private fun createStudent(db: SupportSQLiteDatabase, id: Long) {
db.insert("Students", SQLiteDatabase.CONFLICT_FAIL, ContentValues().apply {
put("id", id)
put("scrapper_base_url", "https://fakelog.cf")
put("mobile_base_url", "")
put("login_mode", "SCRAPPER")
put("login_type", "STANDARD")
put("certificate_key", "")
put("private_key", "")
put("is_parent", false)
put("email", "jan@fakelog.cf")
put("password", "******")
put("symbol", "Default")
put("school_short", "")
put("class_name", "")
put("student_id", Random.nextInt())
put("class_id", Random.nextInt())
put("school_id", "123")
put("school_name", "Wulkan first class school")
put("is_current", false)
put("registration_date", "0")
put("user_login_id", Random.nextInt())
put("student_name", "")
put("user_name", "")
put("nick", "")
})
}
}

View File

@ -1,11 +1,13 @@
package io.github.wulkanowy package io.github.wulkanowy
import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.content.Context 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 android.webkit.WebView
import androidx.fragment.app.FragmentManager
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
@ -46,8 +48,10 @@ class WulkanowyApp : Application(), Configuration.Provider {
MultiDex.install(this) MultiDex.install(this)
} }
@SuppressLint("UnsafeOptInUsageWarning")
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
FragmentManager.enableNewStateManager(false)
initializeAppLanguage() initializeAppLanguage()
themeManager.applyDefaultTheme() themeManager.applyDefaultTheme()

View File

@ -17,6 +17,7 @@ import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.SharedPrefProvider import io.github.wulkanowy.data.db.SharedPrefProvider
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AppInfo
import timber.log.Timber import timber.log.Timber
import javax.inject.Singleton import javax.inject.Singleton
@ -60,7 +61,8 @@ internal class RepositoryModule {
fun provideDatabase( fun provideDatabase(
@ApplicationContext context: Context, @ApplicationContext context: Context,
sharedPrefProvider: SharedPrefProvider, sharedPrefProvider: SharedPrefProvider,
) = AppDatabase.newInstance(context, sharedPrefProvider) appInfo: AppInfo
) = AppDatabase.newInstance(context, sharedPrefProvider, appInfo)
@Singleton @Singleton
@Provides @Provides

View File

@ -6,7 +6,6 @@ import androidx.room.Room
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.RoomDatabase.JournalMode.TRUNCATE import androidx.room.RoomDatabase.JournalMode.TRUNCATE
import androidx.room.TypeConverters import androidx.room.TypeConverters
import androidx.room.migration.Migration
import io.github.wulkanowy.data.db.dao.AttendanceDao import io.github.wulkanowy.data.db.dao.AttendanceDao
import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao
import io.github.wulkanowy.data.db.dao.CompletedLessonsDao import io.github.wulkanowy.data.db.dao.CompletedLessonsDao
@ -86,12 +85,14 @@ import io.github.wulkanowy.data.db.migrations.Migration31
import io.github.wulkanowy.data.db.migrations.Migration32 import io.github.wulkanowy.data.db.migrations.Migration32
import io.github.wulkanowy.data.db.migrations.Migration33 import io.github.wulkanowy.data.db.migrations.Migration33
import io.github.wulkanowy.data.db.migrations.Migration34 import io.github.wulkanowy.data.db.migrations.Migration34
import io.github.wulkanowy.data.db.migrations.Migration35
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
import io.github.wulkanowy.data.db.migrations.Migration7 import io.github.wulkanowy.data.db.migrations.Migration7
import io.github.wulkanowy.data.db.migrations.Migration8 import io.github.wulkanowy.data.db.migrations.Migration8
import io.github.wulkanowy.data.db.migrations.Migration9 import io.github.wulkanowy.data.db.migrations.Migration9
import io.github.wulkanowy.utils.AppInfo
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
@ -131,10 +132,9 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
companion object { companion object {
const val VERSION_SCHEMA = 34 const val VERSION_SCHEMA = 35
fun getMigrations(sharedPrefProvider: SharedPrefProvider): Array<Migration> { fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
return arrayOf(
Migration2(), Migration2(),
Migration3(), Migration3(),
Migration4(), Migration4(),
@ -167,19 +167,21 @@ abstract class AppDatabase : RoomDatabase() {
Migration31(), Migration31(),
Migration32(), Migration32(),
Migration33(), Migration33(),
Migration34() Migration34(),
Migration35(appInfo)
) )
}
fun newInstance(context: Context, sharedPrefProvider: SharedPrefProvider): AppDatabase { fun newInstance(
return Room.databaseBuilder(context, AppDatabase::class.java, "wulkanowy_database") context: Context,
sharedPrefProvider: SharedPrefProvider,
appInfo: AppInfo
) = Room.databaseBuilder(context, AppDatabase::class.java, "wulkanowy_database")
.setJournalMode(TRUNCATE) .setJournalMode(TRUNCATE)
.fallbackToDestructiveMigrationFrom(VERSION_SCHEMA + 1) .fallbackToDestructiveMigrationFrom(VERSION_SCHEMA + 1)
.fallbackToDestructiveMigrationOnDowngrade() .fallbackToDestructiveMigrationOnDowngrade()
.addMigrations(*getMigrations(sharedPrefProvider)) .addMigrations(*getMigrations(sharedPrefProvider, appInfo))
.build() .build()
} }
}
abstract val studentDao: StudentDao abstract val studentDao: StudentDao

View File

@ -8,7 +8,7 @@ import androidx.room.Query
import androidx.room.Transaction import androidx.room.Transaction
import androidx.room.Update 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.StudentNickAndAvatar
import io.github.wulkanowy.data.db.entities.StudentWithSemesters import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import javax.inject.Singleton import javax.inject.Singleton
@ -23,13 +23,13 @@ interface StudentDao {
suspend fun delete(student: Student) suspend fun delete(student: Student)
@Update(entity = Student::class) @Update(entity = Student::class)
suspend fun update(studentNick: StudentNick) suspend fun update(studentNickAndAvatar: StudentNickAndAvatar)
@Query("SELECT * FROM Students WHERE is_current = 1") @Query("SELECT * FROM Students WHERE is_current = 1")
suspend fun loadCurrent(): Student? suspend fun loadCurrent(): Student?
@Query("SELECT * FROM Students WHERE id = :id") @Query("SELECT * FROM Students WHERE id = :id")
suspend fun loadById(id: Int): Student? suspend fun loadById(id: Long): Student?
@Query("SELECT * FROM Students") @Query("SELECT * FROM Students")
suspend fun loadAll(): List<Student> suspend fun loadAll(): List<Student>

View File

@ -10,7 +10,7 @@ import java.time.LocalDateTime
data class Message( data class Message(
@ColumnInfo(name = "student_id") @ColumnInfo(name = "student_id")
val studentId: Int, val studentId: Long,
@ColumnInfo(name = "real_id") @ColumnInfo(name = "real_id")
val realId: Int, val realId: Int,

View File

@ -81,4 +81,7 @@ data class Student(
var id: Long = 0 var id: Long = 0
var nick = "" var nick = ""
@ColumnInfo(name = "avatar_color")
var avatarColor = 0L
} }

View File

@ -1,13 +1,17 @@
package io.github.wulkanowy.data.db.entities package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import java.io.Serializable import java.io.Serializable
@Entity @Entity
data class StudentNick( data class StudentNickAndAvatar(
val nick: String val nick: String,
@ColumnInfo(name = "avatar_color")
var avatarColor: Long
) : Serializable { ) : Serializable {

View File

@ -0,0 +1,24 @@
package io.github.wulkanowy.data.db.migrations
import androidx.core.database.getLongOrNull
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import io.github.wulkanowy.utils.AppInfo
class Migration35(private val appInfo: AppInfo) : Migration(34, 35) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE Students ADD COLUMN `avatar_color` INTEGER NOT NULL DEFAULT 0")
val studentsCursor = database.query("SELECT * FROM Students")
while (studentsCursor.moveToNext()) {
val studentId = studentsCursor.getLongOrNull(0)
database.execSQL(
"""UPDATE Students
SET avatar_color = ${appInfo.defaultColorsForAvatar.random()}
WHERE id = $studentId"""
)
}
}
}

View File

@ -4,14 +4,14 @@ import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.MessageAttachment import io.github.wulkanowy.data.db.entities.MessageAttachment
import io.github.wulkanowy.data.db.entities.Recipient import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.sdk.pojo.Recipient as SdkRecipient
import io.github.wulkanowy.sdk.pojo.MessageAttachment as SdkMessageAttachment
import java.time.LocalDateTime import java.time.LocalDateTime
import io.github.wulkanowy.sdk.pojo.Message as SdkMessage import io.github.wulkanowy.sdk.pojo.Message as SdkMessage
import io.github.wulkanowy.sdk.pojo.MessageAttachment as SdkMessageAttachment
import io.github.wulkanowy.sdk.pojo.Recipient as SdkRecipient
fun List<SdkMessage>.mapToEntities(student: Student) = map { fun List<SdkMessage>.mapToEntities(student: Student) = map {
Message( Message(
studentId = student.id.toInt(), studentId = student.id,
realId = it.id ?: 0, realId = it.id ?: 0,
messageId = it.messageId ?: 0, messageId = it.messageId ?: 0,
sender = it.sender?.name.orEmpty(), sender = it.sender?.name.orEmpty(),

View File

@ -5,7 +5,7 @@ import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import java.time.LocalDateTime import java.time.LocalDateTime
import io.github.wulkanowy.sdk.pojo.Student as SdkStudent import io.github.wulkanowy.sdk.pojo.Student as SdkStudent
fun List<SdkStudent>.mapToEntities(password: String = "") = map { fun List<SdkStudent>.mapToEntities(password: String = "", colors: List<Long>) = map {
StudentWithSemesters( StudentWithSemesters(
student = Student( student = Student(
email = it.email, email = it.email,
@ -28,8 +28,10 @@ fun List<SdkStudent>.mapToEntities(password: String = "") = map {
mobileBaseUrl = it.mobileBaseUrl, mobileBaseUrl = it.mobileBaseUrl,
privateKey = it.privateKey, privateKey = it.privateKey,
certificateKey = it.certificateKey, certificateKey = it.certificateKey,
loginMode = it.loginMode.name loginMode = it.loginMode.name,
), ).apply {
avatarColor = colors.random()
},
semesters = it.semesters.mapToEntities(it.studentId) semesters = it.semesters.mapToEntities(it.studentId)
) )
} }

View File

@ -5,11 +5,12 @@ 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.StudentNickAndAvatar
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
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.DispatchersProvider import io.github.wulkanowy.utils.DispatchersProvider
import io.github.wulkanowy.utils.security.decrypt import io.github.wulkanowy.utils.security.decrypt
import io.github.wulkanowy.utils.security.encrypt import io.github.wulkanowy.utils.security.encrypt
@ -23,7 +24,8 @@ class StudentRepository @Inject constructor(
private val dispatchers: DispatchersProvider, private val dispatchers: DispatchersProvider,
private val studentDb: StudentDao, private val studentDb: StudentDao,
private val semesterDb: SemesterDao, private val semesterDb: SemesterDao,
private val sdk: Sdk private val sdk: Sdk,
private val appInfo: AppInfo
) { ) {
suspend fun isStudentSaved() = getSavedStudents(false).isNotEmpty() suspend fun isStudentSaved() = getSavedStudents(false).isNotEmpty()
@ -35,7 +37,8 @@ class StudentRepository @Inject constructor(
symbol: String, symbol: String,
token: String token: String
): List<StudentWithSemesters> = ): List<StudentWithSemesters> =
sdk.getStudentsFromMobileApi(token, pin, symbol, "").mapToEntities() sdk.getStudentsFromMobileApi(token, pin, symbol, "")
.mapToEntities(colors = appInfo.defaultColorsForAvatar)
suspend fun getStudentsScrapper( suspend fun getStudentsScrapper(
email: String, email: String,
@ -44,7 +47,7 @@ class StudentRepository @Inject constructor(
symbol: String symbol: String
): List<StudentWithSemesters> = ): List<StudentWithSemesters> =
sdk.getStudentsFromScrapper(email, password, scrapperBaseUrl, symbol) sdk.getStudentsFromScrapper(email, password, scrapperBaseUrl, symbol)
.mapToEntities(password) .mapToEntities(password, appInfo.defaultColorsForAvatar)
suspend fun getStudentsHybrid( suspend fun getStudentsHybrid(
email: String, email: String,
@ -52,47 +55,59 @@ class StudentRepository @Inject constructor(
scrapperBaseUrl: String, scrapperBaseUrl: String,
symbol: String symbol: String
): List<StudentWithSemesters> = ): List<StudentWithSemesters> =
sdk.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol).mapToEntities(password) sdk.getStudentsHybrid(email, password, scrapperBaseUrl, "", symbol)
.mapToEntities(password, appInfo.defaultColorsForAvatar)
suspend fun getSavedStudents(decryptPass: Boolean = true) = suspend fun getSavedStudents(decryptPass: Boolean = true) =
withContext(dispatchers.backgroundThread) { studentDb.loadStudentsWithSemesters()
studentDb.loadStudentsWithSemesters().map { .map {
it.apply { it.apply {
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) { if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
student.password = decrypt(student.password) student.password = withContext(dispatchers.backgroundThread) {
decrypt(student.password)
} }
} }
} }
} }
suspend fun getStudentById(id: Int) = withContext(dispatchers.backgroundThread) { suspend fun getStudentById(id: Long, decryptPass: Boolean = true): Student {
studentDb.loadById(id)?.apply { val student = studentDb.loadById(id) ?: throw NoCurrentStudentException()
if (Sdk.Mode.valueOf(loginMode) != Sdk.Mode.API) {
password = decrypt(password)
}
}
} ?: throw NoCurrentStudentException()
suspend fun getCurrentStudent(decryptPass: Boolean = true) = if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
withContext(dispatchers.backgroundThread) { student.password = withContext(dispatchers.backgroundThread) {
studentDb.loadCurrent()?.apply { decrypt(student.password)
if (decryptPass && Sdk.Mode.valueOf(loginMode) != Sdk.Mode.API) {
password = decrypt(password)
} }
} }
} ?: throw NoCurrentStudentException() return student
}
suspend fun getCurrentStudent(decryptPass: Boolean = true): Student {
val student = studentDb.loadCurrent() ?: throw NoCurrentStudentException()
if (decryptPass && Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
student.password = withContext(dispatchers.backgroundThread) {
decrypt(student.password)
}
}
return student
}
suspend fun saveStudents(studentsWithSemesters: List<StudentWithSemesters>): List<Long> { suspend fun saveStudents(studentsWithSemesters: List<StudentWithSemesters>): List<Long> {
semesterDb.insertSemesters(studentsWithSemesters.flatMap { it.semesters }) val semesters = studentsWithSemesters.flatMap { it.semesters }
val students = studentsWithSemesters.map { it.student }
return withContext(dispatchers.backgroundThread) { .map {
studentDb.insertAll(studentsWithSemesters.map { it.student }.map { it.apply {
if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) { if (Sdk.Mode.valueOf(it.loginMode) != Sdk.Mode.API) {
it.copy(password = encrypt(it.password, context)) password = withContext(dispatchers.backgroundThread) {
} else it encrypt(password, context)
})
} }
} }
}
}
semesterDb.insertSemesters(semesters)
return studentDb.insertAll(students)
}
suspend fun switchStudent(studentWithSemesters: StudentWithSemesters) { suspend fun switchStudent(studentWithSemesters: StudentWithSemesters) {
with(studentDb) { with(studentDb) {
@ -103,5 +118,6 @@ class StudentRepository @Inject constructor(
suspend fun logoutStudent(student: Student) = studentDb.delete(student) suspend fun logoutStudent(student: Student) = studentDb.delete(student)
suspend fun updateStudentNick(studentNick: StudentNick) = studentDb.update(studentNick) suspend fun updateStudentNickAndAvatar(studentNickAndAvatar: StudentNickAndAvatar) =
studentDb.update(studentNickAndAvatar)
} }

View File

@ -1,16 +1,17 @@
package io.github.wulkanowy.ui.modules.account package io.github.wulkanowy.ui.modules.account
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.graphics.PorterDuff
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View.GONE import android.view.View.GONE
import android.view.View.VISIBLE import android.view.View.VISIBLE
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible
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.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.utils.createNameInitialsDrawable
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.nickOrName import io.github.wulkanowy.utils.nickOrName
import javax.inject.Inject import javax.inject.Inject
@ -72,9 +73,13 @@ class AccountAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.V
binding: ItemAccountBinding, binding: ItemAccountBinding,
studentWithSemesters: StudentWithSemesters studentWithSemesters: StudentWithSemesters
) { ) {
val context = binding.root.context
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 avatar = context.createNameInitialsDrawable(student.nickOrName, student.avatarColor)
val checkBackgroundColor =
context.getThemeAttrColor(if (isAccountQuickDialogMode) R.attr.colorBackgroundFloating else R.attr.colorSurface)
val isDuplicatedStudent = items.filter { val isDuplicatedStudent = items.filter {
if (it.value !is StudentWithSemesters) return@filter false if (it.value !is StudentWithSemesters) return@filter false
val studentToCompare = it.value.student val studentToCompare = it.value.student
@ -87,15 +92,17 @@ class AccountAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView.V
with(binding) { with(binding) {
accountItemName.text = "${student.nickOrName} ${diary?.diaryName.orEmpty()}" accountItemName.text = "${student.nickOrName} ${diary?.diaryName.orEmpty()}"
accountItemSchool.text = studentWithSemesters.student.schoolName accountItemSchool.text = studentWithSemesters.student.schoolName
accountItemAccountType.setText(if (student.isParent) R.string.account_type_parent else R.string.account_type_student) accountItemImage.setImageDrawable(avatar)
accountItemAccountType.visibility = if (isDuplicatedStudent) VISIBLE else GONE
with(accountItemImage) { with(accountItemAccountType) {
val colorImage = setText(if (student.isParent) R.string.account_type_parent else R.string.account_type_student)
if (student.isCurrent) context.getThemeAttrColor(R.attr.colorPrimary) isVisible = isDuplicatedStudent
else context.getThemeAttrColor(R.attr.colorOnSurface, 153) }
setColorFilter(colorImage, PorterDuff.Mode.SRC_IN) with(accountItemCheck) {
isVisible = student.isCurrent
borderColor = checkBackgroundColor
circleColor = checkBackgroundColor
} }
root.setOnClickListener { onClickListener(studentWithSemesters) } root.setOnClickListener { onClickListener(studentWithSemesters) }

View File

@ -36,23 +36,20 @@ class AccountFragment : BaseFragment<FragmentAccountBinding>(R.layout.fragment_a
override var subtitleString = "" override var subtitleString = ""
override val isViewEmpty get() = accountAdapter.items.isEmpty()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
@Suppress("UNCHECKED_CAST")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding = FragmentAccountBinding.bind(view) binding = FragmentAccountBinding.bind(view)
presenter.onAttachView(this) presenter.onAttachView(this)
} }
override fun initView() { override fun initView() {
binding.accountErrorRetry.setOnClickListener { presenter.onRetry() }
binding.accountErrorDetails.setOnClickListener { presenter.onDetailsClick() }
binding.accountRecycler.apply { binding.accountRecycler.apply {
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
adapter = accountAdapter adapter = accountAdapter
@ -60,9 +57,7 @@ class AccountFragment : BaseFragment<FragmentAccountBinding>(R.layout.fragment_a
accountAdapter.onClickListener = presenter::onItemSelected accountAdapter.onClickListener = presenter::onItemSelected
with(binding) { binding.accountAdd.setOnClickListener { presenter.onAddSelected() }
accountAdd.setOnClickListener { presenter.onAddSelected() }
}
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -84,28 +79,7 @@ class AccountFragment : BaseFragment<FragmentAccountBinding>(R.layout.fragment_a
override fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters) { override fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters) {
(activity as? MainActivity)?.pushView( (activity as? MainActivity)?.pushView(
AccountDetailsFragment.newInstance( AccountDetailsFragment.newInstance(studentWithSemesters)
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

@ -5,7 +5,6 @@ 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.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.flowWithResource import io.github.wulkanowy.utils.flowWithResource
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import timber.log.Timber import timber.log.Timber
@ -16,28 +15,13 @@ class AccountPresenter @Inject constructor(
studentRepository: StudentRepository, studentRepository: StudentRepository,
) : 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 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()
@ -47,6 +31,24 @@ class AccountPresenter @Inject constructor(
view?.openAccountDetailsView(studentWithSemesters) view?.openAccountDetailsView(studentWithSemesters)
} }
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("load")
}
private fun createAccountItems(items: List<StudentWithSemesters>): List<AccountItem<*>> { private fun createAccountItems(items: List<StudentWithSemesters>): List<AccountItem<*>> {
return items.groupBy { return items.groupBy {
Account("${it.student.userName} (${it.student.email})", it.student.isParent) Account("${it.student.userName} (${it.student.email})", it.student.isParent)
@ -60,45 +62,4 @@ class AccountPresenter @Inject constructor(
} }
.flatten() .flatten()
} }
private fun loadData() {
flowWithResource { studentRepository.getSavedStudents(false) }
.onEach {
when (it.status) {
Status.LOADING -> {
Timber.i("Loading account data started")
view?.run {
showProgress(true)
showContent(false)
}
}
Status.SUCCESS -> {
Timber.i("Loading account result: Success")
view?.updateData(createAccountItems(it.data!!))
view?.run {
showContent(true)
showErrorView(false)
}
}
Status.ERROR -> {
Timber.i("Loading account result: An exception occurred")
errorHandler.dispatch(it.error!!)
}
}
}
.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

@ -5,8 +5,6 @@ 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<*>>)
@ -14,13 +12,4 @@ interface AccountView : BaseView {
fun openLoginView() fun openLoginView()
fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters) fun openAccountDetailsView(studentWithSemesters: StudentWithSemesters)
fun showErrorView(show: Boolean)
fun setErrorDetails(message: String)
fun showProgress(show: Boolean)
fun showContent(show: Boolean)
} }

View File

@ -7,6 +7,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.get import androidx.core.view.get
import androidx.core.view.isVisible
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
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.Student
@ -18,6 +19,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.studentinfo.StudentInfoFragment import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoFragment
import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView
import io.github.wulkanowy.utils.createNameInitialsDrawable
import io.github.wulkanowy.utils.nickOrName import io.github.wulkanowy.utils.nickOrName
import javax.inject.Inject import javax.inject.Inject
@ -88,8 +90,15 @@ class AccountDetailsFragment :
override fun showAccountData(student: Student) { override fun showAccountData(student: Student) {
with(binding) { with(binding) {
accountDetailsCheck.isVisible = student.isCurrent
accountDetailsName.text = student.nickOrName accountDetailsName.text = student.nickOrName
accountDetailsSchool.text = student.schoolName accountDetailsSchool.text = student.schoolName
accountDetailsAvatar.setImageDrawable(
requireContext().createNameInitialsDrawable(
student.nickOrName,
student.avatarColor
)
)
} }
} }

View File

@ -21,7 +21,7 @@ class AccountDetailsPresenter @Inject constructor(
private val syncManager: SyncManager private val syncManager: SyncManager
) : BasePresenter<AccountDetailsView>(errorHandler, studentRepository) { ) : BasePresenter<AccountDetailsView>(errorHandler, studentRepository) {
private lateinit var studentWithSemesters: StudentWithSemesters private var studentWithSemesters: StudentWithSemesters? = null
private lateinit var lastError: Throwable private lateinit var lastError: Throwable
@ -69,10 +69,10 @@ class AccountDetailsPresenter @Inject constructor(
} }
Status.SUCCESS -> { Status.SUCCESS -> {
Timber.i("Loading account details view result: Success") Timber.i("Loading account details view result: Success")
studentWithSemesters = it.data!! studentWithSemesters = it.data
view?.run { view?.run {
showAccountData(studentWithSemesters.student) showAccountData(studentWithSemesters!!.student)
enableSelectStudentButton(!studentWithSemesters.student.isCurrent) enableSelectStudentButton(!studentWithSemesters!!.student.isCurrent)
showContent(true) showContent(true)
showErrorView(false) showErrorView(false)
} }
@ -88,17 +88,23 @@ class AccountDetailsPresenter @Inject constructor(
} }
fun onAccountEditSelected() { fun onAccountEditSelected() {
view?.showAccountEditDetailsDialog(studentWithSemesters.student) studentWithSemesters?.let {
view?.showAccountEditDetailsDialog(it.student)
}
} }
fun onStudentInfoSelected(infoType: StudentInfoView.Type) { fun onStudentInfoSelected(infoType: StudentInfoView.Type) {
view?.openStudentInfoView(infoType, studentWithSemesters) studentWithSemesters?.let {
view?.openStudentInfoView(infoType, it)
}
} }
fun onStudentSelect() { fun onStudentSelect() {
Timber.i("Select student ${studentWithSemesters.student.id}") if (studentWithSemesters == null) return
flowWithResource { studentRepository.switchStudent(studentWithSemesters) } Timber.i("Select student ${studentWithSemesters!!.student.id}")
flowWithResource { studentRepository.switchStudent(studentWithSemesters!!) }
.onEach { .onEach {
when (it.status) { when (it.status) {
Status.LOADING -> Timber.i("Attempt to change a student") Status.LOADING -> Timber.i("Attempt to change a student")
@ -122,8 +128,10 @@ class AccountDetailsPresenter @Inject constructor(
} }
fun onLogoutConfirm() { fun onLogoutConfirm() {
if (studentWithSemesters == null) return
flowWithResource { flowWithResource {
val studentToLogout = studentWithSemesters.student val studentToLogout = studentWithSemesters!!.student
studentRepository.logoutStudent(studentToLogout) studentRepository.logoutStudent(studentToLogout)
val students = studentRepository.getSavedStudents(false) val students = studentRepository.getSavedStudents(false)
@ -143,7 +151,7 @@ class AccountDetailsPresenter @Inject constructor(
syncManager.stopSyncWorker() syncManager.stopSyncWorker()
openClearLoginView() openClearLoginView()
} }
studentWithSemesters.student.isCurrent -> { studentWithSemesters!!.student.isCurrent -> {
Timber.i("Logout result: Logout student and switch to another") Timber.i("Logout result: Logout student and switch to another")
recreateMainView() recreateMainView()
} }

View File

@ -0,0 +1,90 @@
package io.github.wulkanowy.ui.modules.account.accountedit
import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.RippleDrawable
import android.graphics.drawable.StateListDrawable
import android.os.Build
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.databinding.ItemAccountEditColorBinding
import javax.inject.Inject
class AccountEditColorAdapter @Inject constructor() :
RecyclerView.Adapter<AccountEditColorAdapter.ViewHolder>() {
var items = listOf<Int>()
var selectedColor = 0
override fun getItemCount() = items.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(
ItemAccountEditColorBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
@SuppressLint("RestrictedApi", "NewApi")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
with(holder.binding) {
accountEditItemColor.setImageDrawable(GradientDrawable().apply {
shape = GradientDrawable.OVAL
setColor(item)
})
accountEditItemColorContainer.foreground = item.createForegroundDrawable()
accountEditCheck.isVisible = selectedColor == item
root.setOnClickListener {
val oldSelectedPosition = items.indexOf(selectedColor)
selectedColor = item
notifyItemChanged(oldSelectedPosition)
notifyItemChanged(position)
}
}
}
private fun Int.createForegroundDrawable(): Drawable =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val mask = GradientDrawable().apply {
shape = GradientDrawable.OVAL
setColor(Color.BLACK)
}
RippleDrawable(ColorStateList.valueOf(this.rippleColor), null, mask)
} else {
val foreground = StateListDrawable().apply {
alpha = 80
setEnterFadeDuration(250)
setExitFadeDuration(250)
}
val mask = GradientDrawable().apply {
shape = GradientDrawable.OVAL
setColor(this@createForegroundDrawable.rippleColor)
}
foreground.apply {
addState(intArrayOf(android.R.attr.state_pressed), mask)
addState(intArrayOf(), ColorDrawable(Color.TRANSPARENT))
}
}
private inline val Int.rippleColor: Int
get() {
val hsv = FloatArray(3)
Color.colorToHSV(this, hsv)
hsv[2] = hsv[2] * 0.5f
return Color.HSVToColor(hsv)
}
class ViewHolder(val binding: ItemAccountEditColorBinding) :
RecyclerView.ViewHolder(binding.root)
}

View File

@ -4,6 +4,7 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.GridLayoutManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.databinding.DialogAccountEditBinding import io.github.wulkanowy.databinding.DialogAccountEditBinding
@ -16,6 +17,9 @@ class AccountEditDialog : BaseDialogFragment<DialogAccountEditBinding>(), Accoun
@Inject @Inject
lateinit var presenter: AccountEditPresenter lateinit var presenter: AccountEditPresenter
@Inject
lateinit var accountEditColorAdapter: AccountEditColorAdapter
companion object { companion object {
private const val ARGUMENT_KEY = "student_with_semesters" private const val ARGUMENT_KEY = "student_with_semesters"
@ -48,8 +52,30 @@ class AccountEditDialog : BaseDialogFragment<DialogAccountEditBinding>(), Accoun
with(binding) { with(binding) {
accountEditDetailsCancel.setOnClickListener { dismiss() } accountEditDetailsCancel.setOnClickListener { dismiss() }
accountEditDetailsSave.setOnClickListener { accountEditDetailsSave.setOnClickListener {
presenter.changeStudentNick(binding.accountEditDetailsNickText.text.toString()) presenter.changeStudentNickAndAvatar(
binding.accountEditDetailsNickText.text.toString(),
accountEditColorAdapter.selectedColor
)
} }
with(binding.accountEditColors) {
layoutManager = GridLayoutManager(context, 4)
adapter = accountEditColorAdapter
}
}
}
override fun updateSelectedColorData(color: Int) {
with(accountEditColorAdapter) {
selectedColor = color
notifyDataSetChanged()
}
}
override fun updateColorsData(colors: List<Int>) {
with(accountEditColorAdapter) {
items = colors
notifyDataSetChanged()
} }
} }

View File

@ -2,10 +2,11 @@ package io.github.wulkanowy.ui.modules.account.accountedit
import io.github.wulkanowy.data.Status import io.github.wulkanowy.data.Status
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.StudentNickAndAvatar
import io.github.wulkanowy.data.repositories.StudentRepository 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.utils.AppInfo
import io.github.wulkanowy.utils.afterLoading import io.github.wulkanowy.utils.afterLoading
import io.github.wulkanowy.utils.flowWithResource import io.github.wulkanowy.utils.flowWithResource
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -13,12 +14,15 @@ import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class AccountEditPresenter @Inject constructor( class AccountEditPresenter @Inject constructor(
private val appInfo: AppInfo,
errorHandler: ErrorHandler, errorHandler: ErrorHandler,
studentRepository: StudentRepository studentRepository: StudentRepository
) : BasePresenter<AccountEditView>(errorHandler, studentRepository) { ) : BasePresenter<AccountEditView>(errorHandler, studentRepository) {
lateinit var student: Student lateinit var student: Student
private val colors = appInfo.defaultColorsForAvatar.map { it.toInt() }
fun onAttachView(view: AccountEditView, student: Student) { fun onAttachView(view: AccountEditView, student: Student) {
super.onAttachView(view) super.onAttachView(view)
this.student = student this.student = student
@ -28,27 +32,49 @@ class AccountEditPresenter @Inject constructor(
showCurrentNick(student.nick.trim()) showCurrentNick(student.nick.trim())
} }
Timber.i("Account edit dialog view was initialized") Timber.i("Account edit dialog view was initialized")
loadData()
view.updateColorsData(colors)
} }
fun changeStudentNick(nick: String) { private fun loadData() {
flowWithResource {
studentRepository.getStudentById(student.id, false).avatarColor
}.onEach { resource ->
when (resource.status) {
Status.LOADING -> Timber.i("Attempt to load student")
Status.SUCCESS -> {
view?.updateSelectedColorData(resource.data?.toInt()!!)
Timber.i("Attempt to load student: Success")
}
Status.ERROR -> {
Timber.i("Attempt to load student: An exception occurred")
errorHandler.dispatch(resource.error!!)
}
}
}.launch("load_data")
}
fun changeStudentNickAndAvatar(nick: String, avatarColor: Int) {
flowWithResource { flowWithResource {
val studentNick = val studentNick =
StudentNick(nick = nick.trim()).apply { id = student.id } StudentNickAndAvatar(nick = nick.trim(), avatarColor = avatarColor.toLong())
studentRepository.updateStudentNick(studentNick) .apply { id = student.id }
studentRepository.updateStudentNickAndAvatar(studentNick)
}.onEach { }.onEach {
when (it.status) { when (it.status) {
Status.LOADING -> Timber.i("Attempt to change a student nick") Status.LOADING -> Timber.i("Attempt to change a student nick and avatar")
Status.SUCCESS -> { Status.SUCCESS -> {
Timber.i("Change a student nick result: Success") Timber.i("Change a student nick and avatar result: Success")
view?.recreateMainView() view?.recreateMainView()
} }
Status.ERROR -> { Status.ERROR -> {
Timber.i("Change a student result: An exception occurred") Timber.i("Change a student nick and avatar result: An exception occurred")
errorHandler.dispatch(it.error!!) errorHandler.dispatch(it.error!!)
} }
} }
} }
.afterLoading { view?.popView() } .afterLoading { view?.popView() }
.launch() .launch("update_student")
} }
} }

View File

@ -11,4 +11,8 @@ interface AccountEditView : BaseView {
fun recreateMainView() fun recreateMainView()
fun showCurrentNick(nick: String) fun showCurrentNick(nick: String)
fun updateSelectedColorData(color: Int)
fun updateColorsData(colors: List<Int>)
} }

View File

@ -6,6 +6,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.databinding.DialogAccountQuickBinding import io.github.wulkanowy.databinding.DialogAccountQuickBinding
import io.github.wulkanowy.ui.base.BaseDialogFragment import io.github.wulkanowy.ui.base.BaseDialogFragment
import io.github.wulkanowy.ui.modules.account.AccountAdapter import io.github.wulkanowy.ui.modules.account.AccountAdapter
@ -24,7 +25,15 @@ class AccountQuickDialog : BaseDialogFragment<DialogAccountQuickBinding>(), Acco
lateinit var presenter: AccountQuickPresenter lateinit var presenter: AccountQuickPresenter
companion object { companion object {
fun newInstance() = AccountQuickDialog()
private const val STUDENTS_ARGUMENT_KEY = "students"
fun newInstance(studentsWithSemesters: List<StudentWithSemesters>) =
AccountQuickDialog().apply {
arguments = Bundle().apply {
putSerializable(STUDENTS_ARGUMENT_KEY, studentsWithSemesters.toTypedArray())
}
}
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -38,8 +47,12 @@ class AccountQuickDialog : BaseDialogFragment<DialogAccountQuickBinding>(), Acco
savedInstanceState: Bundle? savedInstanceState: Bundle?
) = DialogAccountQuickBinding.inflate(inflater).apply { binding = this }.root ) = DialogAccountQuickBinding.inflate(inflater).apply { binding = this }.root
@Suppress("UNCHECKED_CAST")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
presenter.onAttachView(this) val studentsWithSemesters =
(requireArguments()[STUDENTS_ARGUMENT_KEY] as Array<StudentWithSemesters>).toList()
presenter.onAttachView(this, studentsWithSemesters)
} }
override fun initView() { override fun initView() {

View File

@ -17,11 +17,15 @@ class AccountQuickPresenter @Inject constructor(
studentRepository: StudentRepository studentRepository: StudentRepository
) : BasePresenter<AccountQuickView>(errorHandler, studentRepository) { ) : BasePresenter<AccountQuickView>(errorHandler, studentRepository) {
override fun onAttachView(view: AccountQuickView) { private lateinit var studentsWithSemesters: List<StudentWithSemesters>
fun onAttachView(view: AccountQuickView, studentsWithSemesters: List<StudentWithSemesters>) {
super.onAttachView(view) super.onAttachView(view)
this.studentsWithSemesters = studentsWithSemesters
view.initView() view.initView()
Timber.i("Account quick dialog view was initialized") Timber.i("Account quick dialog view was initialized")
loadData() view.updateData(createAccountItems(studentsWithSemesters))
} }
fun onManagerSelected() { fun onManagerSelected() {
@ -57,22 +61,6 @@ class AccountQuickPresenter @Inject constructor(
.launch("switch") .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 { private fun createAccountItems(items: List<StudentWithSemesters>) = items.map {
AccountItem(it, AccountItem.ViewType.ITEM) AccountItem(it, AccountItem.ViewType.ITEM)
} }

View File

@ -28,6 +28,8 @@ import com.ncapdevi.fragnav.FragNavController
import com.ncapdevi.fragnav.FragNavController.Companion.HIDE import com.ncapdevi.fragnav.FragNavController.Companion.HIDE
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
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.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.accountquick.AccountQuickDialog import io.github.wulkanowy.ui.modules.account.accountquick.AccountQuickDialog
@ -43,8 +45,10 @@ import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo import io.github.wulkanowy.utils.AppInfo
import io.github.wulkanowy.utils.UpdateHelper import io.github.wulkanowy.utils.UpdateHelper
import io.github.wulkanowy.utils.createNameInitialsDrawable
import io.github.wulkanowy.utils.dpToPx import io.github.wulkanowy.utils.dpToPx
import io.github.wulkanowy.utils.getThemeAttrColor import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.nickOrName
import io.github.wulkanowy.utils.safelyPopFragments import io.github.wulkanowy.utils.safelyPopFragments
import io.github.wulkanowy.utils.setOnViewChangeListener import io.github.wulkanowy.utils.setOnViewChangeListener
import timber.log.Timber import timber.log.Timber
@ -65,6 +69,8 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
@Inject @Inject
lateinit var appInfo: AppInfo lateinit var appInfo: AppInfo
private var accountMenu: MenuItem? = null
private val overlayProvider by lazy { ElevationOverlayProvider(this) } private val overlayProvider by lazy { ElevationOverlayProvider(this) }
private val navController = private val navController =
@ -192,6 +198,9 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.action_menu_main, menu) menuInflater.inflate(R.menu.action_menu_main, menu)
accountMenu = menu?.findItem(R.id.mainMenuAccount)
presenter.onActionMenuCreated()
return true return true
} }
@ -288,8 +297,8 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
supportActionBar?.setDisplayHomeAsUpEnabled(show) supportActionBar?.setDisplayHomeAsUpEnabled(show)
} }
override fun showAccountPicker() { override fun showAccountPicker(studentWithSemesters: List<StudentWithSemesters>) {
navController.showDialogFragment(AccountQuickDialog.newInstance()) navController.showDialogFragment(AccountQuickDialog.newInstance(studentWithSemesters))
} }
override fun showActionBarElevation(show: Boolean) { override fun showActionBarElevation(show: Boolean) {
@ -323,6 +332,13 @@ class MainActivity : BaseActivity<MainPresenter, ActivityMainBinding>(), MainVie
presenter.onBackPressed { super.onBackPressed() } presenter.onBackPressed { super.onBackPressed() }
} }
override fun showStudentAvatar(student: Student) {
accountMenu?.run {
icon = createNameInitialsDrawable(student.nickOrName, student.avatarColor, 0.44f)
title = getString(R.string.main_account_picker)
}
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
navController.onSaveInstanceState(outState) navController.onSaveInstanceState(outState)

View File

@ -1,5 +1,7 @@
package io.github.wulkanowy.ui.modules.main package io.github.wulkanowy.ui.modules.main
import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.StudentWithSemesters
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
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.services.sync.SyncManager
@ -9,6 +11,8 @@ import io.github.wulkanowy.ui.modules.main.MainView.Section.GRADE
import io.github.wulkanowy.ui.modules.main.MainView.Section.MESSAGE import io.github.wulkanowy.ui.modules.main.MainView.Section.MESSAGE
import io.github.wulkanowy.ui.modules.main.MainView.Section.SCHOOL import io.github.wulkanowy.ui.modules.main.MainView.Section.SCHOOL
import io.github.wulkanowy.utils.AnalyticsHelper import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.flowWithResource
import kotlinx.coroutines.flow.onEach
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -17,9 +21,11 @@ class MainPresenter @Inject constructor(
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val prefRepository: PreferencesRepository, private val prefRepository: PreferencesRepository,
private val syncManager: SyncManager, private val syncManager: SyncManager,
private val analytics: AnalyticsHelper private val analytics: AnalyticsHelper,
) : BasePresenter<MainView>(errorHandler, studentRepository) { ) : BasePresenter<MainView>(errorHandler, studentRepository) {
var studentsWitSemesters: List<StudentWithSemesters>? = null
fun onAttachView(view: MainView, initMenu: MainView.Section?) { fun onAttachView(view: MainView, initMenu: MainView.Section?) {
super.onAttachView(view) super.onAttachView(view)
view.apply { view.apply {
@ -35,6 +41,28 @@ class MainPresenter @Inject constructor(
analytics.logEvent("app_open", "destination" to initMenu?.name) analytics.logEvent("app_open", "destination" to initMenu?.name)
} }
fun onActionMenuCreated() {
if (!studentsWitSemesters.isNullOrEmpty()) {
showCurrentStudentAvatar()
return
}
flowWithResource { studentRepository.getSavedStudents(false) }
.onEach { resource ->
when (resource.status) {
Status.LOADING -> Timber.i("Loading student avatar data started")
Status.SUCCESS -> {
studentsWitSemesters = resource.data
showCurrentStudentAvatar()
}
Status.ERROR -> {
Timber.i("Loading student avatar result: An exception occurred")
errorHandler.dispatch(resource.error!!)
}
}
}.launch("avatar")
}
fun onViewChange(section: MainView.Section?) { fun onViewChange(section: MainView.Section?) {
view?.apply { view?.apply {
showActionBarElevation(section != GRADE && section != MESSAGE && section != SCHOOL) showActionBarElevation(section != GRADE && section != MESSAGE && section != SCHOOL)
@ -48,8 +76,10 @@ class MainPresenter @Inject constructor(
} }
fun onAccountManagerSelected(): Boolean { fun onAccountManagerSelected(): Boolean {
if (studentsWitSemesters.isNullOrEmpty()) return true
Timber.i("Select account manager") Timber.i("Select account manager")
view?.showAccountPicker() view?.showAccountPicker(studentsWitSemesters!!)
return true return true
} }
@ -81,6 +111,13 @@ class MainPresenter @Inject constructor(
} == true } == true
} }
private fun showCurrentStudentAvatar() {
val currentStudent =
studentsWitSemesters!!.single { it.student.isCurrent }.student
view?.showStudentAvatar(currentStudent)
}
private fun getProperViewIndexes(initMenu: MainView.Section?): Pair<Int, Int> { private fun getProperViewIndexes(initMenu: MainView.Section?): Pair<Int, Int> {
return when (initMenu?.id) { return when (initMenu?.id) {
in 0..3 -> initMenu!!.id to -1 in 0..3 -> initMenu!!.id to -1

View File

@ -1,5 +1,7 @@
package io.github.wulkanowy.ui.modules.main package io.github.wulkanowy.ui.modules.main
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.base.BaseView
interface MainView : BaseView { interface MainView : BaseView {
@ -22,7 +24,7 @@ interface MainView : BaseView {
fun showHomeArrow(show: Boolean) fun showHomeArrow(show: Boolean)
fun showAccountPicker() fun showAccountPicker(studentWithSemesters: List<StudentWithSemesters>)
fun showActionBarElevation(show: Boolean) fun showActionBarElevation(show: Boolean)
@ -36,6 +38,8 @@ interface MainView : BaseView {
fun popView(depth: Int = 1) fun popView(depth: Int = 1)
fun showStudentAvatar(student: Student)
interface MainChildView { interface MainChildView {
fun onFragmentReselected() fun onFragmentReselected()

View File

@ -35,8 +35,8 @@ open class AppInfo @Inject constructor() {
open val systemLanguage: String open val systemLanguage: String
get() = Resources.getSystem().configuration.locale.language get() = Resources.getSystem().configuration.locale.language
open val defaultColorsForAvatar = listOf( val defaultColorsForAvatar = listOf(
0xe57373, 0xf06292, 0xba68c8, 0x9575cd, 0x7986cb, 0x64b5f6, 0x4fc3f7, 0x4dd0e1, 0x4db6ac, 0xd32f2f, 0xE64A19, 0xFFA000, 0xAFB42B, 0x689F38, 0x388E3C, 0x00796B, 0x0097A7,
0x81c784, 0xaed581, 0xff8a65, 0xd4e157, 0xffd54f, 0xffb74d, 0xa1887f, 0x90a4ae 0x1976D2, 0x3647b5, 0x6236c9, 0x9225c1, 0xC2185B, 0x616161, 0x455A64, 0x7a5348
) ).map { (it and 0x00ffffff or (255 shl 24)).toLong() }
} }

View File

@ -1,8 +1,15 @@
package io.github.wulkanowy.utils package io.github.wulkanowy.utils
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.Typeface
import android.net.Uri import android.net.Uri
import android.text.TextPaint
import android.util.DisplayMetrics.DENSITY_DEFAULT import android.util.DisplayMetrics.DENSITY_DEFAULT
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
@ -10,6 +17,9 @@ import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
import androidx.core.graphics.applyCanvas
import androidx.core.graphics.drawable.RoundedBitmapDrawable
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory
import io.github.wulkanowy.BuildConfig.APPLICATION_ID import io.github.wulkanowy.BuildConfig.APPLICATION_ID
@ColorInt @ColorInt
@ -30,7 +40,8 @@ fun Context.getThemeAttrColor(@AttrRes colorAttr: Int, alpha: Int): Int {
@ColorInt @ColorInt
fun Context.getCompatColor(@ColorRes colorRes: Int) = ContextCompat.getColor(this, colorRes) fun Context.getCompatColor(@ColorRes colorRes: Int) = ContextCompat.getColor(this, colorRes)
fun Context.getCompatDrawable(@DrawableRes drawableRes: Int) = ContextCompat.getDrawable(this, drawableRes) fun Context.getCompatDrawable(@DrawableRes drawableRes: Int) =
ContextCompat.getDrawable(this, drawableRes)
fun Context.openInternetBrowser(uri: String, onActivityNotFound: (uri: String) -> Unit) { fun Context.openInternetBrowser(uri: String, onActivityNotFound: (uri: String) -> Unit) {
Intent.parseUri(uri, 0).let { Intent.parseUri(uri, 0).let {
@ -45,7 +56,13 @@ fun Context.openAppInMarket(onActivityNotFound: (uri: String) -> Unit) {
} }
} }
fun Context.openEmailClient(chooserTitle: String, email: String, subject: String, body: String, onActivityNotFound: () -> Unit = {}) { fun Context.openEmailClient(
chooserTitle: String,
email: String,
subject: String,
body: String,
onActivityNotFound: () -> Unit = {}
) {
val intent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:")).apply { val intent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:")).apply {
putExtra(Intent.EXTRA_EMAIL, arrayOf(email)) putExtra(Intent.EXTRA_EMAIL, arrayOf(email))
putExtra(Intent.EXTRA_SUBJECT, subject) putExtra(Intent.EXTRA_SUBJECT, subject)
@ -85,3 +102,39 @@ fun Context.shareText(text: String, subject: String?) {
} }
fun Context.dpToPx(dp: Float) = dp * resources.displayMetrics.densityDpi / DENSITY_DEFAULT fun Context.dpToPx(dp: Float) = dp * resources.displayMetrics.densityDpi / DENSITY_DEFAULT
@SuppressLint("DefaultLocale")
fun Context.createNameInitialsDrawable(
text: String,
backgroundColor: Long,
scaleFactory: Float = 1f
): RoundedBitmapDrawable {
val words = text.split(" ")
val firstCharFirstWord = words.getOrNull(0)?.firstOrNull() ?: ""
val firstCharSecondWord = words.getOrNull(1)?.firstOrNull() ?: ""
val initials = "$firstCharFirstWord$firstCharSecondWord".toUpperCase()
val bounds = Rect()
val dimension = this.dpToPx(64f * scaleFactory).toInt()
val textPaint = TextPaint().apply {
typeface = Typeface.SANS_SERIF
color = Color.WHITE
textAlign = Paint.Align.CENTER
isAntiAlias = true
textSize = this@createNameInitialsDrawable.dpToPx(30f * scaleFactory)
getTextBounds(initials, 0, initials.length, bounds)
}
val xCoordinate = (dimension / 2).toFloat()
val yCoordinate = (dimension / 2 + (bounds.bottom - bounds.top) / 2).toFloat()
val bitmap = Bitmap.createBitmap(dimension, dimension, Bitmap.Config.ARGB_8888)
.applyCanvas {
drawColor(backgroundColor.toInt())
drawText(initials, 0, initials.length, xCoordinate, yCoordinate, textPaint)
}
return RoundedBitmapDrawableFactory.create(this.resources, bitmap)
.apply { isCircular = true }
}

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="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z" />
</vector>

View File

@ -1,6 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
@ -36,7 +40,6 @@
android:layout_marginEnd="24dp" android:layout_marginEnd="24dp"
android:hint="@string/account_edit_nick_hint" android:hint="@string/account_edit_nick_hint"
app:endIconMode="clear_text" app:endIconMode="clear_text"
app:errorEnabled="true"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/accountEditDetailsHeader"> app:layout_constraintTop_toBottomOf="@id/accountEditDetailsHeader">
@ -51,6 +54,38 @@
android:maxLength="20" /> android:maxLength="20" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/accountEditDetailsSecondHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="28dp"
android:layout_marginEnd="24dp"
android:text="Wybierz kolor avatara"
android:textSize="21sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/accountEditDetailsNick" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/account_edit_colors"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="14dp"
android:layout_marginEnd="24dp"
android:overScrollMode="never"
android:scrollbars="none"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/accountEditDetailsSecondHeader"
tools:itemCount="12"
tools:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
tools:listitem="@layout/item_account_edit_color"
tools:spanCount="4" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/accountEditDetailsSave" android:id="@+id/accountEditDetailsSave"
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog" style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
@ -66,7 +101,7 @@
android:text="@string/all_save" android:text="@string/all_save"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/accountEditDetailsNick" /> app:layout_constraintTop_toBottomOf="@id/account_edit_colors" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/accountEditDetailsCancel" android:id="@+id/accountEditDetailsCancel"
@ -83,5 +118,6 @@
android:text="@android:string/cancel" android:text="@android:string/cancel"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/accountEditDetailsSave" app:layout_constraintEnd_toStartOf="@id/accountEditDetailsSave"
app:layout_constraintTop_toBottomOf="@id/accountEditDetailsNick" /> app:layout_constraintTop_toBottomOf="@id/account_edit_colors" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -2,7 +2,7 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="300dp" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<LinearLayout <LinearLayout

View File

@ -5,16 +5,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/account_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/accountRecycler" android:id="@+id/accountRecycler"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -36,55 +26,4 @@
android:insetBottom="0dp" android:insetBottom="0dp"
android:text="@string/account_add_new" android:text="@string/account_add_new"
app:layout_constraintBottom_toBottomOf="parent" /> app:layout_constraintBottom_toBottomOf="parent" />
<LinearLayout
android:id="@+id/account_error"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="invisible"
tools:ignore="UseCompoundDrawables"
tools:visibility="gone">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
app:srcCompat="@drawable/ic_all_account"
app:tint="?colorOnBackground"
tools:ignore="contentDescription" />
<TextView
android:id="@+id/account_error_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:padding="8dp"
android:text="@string/error_unknown"
android:textSize="20sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/account_error_details"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:text="@string/all_details" />
<com.google.android.material.button.MaterialButton
android:id="@+id/account_error_retry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/all_retry" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -21,9 +21,9 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center" android:gravity="center"
android:orientation="vertical" android:orientation="vertical"
android:visibility="invisible" android:visibility="gone"
tools:ignore="UseCompoundDrawables" tools:ignore="UseCompoundDrawables"
tools:visibility="visible"> tools:visibility="gone">
<ImageView <ImageView
android:layout_width="100dp" android:layout_width="100dp"
@ -69,7 +69,9 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/account_details_content" android:id="@+id/account_details_content"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:visibility="gone"
tools:visibility="visible">
<ScrollView <ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
@ -90,9 +92,22 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_all_account" tools:ignore="ContentDescription"
app:tint="?colorPrimary" tools:src="@tools:sample/avatars" />
tools:ignore="ContentDescription" />
<com.mikhaellopez.circularimageview.CircularImageView
android:id="@+id/account_details_check"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="70dp"
android:layout_marginTop="70dp"
app:civ_border_color="?colorSurface"
app:civ_border_width="1dp"
app:civ_circle_color="?colorSurface"
app:layout_constraintStart_toStartOf="@id/accountDetailsAvatar"
app:layout_constraintTop_toTopOf="@id/accountDetailsAvatar"
app:srcCompat="@drawable/ic_all_round_check"
app:tint="?colorPrimary" />
<TextView <TextView
android:id="@+id/accountDetailsName" android:id="@+id/accountDetailsName"

View File

@ -18,15 +18,27 @@
android:layout_height="40dp" android:layout_height="40dp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_all_account"
tools:ignore="ContentDescription" tools:ignore="ContentDescription"
tools:tint="@color/colorPrimary" /> tools:src="@tools:sample/avatars" />
<com.mikhaellopez.circularimageview.CircularImageView
android:id="@+id/account_item_check"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginStart="28dp"
android:layout_marginTop="28dp"
app:civ_border_width="1dp"
app:layout_constraintStart_toStartOf="@id/accountItemImage"
app:layout_constraintTop_toTopOf="@id/accountItemImage"
app:srcCompat="@drawable/ic_all_round_check"
app:tint="?colorPrimary" />
<TextView <TextView
android:id="@+id/accountItemName" android:id="@+id/accountItemName"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textSize="16sp" android:textSize="16sp"
@ -41,6 +53,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="3dp" android:layout_marginTop="3dp"
android:layout_marginEnd="16dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"
@ -56,6 +69,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="3dp" android:layout_marginTop="3dp"
android:layout_marginEnd="16dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textColor="?android:textColorSecondary" android:textColor="?android:textColorSecondary"

View File

@ -0,0 +1,27 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/account_edit_item_color_container"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="10dp"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/account_edit_item_color"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:ignore="ContentDescription"
tools:src="@tools:sample/avatars" />
<ImageView
android:id="@+id/account_edit_check"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center"
app:srcCompat="@drawable/ic_check"
app:tint="@android:color/white"
tools:ignore="ContentDescription" />
</FrameLayout>

View File

@ -23,5 +23,4 @@
android:layout_margin="10dp" android:layout_margin="10dp"
android:background="?android:windowBackground" android:background="?android:windowBackground"
tools:context=".ui.modules.grade.statistics.GradeStatisticsAdapter" /> tools:context=".ui.modules.grade.statistics.GradeStatisticsAdapter" />
</LinearLayout> </LinearLayout>

View File

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<item <item
android:id="@+id/mainMenuAccount" android:id="@+id/mainMenuAccount"
android:icon="@drawable/ic_all_account"
android:orderInCategory="2" android:orderInCategory="2"
android:title="@string/main_account_picker" app:showAsAction="always"
app:iconTint="@color/material_on_surface_emphasis_medium" tools:ignore="MenuTitle" />
app:showAsAction="always" />
</menu> </menu>

View File

@ -5,6 +5,7 @@ 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.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.pojo.Student import io.github.wulkanowy.sdk.pojo.Student
import io.github.wulkanowy.utils.AppInfo
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
@ -30,7 +31,14 @@ class StudentTest {
@Before @Before
fun initApi() { fun initApi() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
studentRepository = StudentRepository(mockk(), TestDispatchersProvider(), studentDb, semesterDb, mockSdk) studentRepository = StudentRepository(
mockk(),
TestDispatchersProvider(),
studentDb,
semesterDb,
mockSdk,
AppInfo()
)
} }
@Test @Test

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.2-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists