Add account manager (#671)

This commit is contained in:
Rafał Borcz 2021-01-29 21:53:46 +01:00 committed by GitHub
parent 26565b627a
commit d79b1c9a58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 4409 additions and 350 deletions

View File

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

View File

@ -131,14 +131,14 @@ play {
ext {
work_manager = "2.5.0"
room = "2.2.6"
room = "2.3.0-alpha04"
chucker = "3.4.0"
mockk = "1.10.5"
moshi = "1.11.0"
}
dependencies {
implementation "io.github.wulkanowy:sdk:a722e777"
implementation "io.github.wulkanowy:sdk:b7576e86"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.1'
@ -214,6 +214,7 @@ dependencies {
testImplementation "junit:junit:4.13.1"
testImplementation "io.mockk:mockk:$mockk"
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.2'
testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
androidTestImplementation "androidx.test:core:1.3.0"
androidTestImplementation "androidx.test:runner:1.3.0"

File diff suppressed because it is too large Load Diff

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

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

View File

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

View File

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

View File

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

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,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,3 @@
package io.github.wulkanowy.data.enums
enum class Gender { MALE, FEMALE }

View File

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

View File

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

View File

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

View File

@ -65,7 +65,11 @@ class AboutFragment : BaseFragment<FragmentAboutBinding>(R.layout.fragment_about
override val homepageRes: Triple<String, String, Drawable?>?
get() = context?.run {
Triple(getString(R.string.about_homepage), getString(R.string.about_homepage_summary), getCompatDrawable(R.drawable.ic_about_homepage))
Triple(
getString(R.string.about_homepage),
getString(R.string.about_homepage_summary),
getCompatDrawable(R.drawable.ic_all_home)
)
}
override val licensesRes: Triple<String, String, Drawable?>?

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,134 @@
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.StudentWithSemesters
import io.github.wulkanowy.databinding.FragmentAccountDetailsBinding
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoFragment
import io.github.wulkanowy.ui.modules.studentinfo.StudentInfoView
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)
arguments?.let {
presenter.studentWithSemesters =
it.getSerializable(ARGUMENT_KEY) as StudentWithSemesters
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentAccountDetailsBinding.bind(view)
presenter.onAttachView(this)
}
override fun initView() {
binding.accountDetailsLogout.setOnClickListener { presenter.onRemoveSelected() }
binding.accountDetailsSelect.setOnClickListener { presenter.onStudentSelect() }
binding.accountDetailsSelect.isEnabled = !presenter.studentWithSemesters.student.isCurrent
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
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.accountDetailsMenuEdit) {
showAccountEditDetailsDialog()
return true
} else false
}
override fun showAccountData(studentWithSemesters: StudentWithSemesters) {
with(binding) {
accountDetailsName.text = studentWithSemesters.student.studentName
accountDetailsSchool.text = studentWithSemesters.student.schoolName
}
}
override fun showAccountEditDetailsDialog() {
(requireActivity() as MainActivity).showDialogFragment(AccountEditDetailsDialog.newInstance())
}
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 onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
}

View File

@ -0,0 +1,100 @@
package io.github.wulkanowy.ui.modules.account.accountdetails
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.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) {
lateinit var studentWithSemesters: StudentWithSemesters
override fun onAttachView(view: AccountDetailsView) {
super.onAttachView(view)
view.initView()
Timber.i("Account details view was initialized")
view.showAccountData(studentWithSemesters)
}
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")
}
}

View File

@ -0,0 +1,25 @@
package io.github.wulkanowy.ui.modules.account.accountdetails
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(studentWithSemesters: StudentWithSemesters)
fun showAccountEditDetailsDialog()
fun showLogoutConfirmDialog()
fun popView()
fun recreateMainView()
fun openStudentInfoView(
infoType: StudentInfoView.Type,
studentWithSemesters: StudentWithSemesters
)
}

View File

@ -0,0 +1,38 @@
package io.github.wulkanowy.ui.modules.account.accountdetails
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.databinding.DialogAccountEditDetailsBinding
import io.github.wulkanowy.utils.lifecycleAwareVariable
class AccountEditDetailsDialog : DialogFragment() {
private var binding: DialogAccountEditDetailsBinding by lifecycleAwareVariable()
companion object {
fun newInstance() = AccountEditDetailsDialog()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return DialogAccountEditDetailsBinding.inflate(inflater).apply { binding = this }.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.accountEditDetailsCancel.setOnClickListener { dismiss() }
}
}

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

@ -60,6 +60,7 @@ class AttendanceFragment : BaseFragment<FragmentAttendanceBinding>(R.layout.frag
override val excuseActionMode: Boolean get() = attendanceAdapter.excuseActionMode
private var actionMode: ActionMode? = null
private val actionModeCallback = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
val inflater = mode.menuInflater

View File

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

View File

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

View File

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

View File

@ -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,228 @@
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 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() }
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_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

@ -28,4 +28,9 @@ open class AppInfo @Inject constructor() {
@Suppress("DEPRECATION")
open val systemLanguage: String
get() = Resources.getSystem().configuration.locale.language
open val defaultColorsForAvatar = listOf(
0xe57373, 0xf06292, 0xba68c8, 0x9575cd, 0x7986cb, 0x64b5f6, 0x4fc3f7, 0x4dd0e1, 0x4db6ac,
0x81c784, 0xaed581, 0xff8a65, 0xd4e157, 0xffd54f, 0xffb74d, 0xa1887f, 0x90a4ae
)
}

View File

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

View File

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

View File

@ -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

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<View
android:layout_width="280dp"
android:layout_height="1dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/accountEditDetailsHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="24dp"
android:text="Modify data"
android:textSize="21sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/accountEditDetailsNick"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="28dp"
android:layout_marginEnd="24dp"
android:hint="Nick"
app:errorEnabled="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/accountEditDetailsHeader">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="textPersonName" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/accountEditDetailsSave"
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
android:layout_width="88dp"
android:layout_height="36dp"
android:layout_marginTop="36dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:text="Save"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/accountEditDetailsNick" />
<com.google.android.material.button.MaterialButton
android:id="@+id/accountEditDetailsCancel"
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
android:layout_width="88dp"
android:layout_height="36dp"
android:layout_marginTop="36dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:text="@android:string/cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/accountEditDetailsSave"
app:layout_constraintTop_toBottomOf="@id/accountEditDetailsNick" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -8,23 +8,24 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:orientation="vertical"
tools:ignore="UselessParent">
<TextView
android:id="@+id/accountDialogTitle"
android:id="@+id/account_quick_dialog_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="24dp"
android:paddingLeft="24dp"
android:paddingEnd="24dp"
android:paddingRight="24dp"
android:text="@string/account_title"
android:text="@string/account_quick_title"
android:textSize="20sp"
android:textStyle="bold"
app:firstBaselineToTopHeight="40dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/accountDialogRecycler"
android:id="@+id/account_quick_dialog_recycler"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="8dp"
@ -39,26 +40,12 @@
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/accountDialogAdd"
android:id="@+id/account_quick_dialog_manger"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/account_add_new"
android:textColor="?android:textColorPrimary" />
<Space
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/accountDialogRemove"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/account_logout" />
android:text="@string/account_quick_manager" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
android:layout_height="match_parent">
<me.zhanghai.android.materialprogressbar.MaterialProgressBar
android:id="@+id/account_progress"
style="@style/Widget.MaterialProgressBar.ProgressBar"
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
android:id="@+id/accountRecycler"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/accountAdd"
app:layout_constraintTop_toTopOf="parent"
tools:listitem="@layout/item_account" />
<com.google.android.material.button.MaterialButton
android:id="@+id/accountAdd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="16dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:text="@string/account_add_new"
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="visible">
<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>

View File

@ -0,0 +1,206 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:fillViewport="true"
app:layout_constraintBottom_toTopOf="@id/accountDetailsSelect"
app:layout_constraintTop_toTopOf="parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/accountDetailsAvatar"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginTop="36dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_all_account"
app:tint="?colorPrimary"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/accountDetailsName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textSize="24sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/accountDetailsAvatar"
tools:text="Jan Kowalski" />
<TextView
android:id="@+id/accountDetailsSchool"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:textColor="?android:textColorSecondary"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/accountDetailsName"
tools:text="Szkoła FakeLog" />
<LinearLayout
android:id="@+id/accountDetailsPersonalData"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginTop="16dp"
android:background="?selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="@id/accountDetailsSchool"
tools:ignore="UseCompoundDrawables">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
app:srcCompat="@drawable/ic_all_account"
app:tint="?colorOnBackground"
tools:ignore="ContentDescription" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="32dp"
android:layout_marginEnd="16dp"
android:gravity="center_vertical"
android:text="@string/account_personal_data"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/accountDetailsAddressData"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="?selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="@id/accountDetailsPersonalData"
tools:ignore="UseCompoundDrawables">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
app:srcCompat="@drawable/ic_all_home"
app:tint="?colorOnBackground"
tools:ignore="ContentDescription" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="32dp"
android:layout_marginEnd="16dp"
android:gravity="center_vertical"
android:text="@string/account_address"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/accountDetailsContactData"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="?selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="@id/accountDetailsAddressData"
tools:ignore="UseCompoundDrawables">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
app:srcCompat="@drawable/ic_all_phone"
app:tint="?colorOnBackground"
tools:ignore="ContentDescription" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="32dp"
android:layout_marginEnd="16dp"
android:gravity="center_vertical"
android:text="@string/account_contact"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/accountDetailsFamilyData"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="?selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="@id/accountDetailsContactData"
tools:ignore="UseCompoundDrawables">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
app:srcCompat="@drawable/ic_account_details_family"
app:tint="?colorOnBackground"
tools:ignore="ContentDescription" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="32dp"
android:layout_marginEnd="16dp"
android:gravity="center_vertical"
android:text="@string/account_family"
android:textSize="16sp" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<com.google.android.material.button.MaterialButton
android:id="@+id/accountDetailsSelect"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:text="@string/account_select_student"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@id/accountDetailsLogout" />
<com.google.android.material.button.MaterialButton
android:id="@+id/accountDetailsLogout"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
android:text="@string/account_logout"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -140,7 +140,7 @@
android:id="@+id/schoolTelephoneButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_school_phone"
app:srcCompat="@drawable/ic_all_phone"
android:contentDescription="@string/school_telephone_button"
android:background="?attr/selectableItemBackgroundBorderless"
android:tint="?colorPrimary"

View File

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
android:layout_height="match_parent">
<me.zhanghai.android.materialprogressbar.MaterialProgressBar
android:id="@+id/student_info_progress"
style="@style/Widget.MaterialProgressBar.ProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:layout_width="match_parent"
android:id="@+id/student_info_swipe"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/student_info_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<LinearLayout
android:id="@+id/student_info_empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="10dp"
android:visibility="gone"
tools:ignore="UseCompoundDrawables"
tools:visibility="gone">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
app:srcCompat="@drawable/ic_all_about"
app:tint="?colorOnBackground"
tools:ignore="contentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:text="@string/student_info_empty"
android:textSize="20sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/student_info_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="visible">
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
app:srcCompat="@drawable/ic_error"
app:tint="?colorOnBackground"
tools:ignore="contentDescription" />
<TextView
android:id="@+id/student_info_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/student_info_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/student_info_error_retry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/all_retry" />
</LinearLayout>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -4,15 +4,22 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="24dp"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:paddingHorizontal="16dp"
android:paddingBottom="16dp"
tools:context=".ui.modules.account.AccountAdapter">
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:id="@+id/accountHeaderDivider"
android:layout_marginTop="16dp"
android:background="@color/colorDivider" />
<TextView
android:id="@+id/accountHeaderEmail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textColor="?android:textColorPrimary"
android:textSize="14sp"
tools:text="jan@fakelog.cf" />
@ -21,7 +28,6 @@
android:id="@+id/accountHeaderType"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
tools:text="Konto ucznia" />

View File

@ -5,9 +5,11 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="horizontal"
android:paddingVertical="8dp"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp"
tools:context=".ui.modules.account.AccountAdapter">
<ImageView
@ -49,7 +51,7 @@
tools:text="@tools:sample/lorem/random" />
<TextView
android:id="@+id/accountItemLoginMode"
android:id="@+id/accountItemAccountType"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
@ -58,7 +60,6 @@
android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/accountItemImage"
app:layout_constraintTop_toBottomOf="@id/accountItemSchool"

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
android:layout_height="64dp"
android:background="?selectableItemBackground"
android:clickable="true"
android:focusable="true">
<TextView
android:id="@+id/student_info_item_title"
android:layout_width="match_parent"
android:layout_height="28dp"
android:layout_marginStart="16dp"
android:layout_marginTop="5dp"
android:layout_marginEnd="16dp"
android:gravity="bottom"
android:maxLines="1"
android:textColor="?android:textColorPrimary"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@tools:sample/lorem/random" />
<TextView
android:id="@+id/student_info_item_subtitle"
android:layout_width="match_parent"
android:layout_height="18dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:gravity="bottom"
android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/student_info_item_title"
tools:text="@tools:sample/lorem/random" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/accountDetailsMenuEdit"
android:icon="@drawable/ic_menu_message_write"
android:orderInCategory="1"
android:title="@string/account_details_edit"
app:iconTint="@color/material_on_surface_emphasis_medium"
app:showAsAction="ifRoom" />
</menu>

View File

@ -17,7 +17,10 @@
<string name="send_message_title">New message</string>
<string name="note_title">Notes and achievements</string>
<string name="homework_title">Homework</string>
<string name="account_title">Choose account</string>
<string name="account_title">Accounts manager</string>
<string name="account_quick_title">Select account</string>
<string name="account_details_title">Account details</string>
<string name="student_info_title">Student info</string>
<!--Subtitles-->
@ -344,12 +347,19 @@
<!--Account-->
<string name="account_add_new">Add account</string>
<string name="account_logout">Logout</string>
<string name="account_confirm">Do you want to log out of an active student?</string>
<string name="account_confirm">Do you want to log out this student?</string>
<string name="account_logout_student">Student logout</string>
<string name="account_type_student">Student account</string>
<string name="account_type_parent">Parent account</string>
<string name="account_login_mobile_api">Mobile API mode</string>
<string name="account_login_hybrid">Hybrid mode</string>
<string name="account_details_edit">Edit data</string>
<string name="account_quick_manager">Accounts manager</string>
<string name="account_select_student">Select student</string>
<string name="account_family">Family</string>
<string name="account_contact">Contact</string>
<string name="account_address">Residence details</string>
<string name="account_personal_data">Personal information</string>
<!--About-->
@ -382,6 +392,28 @@
<string name="contributor_see_more">See more on GitHub</string>
<!--Student info-->
<string name="student_info_empty">No info about student</string>
<string name="student_info_first_name">Name</string>
<string name="student_info_second_name">Second name</string>
<string name="student_info_gender">Gender</string>
<string name="student_info_polish_citizenship">Polish citizenship</string>
<string name="student_info_family_name">Family name</string>
<string name="student_info_parents_name">Mother\'s and father\'s names</string>
<string name="student_info_phone">Phone</string>
<string name="student_info_cellphone">Cellphone</string>
<string name="student_info_email">E-mail</string>
<string name="student_info_address">Address of residence</string>
<string name="student_info_registered_address">Address of registration</string>
<string name="student_info_correspondence_address">Correspondence address</string>
<string name="student_info_full_name">Surname and first name</string>
<string name="student_info_kinship">Degree of kinship</string>
<string name="student_info_guardian_address">Address</string>
<string name="student_info_phones">Phones</string>
<string name="student_info_male">Male</string>
<string name="student_info_female">Female</string>
<!--Log viewer-->
<string name="logviewer_share">Share logs</string>
<string name="logviewer_refresh">Refresh</string>
@ -410,6 +442,8 @@
<string name="all_next">Next</string>
<string name="all_search">Search</string>
<string name="all_search_hint">Search…</string>
<string name="all_yes">Yes</string>
<string name="all_no">No</string>
<!--Timetable Widget-->
@ -508,4 +542,5 @@
<string name="error_unknown">An unexpected error occurred</string>
<string name="error_feature_disabled">Feature disabled by your school</string>
<string name="error_feature_not_available">Feature not available. Login in a mode other than Mobile API</string>
</resources>

View File

@ -35,7 +35,9 @@ class StudentTest {
@Test
fun testRemoteAll() {
coEvery { mockSdk.getStudentsFromScrapper(any(), any(), any(), any()) } returns listOf(getStudent("test"))
coEvery { mockSdk.getStudentsFromScrapper(any(), any(), any(), any()) } returns listOf(
getStudent("test")
)
val students = runBlocking { studentRepository.getStudentsScrapper("", "", "http://fakelog.cf", "") }
assertEquals(1, students.size)

View File

@ -41,7 +41,29 @@ class GradeAverageProviderTest {
private lateinit var gradeAverageProvider: GradeAverageProvider
private val student = Student("", "", "", "SCRAPPER", "", "", false, "", "", "", 101, 0, "", "", "", "", "", "", 1, true, LocalDateTime.now())
private val student = Student(
scrapperBaseUrl = "",
mobileBaseUrl = "",
loginType = "",
loginMode = "SCRAPPER",
certificateKey = "",
privateKey = "",
isParent = false,
email = "",
password = "",
symbol = "",
studentId = 101,
userLoginId = 0,
userName = "",
studentName = "",
schoolSymbol = "",
schoolShortName = "",
schoolName = "",
className = "",
classId = 1,
isCurrent = true,
registrationDate = LocalDateTime.now()
)
private val semesters = mutableListOf(
getSemesterEntity(10, 21, of(2019, 1, 31), of(2019, 6, 23)),

View File

@ -99,8 +99,32 @@ class LoginFormPresenterTest {
@Test
fun loginTest() {
val studentTest = Student(email = "test@", password = "123", scrapperBaseUrl = "https://fakelog.cf/", loginType = "AUTO", studentName = "", schoolSymbol = "", schoolName = "", studentId = 0, classId = 1, isCurrent = false, symbol = "", registrationDate = now(), className = "", mobileBaseUrl = "", privateKey = "", certificateKey = "", loginMode = "", userLoginId = 0, schoolShortName = "", isParent = false, userName = "")
coEvery { repository.getStudentsScrapper(any(), any(), any(), any()) } returns listOf(StudentWithSemesters(studentTest, emptyList()))
val studentTest = Student(
email = "test@",
password = "123",
scrapperBaseUrl = "https://fakelog.cf/",
loginType = "AUTO",
studentName = "",
schoolSymbol = "",
schoolName = "",
studentId = 0,
classId = 1,
isCurrent = false,
symbol = "",
registrationDate = now(),
className = "",
mobileBaseUrl = "",
privateKey = "",
certificateKey = "",
loginMode = "",
userLoginId = 0,
schoolShortName = "",
isParent = false,
userName = ""
)
coEvery { repository.getStudentsScrapper(any(), any(), any(), any()) } returns listOf(
StudentWithSemesters(studentTest, emptyList())
)
every { loginFormView.formUsernameValue } returns "@"
every { loginFormView.formPassValue } returns "123456"

View File

@ -38,7 +38,31 @@ class LoginStudentSelectPresenterTest {
private lateinit var presenter: LoginStudentSelectPresenter
private val testStudent by lazy { Student(email = "test", password = "test123", scrapperBaseUrl = "https://fakelog.cf", loginType = "AUTO", symbol = "", isCurrent = false, studentId = 0, schoolName = "", schoolSymbol = "", classId = 1, studentName = "", registrationDate = now(), className = "", loginMode = "", certificateKey = "", privateKey = "", mobileBaseUrl = "", schoolShortName = "", userLoginId = 1, isParent = false, userName = "") }
private val testStudent by lazy {
Student(
email = "test",
password = "test123",
scrapperBaseUrl = "https://fakelog.cf",
loginType = "AUTO",
symbol = "",
isCurrent = false,
studentId = 0,
schoolName = "",
schoolSymbol = "",
classId = 1,
studentName = "",
registrationDate = now(),
className = "",
loginMode = "",
certificateKey = "",
privateKey = "",
mobileBaseUrl = "",
schoolShortName = "",
userLoginId = 1,
isParent = false,
userName = ""
)
}
private val testException by lazy { RuntimeException("Problem") }
@ -64,8 +88,24 @@ class LoginStudentSelectPresenterTest {
@Test
fun onSelectedStudentTest() {
coEvery { studentRepository.saveStudents(listOf(StudentWithSemesters(testStudent, emptyList()))) } returns listOf(1L)
coEvery { studentRepository.switchStudent(StudentWithSemesters(testStudent, emptyList())) } just Runs
coEvery {
studentRepository.saveStudents(
listOf(
StudentWithSemesters(
testStudent,
emptyList()
)
)
)
} returns listOf(1L)
coEvery {
studentRepository.switchStudent(
StudentWithSemesters(
testStudent,
emptyList()
)
)
} just Runs
every { loginStudentSelectView.openMainView() } just Runs
presenter.onItemSelected(StudentWithSemesters(testStudent, emptyList()), false)
presenter.onSignIn()
@ -77,7 +117,16 @@ class LoginStudentSelectPresenterTest {
@Test
fun onSelectedStudentErrorTest() {
coEvery { studentRepository.saveStudents(listOf(StudentWithSemesters(testStudent, emptyList()))) } throws testException
coEvery {
studentRepository.saveStudents(
listOf(
StudentWithSemesters(
testStudent,
emptyList()
)
)
)
} throws testException
coEvery { studentRepository.logoutStudent(testStudent) } just Runs
presenter.onItemSelected(StudentWithSemesters(testStudent, emptyList()), false)
presenter.onSignIn()

View File

@ -7,10 +7,13 @@
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
android.enableJetifier=true
android.useAndroidX=true
org.gradle.jvmargs=-Xmx1536m
kotlin.code.style=official
kapt.incremental.apt=true
kapt.use.worker.api=true
kapt.include.compile.classpath=false
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit