From 2807659da3354131db41927246bfca3881d97b15 Mon Sep 17 00:00:00 2001 From: Antoni Czaplicki <56671347+Antoni-Czaplicki@users.noreply.github.com> Date: Mon, 18 Oct 2021 23:46:39 +0200 Subject: [PATCH] [UI] Add teachers list feature. (#94) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add teachers list feature * Fix * some fixes * Auto stash before merge of "develop" and "origin/develop" * Add teachers list feature * Fix * some fixes * [UI] Fix updating badges crashing with expanded items. * Add support for fetching teachers from vulcan, fix some code * Update app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Teacher.kt Co-authored-by: Kuba Szczodrzyński * Update some code * Update app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt Co-authored-by: Kuba Szczodrzyński * Update app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Teacher.kt Co-authored-by: Kuba Szczodrzyński * Update app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt Co-authored-by: Kuba Szczodrzyński * Update app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt Co-authored-by: Kuba Szczodrzyński * Update app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeTeachers.kt Co-authored-by: Kuba Szczodrzyński * Update app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Teacher.kt Co-authored-by: Kuba Szczodrzyński * Update app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration96.kt Co-authored-by: Kuba Szczodrzyński * Update app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt Co-authored-by: Kuba Szczodrzyński * Update app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt Co-authored-by: Kuba Szczodrzyński * Update app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeTeachers.kt Co-authored-by: Kuba Szczodrzyński * Update app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt Co-authored-by: Kuba Szczodrzyński * Update app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt Co-authored-by: Kuba Szczodrzyński * Update app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt Co-authored-by: Kuba Szczodrzyński * Update app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt Co-authored-by: Kuba Szczodrzyński * Optimize code * Fix loadTarget algorithm * Apply suggestions from code review Co-authored-by: Kuba Szczodrzyński * Update app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt Co-authored-by: Kuba Szczodrzyński Co-authored-by: Kuba Szczodrzyński --- .idea/copyright/Antoni.xml | 6 + .../pl/szczodrzynski/edziennik/Extensions.kt | 9 + .../szczodrzynski/edziennik/MainActivity.kt | 619 +++++++++++------- .../edziennik/data/api/Constants.kt | 1 + .../api/edziennik/vulcan/VulcanFeatures.kt | 4 +- .../api/edziennik/vulcan/data/VulcanData.kt | 7 +- .../vulcan/data/hebe/VulcanHebeTeachers.kt | 46 ++ .../szczodrzynski/edziennik/data/db/AppDb.kt | 3 +- .../edziennik/data/db/entity/Teacher.kt | 24 +- .../data/db/migration/Migration96.kt | 15 + .../MessagesComposeSuggestionAdapter.kt | 2 +- .../ui/modules/teachers/TeachersAdapter.kt | 69 ++ .../modules/teachers/TeachersListFragment.kt | 82 +++ ...e_suggestion_item.xml => teacher_item.xml} | 0 .../res/layout/teachers_list_fragment.xml | 43 ++ app/src/main/res/values-de/strings.xml | 4 + app/src/main/res/values-en/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + 18 files changed, 676 insertions(+), 266 deletions(-) create mode 100644 .idea/copyright/Antoni.xml create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeTeachers.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration96.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/teachers/TeachersAdapter.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/teachers/TeachersListFragment.kt rename app/src/main/res/layout/{messages_compose_suggestion_item.xml => teacher_item.xml} (100%) create mode 100644 app/src/main/res/layout/teachers_list_fragment.xml diff --git a/.idea/copyright/Antoni.xml b/.idea/copyright/Antoni.xml new file mode 100644 index 00000000..438a39d6 --- /dev/null +++ b/.idea/copyright/Antoni.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt index b2a62c8c..9504026b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt @@ -44,6 +44,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.gson.* import com.google.gson.JsonArray import com.google.gson.JsonObject +import com.mikepenz.iconics.typeface.IIcon +import com.mikepenz.materialdrawer.holder.StringHolder import im.wangchao.mhttp.Response import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay @@ -59,6 +61,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.data.db.entity.Team import pl.szczodrzynski.edziennik.utils.models.Time +import pl.szczodrzynski.navlib.ImageHolder import java.io.InterruptedIOException import java.io.PrintWriter import java.io.StringWriter @@ -862,6 +865,8 @@ fun @receiver:DrawableRes Int.resolveDrawable(context: Context): Drawable { } } +fun @receiver:StringRes Int.resolveString(context: Context) = context.getString(this) + fun View.findParentById(targetId: Int): View? { if (id == targetId) { return this @@ -1371,3 +1376,7 @@ fun CharSequence.getWordBounds(position: Int, onlyInWord: Boolean = false): Pair } infix fun Int.hasSet(what: Int) = this and what == what + +fun Int.toStringHolder() = StringHolder(this) +fun CharSequence.toStringHolder() = StringHolder(this) +fun IIcon.toImageHolder() = ImageHolder(this) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt index b27af5bb..db7e5e19 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt @@ -25,9 +25,7 @@ import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp -import com.mikepenz.materialdrawer.model.DividerDrawerItem -import com.mikepenz.materialdrawer.model.ProfileDrawerItem -import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem +import com.mikepenz.materialdrawer.model.* import com.mikepenz.materialdrawer.model.interfaces.* import com.mikepenz.materialdrawer.model.utils.hiddenInMiniDrawer import eu.szkolny.font.SzkolnyFont @@ -76,6 +74,7 @@ import pl.szczodrzynski.edziennik.ui.modules.messages.single.MessageFragment import pl.szczodrzynski.edziennik.ui.modules.notifications.NotificationsListFragment import pl.szczodrzynski.edziennik.ui.modules.settings.ProfileManagerFragment import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsFragment +import pl.szczodrzynski.edziennik.ui.modules.teachers.TeachersListFragment import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment import pl.szczodrzynski.edziennik.ui.modules.webpush.WebPushFragment import pl.szczodrzynski.edziennik.utils.* @@ -116,6 +115,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope { const val DRAWER_ITEM_ATTENDANCE = 16 const val DRAWER_ITEM_ANNOUNCEMENTS = 18 const val DRAWER_ITEM_NOTIFICATIONS = 20 + const val DRAWER_ITEM_MORE = 21 + const val DRAWER_ITEM_TEACHERS = 22 const val DRAWER_ITEM_SETTINGS = 101 const val DRAWER_ITEM_DEBUG = 102 @@ -130,104 +131,137 @@ class MainActivity : AppCompatActivity(), CoroutineScope { val navTargetList: List by lazy { val list: MutableList = mutableListOf() + val moreList: MutableList = mutableListOf() + + moreList += NavTarget(DRAWER_ITEM_TEACHERS, + R.string.menu_teachers, + TeachersListFragment::class) + .withIcon(CommunityMaterial.Icon3.cmd_shield_account_outline) + .isStatic(true) // home item list += NavTarget(DRAWER_ITEM_HOME, R.string.menu_home_page, HomeFragment::class) - .withTitle(R.string.app_name) - .withIcon(CommunityMaterial.Icon2.cmd_home_outline) - .isInDrawer(true) - .isStatic(true) - .withPopToHome(false) + .withTitle(R.string.app_name) + .withIcon(CommunityMaterial.Icon2.cmd_home_outline) + .isInDrawer(true) + .isStatic(true) + .withPopToHome(false) - list += NavTarget(DRAWER_ITEM_TIMETABLE, R.string.menu_timetable, TimetableFragment::class) - .withIcon(CommunityMaterial.Icon3.cmd_timetable) - .withBadgeTypeId(TYPE_LESSON_CHANGE) - .isInDrawer(true) + list += NavTarget(DRAWER_ITEM_TIMETABLE, + R.string.menu_timetable, + TimetableFragment::class) + .withIcon(CommunityMaterial.Icon3.cmd_timetable) + .withBadgeTypeId(TYPE_LESSON_CHANGE) + .isInDrawer(true) list += NavTarget(DRAWER_ITEM_AGENDA, R.string.menu_agenda, AgendaFragment::class) - .withIcon(CommunityMaterial.Icon.cmd_calendar_outline) - .withBadgeTypeId(TYPE_EVENT) - .isInDrawer(true) + .withIcon(CommunityMaterial.Icon.cmd_calendar_outline) + .withBadgeTypeId(TYPE_EVENT) + .isInDrawer(true) list += NavTarget(DRAWER_ITEM_GRADES, R.string.menu_grades, GradesListFragment::class) - .withIcon(CommunityMaterial.Icon3.cmd_numeric_5_box_outline) - .withBadgeTypeId(TYPE_GRADE) - .isInDrawer(true) + .withIcon(CommunityMaterial.Icon3.cmd_numeric_5_box_outline) + .withBadgeTypeId(TYPE_GRADE) + .isInDrawer(true) list += NavTarget(DRAWER_ITEM_MESSAGES, R.string.menu_messages, MessagesFragment::class) - .withIcon(CommunityMaterial.Icon.cmd_email_outline) - .withBadgeTypeId(TYPE_MESSAGE) - .isInDrawer(true) + .withIcon(CommunityMaterial.Icon.cmd_email_outline) + .withBadgeTypeId(TYPE_MESSAGE) + .isInDrawer(true) list += NavTarget(DRAWER_ITEM_HOMEWORK, R.string.menu_homework, HomeworkFragment::class) - .withIcon(SzkolnyFont.Icon.szf_notebook_outline) - .withBadgeTypeId(TYPE_HOMEWORK) - .isInDrawer(true) + .withIcon(SzkolnyFont.Icon.szf_notebook_outline) + .withBadgeTypeId(TYPE_HOMEWORK) + .isInDrawer(true) - list += NavTarget(DRAWER_ITEM_BEHAVIOUR, R.string.menu_notices, BehaviourFragment::class) - .withIcon(CommunityMaterial.Icon.cmd_emoticon_outline) - .withBadgeTypeId(TYPE_NOTICE) - .isInDrawer(true) + list += NavTarget(DRAWER_ITEM_BEHAVIOUR, + R.string.menu_notices, + BehaviourFragment::class) + .withIcon(CommunityMaterial.Icon.cmd_emoticon_outline) + .withBadgeTypeId(TYPE_NOTICE) + .isInDrawer(true) - list += NavTarget(DRAWER_ITEM_ATTENDANCE, R.string.menu_attendance, AttendanceFragment::class) - .withIcon(CommunityMaterial.Icon.cmd_calendar_remove_outline) - .withBadgeTypeId(TYPE_ATTENDANCE) - .isInDrawer(true) + list += NavTarget(DRAWER_ITEM_ATTENDANCE, + R.string.menu_attendance, + AttendanceFragment::class) + .withIcon(CommunityMaterial.Icon.cmd_calendar_remove_outline) + .withBadgeTypeId(TYPE_ATTENDANCE) + .isInDrawer(true) - list += NavTarget(DRAWER_ITEM_ANNOUNCEMENTS, R.string.menu_announcements, AnnouncementsFragment::class) - .withIcon(CommunityMaterial.Icon.cmd_bullhorn_outline) - .withBadgeTypeId(TYPE_ANNOUNCEMENT) - .isInDrawer(true) + list += NavTarget(DRAWER_ITEM_ANNOUNCEMENTS, + R.string.menu_announcements, + AnnouncementsFragment::class) + .withIcon(CommunityMaterial.Icon.cmd_bullhorn_outline) + .withBadgeTypeId(TYPE_ANNOUNCEMENT) + .isInDrawer(true) + + list += NavTarget(DRAWER_ITEM_MORE, R.string.menu_more, null) + .withIcon(CommunityMaterial.Icon.cmd_dots_horizontal_circle_outline) + .isInDrawer(true) + .isStatic(true) + .withSubItems(*moreList.toTypedArray()) // static drawer items - list += NavTarget(DRAWER_ITEM_NOTIFICATIONS, R.string.menu_notifications, NotificationsListFragment::class) - .withIcon(CommunityMaterial.Icon.cmd_bell_ring_outline) - .isInDrawer(true) - .isStatic(true) - .isBelowSeparator(true) + list += NavTarget(DRAWER_ITEM_NOTIFICATIONS, + R.string.menu_notifications, + NotificationsListFragment::class) + .withIcon(CommunityMaterial.Icon.cmd_bell_ring_outline) + .isInDrawer(true) + .isStatic(true) + .isBelowSeparator(true) list += NavTarget(DRAWER_ITEM_SETTINGS, R.string.menu_settings, SettingsFragment::class) - .withIcon(CommunityMaterial.Icon.cmd_cog_outline) - .isInDrawer(true) - .isStatic(true) - .isBelowSeparator(true) + .withIcon(CommunityMaterial.Icon.cmd_cog_outline) + .isInDrawer(true) + .isStatic(true) + .isBelowSeparator(true) // profile settings items list += NavTarget(DRAWER_PROFILE_ADD_NEW, R.string.menu_add_new_profile, null) - .withIcon(CommunityMaterial.Icon3.cmd_plus) - .withDescription(R.string.drawer_add_new_profile_desc) - .isInProfileList(true) + .withIcon(CommunityMaterial.Icon3.cmd_plus) + .withDescription(R.string.drawer_add_new_profile_desc) + .isInProfileList(true) - list += NavTarget(DRAWER_PROFILE_MANAGE, R.string.menu_manage_profiles, ProfileManagerFragment::class) - .withTitle(R.string.title_profile_manager) - .withIcon(CommunityMaterial.Icon.cmd_account_group) - .withDescription(R.string.drawer_manage_profiles_desc) - .isInProfileList(false) + list += NavTarget(DRAWER_PROFILE_MANAGE, + R.string.menu_manage_profiles, + ProfileManagerFragment::class) + .withTitle(R.string.title_profile_manager) + .withIcon(CommunityMaterial.Icon.cmd_account_group) + .withDescription(R.string.drawer_manage_profiles_desc) + .isInProfileList(false) - list += NavTarget(DRAWER_PROFILE_MARK_ALL_AS_READ, R.string.menu_mark_everything_as_read, null) - .withIcon(CommunityMaterial.Icon.cmd_eye_check_outline) - .isInProfileList(true) + list += NavTarget(DRAWER_PROFILE_MARK_ALL_AS_READ, + R.string.menu_mark_everything_as_read, + null) + .withIcon(CommunityMaterial.Icon.cmd_eye_check_outline) + .isInProfileList(true) list += NavTarget(DRAWER_PROFILE_SYNC_ALL, R.string.menu_sync_all, null) - .withIcon(CommunityMaterial.Icon.cmd_download_outline) - .isInProfileList(true) + .withIcon(CommunityMaterial.Icon.cmd_download_outline) + .isInProfileList(true) // other target items, not directly navigated - list += NavTarget(TARGET_GRADES_EDITOR, R.string.menu_grades_editor, GradesEditorFragment::class) + list += NavTarget(TARGET_GRADES_EDITOR, + R.string.menu_grades_editor, + GradesEditorFragment::class) list += NavTarget(TARGET_FEEDBACK, R.string.menu_feedback, FeedbackFragment::class) - list += NavTarget(TARGET_MESSAGES_DETAILS, R.string.menu_message, MessageFragment::class).withPopTo(DRAWER_ITEM_MESSAGES) - list += NavTarget(TARGET_MESSAGES_COMPOSE, R.string.menu_message_compose, MessagesComposeFragment::class) + list += NavTarget(TARGET_MESSAGES_DETAILS, + R.string.menu_message, + MessageFragment::class).withPopTo(DRAWER_ITEM_MESSAGES) + list += NavTarget(TARGET_MESSAGES_COMPOSE, + R.string.menu_message_compose, + MessagesComposeFragment::class) list += NavTarget(TARGET_WEB_PUSH, R.string.menu_web_push, WebPushFragment::class) if (App.devMode) { list += NavTarget(DRAWER_ITEM_DEBUG, R.string.menu_debug, DebugFragment::class) list += NavTarget(TARGET_LAB, R.string.menu_lab, LabFragment::class) - .withIcon(CommunityMaterial.Icon2.cmd_flask_outline) - .isInDrawer(true) - .isBelowSeparator(true) - .isStatic(true) + .withIcon(CommunityMaterial.Icon2.cmd_flask_outline) + .isInDrawer(true) + .isBelowSeparator(true) + .isStatic(true) } list @@ -323,9 +357,12 @@ class MainActivity : AppCompatActivity(), CoroutineScope { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { window.statusBarColor = statusBarColor } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ColorUtils.calculateLuminance(statusBarColor) > 0.6) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && ColorUtils.calculateLuminance(statusBarColor) > 0.6 + ) { @Suppress("deprecation") - window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + window.decorView.systemUiVisibility = + window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR } // TODO fix navlib navbar detection, orientation change issues, status bar color setting if not fullscreen @@ -345,8 +382,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope { fabGravity = Gravity.CENTER if (Themes.isDark) { setBackgroundColor(blendColors( - getColorFromAttr(context, R.attr.colorSurface), - getColorFromRes(R.color.colorSurface_4dp) + getColorFromAttr(context, R.attr.colorSurface), + getColorFromRes(R.color.colorSurface_4dp) )) elevation = dpToPx(4).toFloat() } @@ -385,8 +422,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope { ProfileConfigDialog(this@MainActivity, appProfile) } true - } - else { + } else { false } } @@ -407,15 +443,17 @@ class MainActivity : AppCompatActivity(), CoroutineScope { app.db.profileDao().all.observe(this) { profiles -> val allArchived = profiles.all { it.archived } - drawer.setProfileList(profiles.filter { it.id >= 0 && (!it.archived || allArchived) }.toMutableList()) + drawer.setProfileList(profiles.filter { + it.id >= 0 && (!it.archived || allArchived) + }.toMutableList()) //prepend the archived profile if loaded if (app.profile.archived && !allArchived) { drawer.prependProfile(Profile( - id = app.profile.id, - loginStoreId = app.profile.loginStoreId, - loginStoreType = app.profile.loginStoreType, - name = app.profile.name, - subname = "Archiwum - ${app.profile.subname}" + id = app.profile.id, + loginStoreId = app.profile.loginStoreId, + loginStoreType = app.profile.loginStoreType, + name = app.profile.name, + subname = "Archiwum - ${app.profile.subname}" ).also { it.archived = true }) @@ -437,9 +475,9 @@ class MainActivity : AppCompatActivity(), CoroutineScope { b.swipeRefreshLayout.isEnabled = true b.swipeRefreshLayout.setOnRefreshListener { launch { syncCurrentFeature() } } b.swipeRefreshLayout.setColorSchemeResources( - R.color.md_blue_500, - R.color.md_amber_500, - R.color.md_green_500 + R.color.md_blue_500, + R.color.md_amber_500, + R.color.md_green_500 ) SyncWorker.scheduleNext(app) @@ -469,9 +507,12 @@ class MainActivity : AppCompatActivity(), CoroutineScope { val today = Date.getToday() if ((today.month % 11 == 1) && app.config.ui.snowfall) { b.rootFrame.addView(layoutInflater.inflate(R.layout.snowfall, b.rootFrame, false)) - } - else if (app.config.ui.eggfall && BigNightUtil().isDataWielkanocyNearDzisiaj()) { - val eggfall = layoutInflater.inflate(R.layout.eggfall, b.rootFrame, false) as SnowfallView + } else if (app.config.ui.eggfall && BigNightUtil().isDataWielkanocyNearDzisiaj()) { + val eggfall = layoutInflater.inflate( + R.layout.eggfall, + b.rootFrame, + false + ) as SnowfallView eggfall.setSnowflakeBitmaps(listOf( BitmapFactory.decodeResource(resources, R.drawable.egg1), BitmapFactory.decodeResource(resources, R.drawable.egg2), @@ -500,65 +541,68 @@ class MainActivity : AppCompatActivity(), CoroutineScope { if (app.config.appRateSnackbarTime != 0L && app.config.appRateSnackbarTime <= System.currentTimeMillis()) { navView.coordinator.postDelayed({ CafeBar.builder(this) - .content(R.string.rate_snackbar_text) - .icon(IconicsDrawable(this).apply { - icon = CommunityMaterial.Icon3.cmd_star_outline - sizeDp = 24 - colorInt = Themes.getPrimaryTextColor(this@MainActivity) - }) - .positiveText(R.string.rate_snackbar_positive) - .positiveColor(-0xb350b0) - .negativeText(R.string.rate_snackbar_negative) - .negativeColor(0xff666666.toInt()) - .neutralText(R.string.rate_snackbar_neutral) - .neutralColor(0xff666666.toInt()) - .onPositive { cafeBar -> - Utils.openGooglePlay(this) - cafeBar.dismiss() - app.config.appRateSnackbarTime = 0 - } - .onNegative { cafeBar -> - Toast.makeText(this, R.string.rate_snackbar_negative_message, Toast.LENGTH_LONG).show() - cafeBar.dismiss() - app.config.appRateSnackbarTime = 0 - } - .onNeutral { cafeBar -> - Toast.makeText(this, R.string.ok, Toast.LENGTH_LONG).show() - cafeBar.dismiss() - app.config.appRateSnackbarTime = System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000 - } - .autoDismiss(false) - .swipeToDismiss(true) - .floating(true) - .show() + .content(R.string.rate_snackbar_text) + .icon(IconicsDrawable(this).apply { + icon = CommunityMaterial.Icon3.cmd_star_outline + sizeDp = 24 + colorInt = Themes.getPrimaryTextColor(this@MainActivity) + }) + .positiveText(R.string.rate_snackbar_positive) + .positiveColor(-0xb350b0) + .negativeText(R.string.rate_snackbar_negative) + .negativeColor(0xff666666.toInt()) + .neutralText(R.string.rate_snackbar_neutral) + .neutralColor(0xff666666.toInt()) + .onPositive { cafeBar -> + Utils.openGooglePlay(this) + cafeBar.dismiss() + app.config.appRateSnackbarTime = 0 + } + .onNegative { cafeBar -> + Toast.makeText(this, + R.string.rate_snackbar_negative_message, + Toast.LENGTH_LONG).show() + cafeBar.dismiss() + app.config.appRateSnackbarTime = 0 + } + .onNeutral { cafeBar -> + Toast.makeText(this, R.string.ok, Toast.LENGTH_LONG).show() + cafeBar.dismiss() + app.config.appRateSnackbarTime = + System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000 + } + .autoDismiss(false) + .swipeToDismiss(true) + .floating(true) + .show() }, 10000) } // CONTEXT MENU ITEMS bottomSheet.removeAllItems() bottomSheet.appendItems( - BottomSheetPrimaryItem(false) - .withTitle(R.string.menu_sync) - .withIcon(CommunityMaterial.Icon.cmd_download_outline) - .withOnClickListener { - bottomSheet.close() - SyncViewListDialog(this, navTargetId) - }, - BottomSheetSeparatorItem(false), - BottomSheetPrimaryItem(false) - .withTitle(R.string.menu_settings) - .withIcon(CommunityMaterial.Icon.cmd_cog_outline) - .withOnClickListener { loadTarget(DRAWER_ITEM_SETTINGS) }, - BottomSheetPrimaryItem(false) - .withTitle(R.string.menu_feedback) - .withIcon(CommunityMaterial.Icon2.cmd_help_circle_outline) - .withOnClickListener { loadTarget(TARGET_FEEDBACK) } + BottomSheetPrimaryItem(false) + .withTitle(R.string.menu_sync) + .withIcon(CommunityMaterial.Icon.cmd_download_outline) + .withOnClickListener { + bottomSheet.close() + SyncViewListDialog(this, navTargetId) + }, + BottomSheetSeparatorItem(false), + BottomSheetPrimaryItem(false) + .withTitle(R.string.menu_settings) + .withIcon(CommunityMaterial.Icon.cmd_cog_outline) + .withOnClickListener { loadTarget(DRAWER_ITEM_SETTINGS) }, + BottomSheetPrimaryItem(false) + .withTitle(R.string.menu_feedback) + .withIcon(CommunityMaterial.Icon2.cmd_help_circle_outline) + .withOnClickListener { loadTarget(TARGET_FEEDBACK) } ) if (App.devMode) { bottomSheet += BottomSheetPrimaryItem(false) - .withTitle(R.string.menu_debug) - .withIcon(CommunityMaterial.Icon.cmd_android_debug_bridge) - .withOnClickListener { loadTarget(DRAWER_ITEM_DEBUG) } + .withTitle(R.string.menu_debug) + .withIcon(CommunityMaterial.Icon.cmd_android_debug_bridge) + .withOnClickListener { loadTarget(DRAWER_ITEM_DEBUG) } } } @@ -570,17 +614,22 @@ class MainActivity : AppCompatActivity(), CoroutineScope { DRAWER_PROFILE_SYNC_ALL -> { EdziennikTask.sync().enqueue(this) } - DRAWER_PROFILE_MARK_ALL_AS_READ -> { launch { - withContext(Dispatchers.Default) { - app.db.profileDao().allNow.forEach { profile -> - if (profile.loginStoreType != LoginStore.LOGIN_TYPE_LIBRUS) - app.db.metadataDao().setAllSeenExceptMessagesAndAnnouncements(profile.id, true) - else - app.db.metadataDao().setAllSeenExceptMessages(profile.id, true) + DRAWER_PROFILE_MARK_ALL_AS_READ -> { + launch { + withContext(Dispatchers.Default) { + app.db.profileDao().allNow.forEach { profile -> + if (profile.loginStoreType != LoginStore.LOGIN_TYPE_LIBRUS) + app.db.metadataDao() + .setAllSeenExceptMessagesAndAnnouncements(profile.id, true) + else + app.db.metadataDao().setAllSeenExceptMessages(profile.id, true) + } } + Toast.makeText(this@MainActivity, + R.string.main_menu_mark_as_read_success, + Toast.LENGTH_SHORT).show() } - Toast.makeText(this@MainActivity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show() - }} + } else -> { loadTarget(id) } @@ -599,36 +648,36 @@ class MainActivity : AppCompatActivity(), CoroutineScope { private suspend fun syncCurrentFeature() { if (app.profile.archived) { MaterialAlertDialogBuilder(this) - .setTitle(R.string.profile_archived_title) - .setMessage( - R.string.profile_archived_text, - app.profile.studentSchoolYearStart, - app.profile.studentSchoolYearStart + 1 - ) - .setPositiveButton(R.string.ok, null) - .show() + .setTitle(R.string.profile_archived_title) + .setMessage( + R.string.profile_archived_text, + app.profile.studentSchoolYearStart, + app.profile.studentSchoolYearStart + 1 + ) + .setPositiveButton(R.string.ok, null) + .show() swipeRefreshLayout.isRefreshing = false return } if (app.profile.shouldArchive()) { MaterialAlertDialogBuilder(this) - .setTitle(R.string.profile_archiving_title) - .setMessage( - R.string.profile_archiving_format, - app.profile.dateYearEnd.formattedString - ) - .setPositiveButton(R.string.ok, null) - .show() + .setTitle(R.string.profile_archiving_title) + .setMessage( + R.string.profile_archiving_format, + app.profile.dateYearEnd.formattedString + ) + .setPositiveButton(R.string.ok, null) + .show() } if (app.profile.isBeforeYear()) { MaterialAlertDialogBuilder(this) - .setTitle(R.string.profile_year_not_started_title) - .setMessage( - R.string.profile_year_not_started_format, - app.profile.dateSemester1Start.formattedString - ) - .setPositiveButton(R.string.ok, null) - .show() + .setTitle(R.string.profile_year_not_started_title) + .setMessage( + R.string.profile_year_not_started_format, + app.profile.dateSemester1Start.formattedString + ) + .setPositiveButton(R.string.ok, null) + .show() swipeRefreshLayout.isRefreshing = false return } @@ -663,16 +712,18 @@ class MainActivity : AppCompatActivity(), CoroutineScope { else -> null } EdziennikTask.syncProfile( - App.profileId, - listOf(navTargetId to fragmentParam), - arguments = arguments + App.profileId, + listOf(navTargetId to fragmentParam), + arguments = arguments ).enqueue(this) } + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) fun onUpdateEvent(event: Update) { EventBus.getDefault().removeStickyEvent(event) UpdateAvailableDialog(this, event) } + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) fun onRegisterAvailabilityEvent(event: RegisterAvailabilityEvent) { EventBus.getDefault().removeStickyEvent(event) @@ -681,6 +732,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope { RegisterUnavailableDialog(this, error.status!!) } } + @Subscribe(threadMode = ThreadMode.MAIN) fun onApiTaskStartedEvent(event: ApiTaskStartedEvent) { swipeRefreshLayout.isRefreshing = true @@ -692,6 +744,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope { } } } + @Subscribe(threadMode = ThreadMode.MAIN) fun onProfileListEmptyEvent(event: ProfileListEmptyEvent) { d(TAG, "Profile list is empty. Launch LoginActivity.") @@ -699,6 +752,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope { startActivity(Intent(this, LoginActivity::class.java)) finish() } + @Subscribe(threadMode = ThreadMode.MAIN) fun onApiTaskProgressEvent(event: ApiTaskProgressEvent) { if (event.profileId == App.profileId) { @@ -708,11 +762,16 @@ class MainActivity : AppCompatActivity(), CoroutineScope { subtitle = if (event.progress < 0f) event.progressText ?: "" else - getString(R.string.toolbar_subtitle_syncing_format, event.progress.roundToInt(), event.progressText ?: "") + getString( + R.string.toolbar_subtitle_syncing_format, + event.progress.roundToInt(), + event.progressText ?: "", + ) } } } + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) fun onApiTaskFinishedEvent(event: ApiTaskFinishedEvent) { EventBus.getDefault().removeStickyEvent(event) @@ -724,11 +783,13 @@ class MainActivity : AppCompatActivity(), CoroutineScope { } } } + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) fun onApiTaskAllFinishedEvent(event: ApiTaskAllFinishedEvent) { EventBus.getDefault().removeStickyEvent(event) swipeRefreshLayout.isRefreshing = false } + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) fun onApiTaskErrorEvent(event: ApiTaskErrorEvent) { EventBus.getDefault().removeStickyEvent(event) @@ -745,36 +806,41 @@ class MainActivity : AppCompatActivity(), CoroutineScope { mainSnackbar.dismiss() errorSnackbar.addError(event.error).show() } + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) fun onAppManagerDetectedEvent(event: AppManagerDetectedEvent) { EventBus.getDefault().removeStickyEvent(event) if (app.config.sync.dontShowAppManagerDialog) return MaterialAlertDialogBuilder(this) - .setTitle(R.string.app_manager_dialog_title) - .setMessage(R.string.app_manager_dialog_text) - .setPositiveButton(R.string.ok) { _, _ -> - try { - for (intent in appManagerIntentList) { - if (packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) { - startActivity(intent) - } - } - } catch (e: Exception) { - try { - startActivity(Intent(Settings.ACTION_SETTINGS)) - } catch (e: Exception) { - e.printStackTrace() - Toast.makeText(this, R.string.app_manager_open_failed, Toast.LENGTH_SHORT).show() + .setTitle(R.string.app_manager_dialog_title) + .setMessage(R.string.app_manager_dialog_text) + .setPositiveButton(R.string.ok) { _, _ -> + try { + for (intent in appManagerIntentList) { + if (packageManager.resolveActivity(intent, + PackageManager.MATCH_DEFAULT_ONLY) != null + ) { + startActivity(intent) } } + } catch (e: Exception) { + try { + startActivity(Intent(Settings.ACTION_SETTINGS)) + } catch (e: Exception) { + e.printStackTrace() + Toast.makeText(this, R.string.app_manager_open_failed, Toast.LENGTH_SHORT) + .show() + } } - .setNeutralButton(R.string.dont_ask_again) { _, _ -> - app.config.sync.dontShowAppManagerDialog = true - } - .setCancelable(false) - .show() + } + .setNeutralButton(R.string.dont_ask_again) { _, _ -> + app.config.sync.dontShowAppManagerDialog = true + } + .setCancelable(false) + .show() } + @Subscribe(threadMode = ThreadMode.MAIN) fun onUserActionRequiredEvent(event: UserActionRequiredEvent) { app.userActionManager.execute(this, event.profileId, event.type) @@ -808,11 +874,12 @@ class MainActivity : AppCompatActivity(), CoroutineScope { handleIntent(intent?.extras) } } + fun handleIntent(extras: Bundle?) { d(TAG, "handleIntent() {") extras?.keySet()?.forEach { key -> - d(TAG, " \"$key\": "+extras.get(key)) + d(TAG, " \"$key\": " + extras.get(key)) } d(TAG, "}") @@ -823,9 +890,9 @@ class MainActivity : AppCompatActivity(), CoroutineScope { val handled = when (extras.getString("action")) { "serverMessage" -> { ServerMessageDialog( - this, - extras.getString("serverMessageTitle") ?: getString(R.string.app_name), - extras.getString("serverMessageText") ?: "" + this, + extras.getString("serverMessageTitle") ?: getString(R.string.app_name), + extras.getString("serverMessageText") ?: "" ) true } @@ -835,18 +902,20 @@ class MainActivity : AppCompatActivity(), CoroutineScope { } "userActionRequired" -> { app.userActionManager.execute( - this, - extras.getInt("profileId"), - extras.getInt("type") + this, + extras.getInt("profileId"), + extras.getInt("type") ) true } "createManualEvent" -> { - val date = extras.getString("eventDate")?.let { Date.fromY_m_d(it) } ?: Date.getToday() + val date = extras.getString("eventDate") + ?.let { Date.fromY_m_d(it) } + ?: Date.getToday() EventManualDialog( - this, - App.profileId, - defaultDate = date + this, + App.profileId, + defaultDate = date ) true } @@ -913,9 +982,11 @@ class MainActivity : AppCompatActivity(), CoroutineScope { override fun recreate() { recreate(navTargetId) } + fun recreate(targetId: Int) { recreate(targetId, null) } + fun recreate(targetId: Int? = null, arguments: Bundle? = null) { val intent = Intent(this, MainActivity::class.java) if (arguments != null) @@ -932,10 +1003,12 @@ class MainActivity : AppCompatActivity(), CoroutineScope { d(TAG, "Activity started") super.onStart() } + override fun onStop() { d(TAG, "Activity stopped") super.onStop() } + override fun onResume() { d(TAG, "Activity resumed") val filter = IntentFilter() @@ -944,12 +1017,14 @@ class MainActivity : AppCompatActivity(), CoroutineScope { EventBus.getDefault().register(this) super.onResume() } + override fun onPause() { d(TAG, "Activity paused") unregisterReceiver(intentReceiver) EventBus.getDefault().unregister(this) super.onPause() } + override fun onDestroy() { d(TAG, "Activity destroyed") super.onDestroy() @@ -978,11 +1053,11 @@ class MainActivity : AppCompatActivity(), CoroutineScope { | |___| (_) | (_| | (_| | | | | | | | __/ |_| | | | (_) | (_| \__ \ |______\___/ \__,_|\__,_| |_| |_| |_|\___|\__|_| |_|\___/ \__,_|__*/ val navOptions = NavOptions.Builder() - .setEnterAnim(R.anim.task_open_enter) // new fragment enter - .setExitAnim(R.anim.task_open_exit) // old fragment exit - .setPopEnterAnim(R.anim.task_close_enter) // old fragment enter back - .setPopExitAnim(R.anim.task_close_exit) // new fragment exit - .build() + .setEnterAnim(R.anim.task_open_enter) // new fragment enter + .setExitAnim(R.anim.task_open_exit) // old fragment exit + .setPopEnterAnim(R.anim.task_close_enter) // old fragment enter back + .setPopExitAnim(R.anim.task_close_exit) // new fragment exit + .build() private fun canNavigate(): Boolean = onBeforeNavigate?.invoke() != false @@ -1010,6 +1085,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope { } fun loadProfile(id: Int) = loadProfile(id, navTargetId) + // fun loadProfile(id: Int, arguments: Bundle?) = loadProfile(id, navTargetId, arguments) fun loadProfile(profile: Profile): Boolean { if (!canNavigate()) { @@ -1024,6 +1100,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope { loadProfile(profile, navTargetId, null) return true } + private fun loadProfile( id: Int, drawerSelection: Int, @@ -1055,6 +1132,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope { } return true } + private fun loadProfile(profile: Profile, drawerSelection: Int, arguments: Bundle?) { App.profile = profile MessagesFragment.pageSelection = -1 @@ -1068,11 +1146,11 @@ class MainActivity : AppCompatActivity(), CoroutineScope { } if (profile.archived) { drawer.prependProfile(Profile( - id = profile.id, - loginStoreId = profile.loginStoreId, - loginStoreType = profile.loginStoreType, - name = profile.name, - subname = "Archiwum - ${profile.subname}" + id = profile.id, + loginStoreId = profile.loginStoreId, + loginStoreType = profile.loginStoreType, + name = profile.name, + subname = "Archiwum - ${profile.subname}" ).also { it.archived = true }) @@ -1084,6 +1162,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope { drawer.currentProfile = app.profileId loadTarget(drawerSelection, arguments, skipBeforeNavigate = true) } + fun loadTarget( id: Int, arguments: Bundle? = null, @@ -1093,15 +1172,28 @@ class MainActivity : AppCompatActivity(), CoroutineScope { if (loadId == -1) { loadId = DRAWER_ITEM_HOME } - val target = navTargetList - .firstOrNull { it.id == loadId } - return if (target == null) { - Toast.makeText(this, getString(R.string.error_invalid_fragment, id), Toast.LENGTH_LONG).show() - loadTarget(navTargetList.first(), arguments, skipBeforeNavigate) - } else { - loadTarget(target, arguments, skipBeforeNavigate) + val targets = navTargetList + .flatMap { it.subItems?.toList() ?: emptyList() } + .plus(navTargetList) + val target = targets.firstOrNull { it.id == loadId } + return when { + target == null -> { + Toast.makeText( + this, + getString(R.string.error_invalid_fragment, id), + Toast.LENGTH_LONG, + ).show() + loadTarget(navTargetList.first(), arguments, skipBeforeNavigate) + } + target.fragmentClass != null -> { + loadTarget(target, arguments, skipBeforeNavigate) + } + else -> { + false + } } } + private fun loadTarget( target: NavTarget, args: Bundle? = null, @@ -1120,7 +1212,9 @@ class MainActivity : AppCompatActivity(), CoroutineScope { } pausedNavigationData = null - val arguments = args ?: navBackStack.firstOrNull { it.first.id == target.id }?.second ?: Bundle() + val arguments = args + ?: navBackStack.firstOrNull { it.first.id == target.id }?.second + ?: Bundle() bottomSheet.close() bottomSheet.removeAllContextual() bottomSheet.toggleGroupEnabled = false @@ -1132,7 +1226,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope { navView.bottomBar.fabExtended = false navView.bottomBar.setFabOnClickListener(null) - d("NavDebug", "Navigating from ${navTarget.fragmentClass?.java?.simpleName} to ${target.fragmentClass?.java?.simpleName}") + d("NavDebug", + "Navigating from ${navTarget.fragmentClass?.java?.simpleName} to ${target.fragmentClass?.java?.simpleName}") val fragment = target.fragmentClass?.java?.newInstance() ?: return false fragment.arguments = arguments @@ -1141,18 +1236,17 @@ class MainActivity : AppCompatActivity(), CoroutineScope { if (navTarget == target) { // just reload the current target transaction.setCustomAnimations( - R.anim.fade_in, - R.anim.fade_out + R.anim.fade_in, + R.anim.fade_out ) - } - else { + } else { navBackStack.keys().lastIndexOf(target).let { if (it == -1) return@let target // pop the back stack up until that target transaction.setCustomAnimations( - R.anim.task_close_enter, - R.anim.task_close_exit + R.anim.task_close_enter, + R.anim.task_close_exit ) // navigating grades_add -> grades @@ -1175,8 +1269,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope { // target is neither current nor in the back stack // so navigate to it transaction.setCustomAnimations( - R.anim.task_open_enter, - R.anim.task_open_exit + R.anim.task_open_enter, + R.anim.task_open_exit ) navBackStack.add(navTarget to navArguments) navTarget = target @@ -1193,7 +1287,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope { } } - d("NavDebug", "Current fragment ${navTarget.fragmentClass?.java?.simpleName}, pop to home ${navTarget.popToHome}, back stack:") + d("NavDebug", + "Current fragment ${navTarget.fragmentClass?.java?.simpleName}, pop to home ${navTarget.popToHome}, back stack:") navBackStack.forEachIndexed { index, target2 -> d("NavDebug", " - $index: ${target2.first.fragmentClass?.java?.simpleName}") } @@ -1204,16 +1299,21 @@ class MainActivity : AppCompatActivity(), CoroutineScope { // TASK DESCRIPTION if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { val bm = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher) + @Suppress("deprecation") val taskDesc = ActivityManager.TaskDescription( - if (target.id == HOME_ID) getString(R.string.app_name) else getString(R.string.app_task_format, getString(target.name)), - bm, - getColorFromAttr(this, R.attr.colorSurface) + if (target.id == HOME_ID) + getString(R.string.app_name) + else + getString(R.string.app_task_format, getString(target.name)), + bm, + getColorFromAttr(this, R.attr.colorSurface) ) setTaskDescription(taskDesc) } return true } + fun reloadTarget() = loadTarget(navTarget) private fun popBackStack(skipBeforeNavigate: Boolean = false): Boolean { @@ -1236,6 +1336,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope { } return true } + fun navigateUp(skipBeforeNavigate: Boolean = false) { if (!popBackStack(skipBeforeNavigate)) { super.onBackPressed() @@ -1287,29 +1388,31 @@ class MainActivity : AppCompatActivity(), CoroutineScope { |_____/|_| \__,_| \_/\_/ \___|_| |_|\__\___|_| |_| |_|__*/ @Suppress("UNUSED_PARAMETER") private fun createDrawerItem(target: NavTarget, level: Int = 1): IDrawerItem<*> { - val item = DrawerPrimaryItem().apply { - identifier = target.id.toLong() - nameRes = target.name - hiddenInMiniDrawer = !app.config.ui.miniMenuButtons.contains(target.id) - if (target.description != null) - descriptionRes = target.description!! - if (target.icon != null) - withIcon(target.icon!!) - if (target.title != null) - appTitle = getString(target.title!!) - if (target.badgeTypeId != null) - badgeStyle = drawer.badgeStyle - isSelectedBackgroundAnimated = false + val item = when { + target.subItems != null -> ExpandableDrawerItem() + level > 1 -> SecondaryDrawerItem() + else -> DrawerPrimaryItem() + } + + item.also { + it.identifier = target.id.toLong() + it.nameRes = target.name + it.hiddenInMiniDrawer = !app.config.ui.miniMenuButtons.contains(target.id) + it.description = target.description?.toStringHolder() + it.icon = target.icon?.toImageHolder() + if (it is DrawerPrimaryItem) + it.appTitle = target.title?.resolveString(this) + if (it is ColorfulBadgeable && target.badgeTypeId != null) + it.badgeStyle = drawer.badgeStyle + it.isSelectedBackgroundAnimated = false + it.level = level } if (target.badgeTypeId != null) drawer.addUnreadCounterType(target.badgeTypeId!!, target.id) - // TODO sub items - /* - if (target.subItems != null) { - for (subItem in target.subItems!!) { - item.subItems += createDrawerItem(subItem, level+1) - } - }*/ + + item.subItems = target.subItems?.map { + createDrawerItem(it, level + 1) + }?.toMutableList() ?: mutableListOf() return item } @@ -1334,7 +1437,11 @@ class MainActivity : AppCompatActivity(), CoroutineScope { if (target.popToHome) targetPopToHomeList += target.id - if (target.isInDrawer && (target.isStatic || supportedFragments.isEmpty() || supportedFragments.contains(target.id))) { + if (target.isInDrawer && ( + target.isStatic + || supportedFragments.isEmpty() + || supportedFragments.contains(target.id)) + ) { drawerItems += createDrawerItem(target) if (target.id == 1) { targetHomeId = target.id @@ -1367,7 +1474,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope { override fun onBackPressed() { if (!b.navView.onBackPressed()) { if (App.config.ui.openDrawerOnBackPressed && ((navTarget.popTo == null && navTarget.popToHome) - || navTarget.id == DRAWER_ITEM_HOME)) { + || navTarget.id == DRAWER_ITEM_HOME) + ) { b.navView.drawer.toggle() } else { navigateUp() @@ -1376,6 +1484,11 @@ class MainActivity : AppCompatActivity(), CoroutineScope { } fun error(error: ApiError) = errorSnackbar.addError(error).show() - fun snackbar(text: String, actionText: String? = null, onClick: (() -> Unit)? = null) = mainSnackbar.snackbar(text, actionText, onClick) + fun snackbar( + text: String, + actionText: String? = null, + onClick: (() -> Unit)? = null, + ) = mainSnackbar.snackbar(text, actionText, onClick) + fun snackbarDismiss() = mainSnackbar.dismiss() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt index cb0d5a06..1cb7587a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt @@ -109,6 +109,7 @@ const val VULCAN_HEBE_ENDPOINT_PUSH_ALL = "api/mobile/push/all" const val VULCAN_HEBE_ENDPOINT_TIMETABLE = "api/mobile/schedule" const val VULCAN_HEBE_ENDPOINT_TIMETABLE_CHANGES = "api/mobile/schedule/changes" const val VULCAN_HEBE_ENDPOINT_ADDRESSBOOK = "api/mobile/addressbook" +const val VULCAN_HEBE_ENDPOINT_TEACHERS = "api/mobile/teacher" const val VULCAN_HEBE_ENDPOINT_EXAMS = "api/mobile/exam" const val VULCAN_HEBE_ENDPOINT_GRADES = "api/mobile/grade" const val VULCAN_HEBE_ENDPOINT_GRADE_SUMMARY = "api/mobile/grade/summary" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/VulcanFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/VulcanFeatures.kt index 2e239a7e..924d7ced 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/VulcanFeatures.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/VulcanFeatures.kt @@ -21,6 +21,7 @@ const val ENDPOINT_VULCAN_HEBE_NOTICES = 3070 const val ENDPOINT_VULCAN_HEBE_ATTENDANCE = 3080 const val ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX = 3090 const val ENDPOINT_VULCAN_HEBE_MESSAGES_SENT = 3100 +const val ENDPOINT_VULCAN_HEBE_TEACHERS = 3110 const val ENDPOINT_VULCAN_HEBE_LUCKY_NUMBER = 3200 val VulcanFeatures = listOf( @@ -83,6 +84,7 @@ val VulcanFeatures = listOf( Feature(LOGIN_TYPE_VULCAN, FEATURE_ALWAYS_NEEDED, listOf( ENDPOINT_VULCAN_HEBE_MAIN to LOGIN_METHOD_VULCAN_HEBE, - ENDPOINT_VULCAN_HEBE_ADDRESSBOOK to LOGIN_METHOD_VULCAN_HEBE + ENDPOINT_VULCAN_HEBE_ADDRESSBOOK to LOGIN_METHOD_VULCAN_HEBE, + ENDPOINT_VULCAN_HEBE_TEACHERS to LOGIN_METHOD_VULCAN_HEBE ), listOf(LOGIN_METHOD_VULCAN_HEBE)) ) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanData.kt index 0724ca46..6170d817 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanData.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanData.kt @@ -27,6 +27,7 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) { ENDPOINT_VULCAN_HEBE_NOTICES, ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX, ENDPOINT_VULCAN_HEBE_MESSAGES_SENT, + ENDPOINT_VULCAN_HEBE_TEACHERS, ENDPOINT_VULCAN_HEBE_LUCKY_NUMBER ) @@ -103,9 +104,13 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) { VulcanHebePushConfig(data, lastSync, onSuccess) } ENDPOINT_VULCAN_HEBE_ADDRESSBOOK -> { - data.startProgress(R.string.edziennik_progress_endpoint_teachers) + data.startProgress(R.string.edziennik_progress_endpoint_addressbook) VulcanHebeAddressbook(data, lastSync, onSuccess) } + ENDPOINT_VULCAN_HEBE_TEACHERS -> { + data.startProgress(R.string.edziennik_progress_endpoint_teachers) + VulcanHebeTeachers(data, lastSync, onSuccess) + } ENDPOINT_VULCAN_HEBE_TIMETABLE -> { data.startProgress(R.string.edziennik_progress_endpoint_timetable) VulcanHebeTimetable(data, lastSync, onSuccess) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeTeachers.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeTeachers.kt new file mode 100644 index 00000000..f2fdfe40 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeTeachers.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) Antoni Czaplicki 2021-10-15. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe + +import pl.szczodrzynski.edziennik.DAY +import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_TEACHERS +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_TEACHERS +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe +import pl.szczodrzynski.edziennik.getString + +class VulcanHebeTeachers( + override val data: DataVulcan, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit, +) : VulcanHebe(data, lastSync) { + companion object { + const val TAG = "VulcanHebeTeachers" + } + + init { + apiGetList( + TAG, + VULCAN_HEBE_ENDPOINT_TEACHERS, + HebeFilterType.BY_PERIOD, + lastSync = lastSync, + ) { list, _ -> + list.forEach { person -> + val name = person.getString("Name") + val surname = person.getString("Surname") + val displayName = person.getString("DisplayName") + val subjectName = person.getString("Description") ?: return@apiGetList + + val teacher = data.getTeacherByFirstLast( + name?.plus(" ")?.plus(surname) ?: displayName ?: return@forEach + ) + + teacher.addSubject(data.getSubject(null, subjectName).id) + } + data.setSyncNext(ENDPOINT_VULCAN_HEBE_TEACHERS, 2 * DAY) + onSuccess(ENDPOINT_VULCAN_HEBE_TEACHERS) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt index 868a5aef..61a7420b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt @@ -43,7 +43,7 @@ import pl.szczodrzynski.edziennik.data.db.migration.* LibrusLesson::class, TimetableManual::class, Metadata::class -], version = 95) +], version = 96) @TypeConverters( ConverterTime::class, ConverterDate::class, @@ -181,6 +181,7 @@ abstract class AppDb : RoomDatabase() { Migration93(), Migration94(), Migration95(), + Migration96(), ).allowMainThreadQueries().build() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Teacher.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Teacher.kt index c41b8a7a..cf5aa195 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Teacher.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Teacher.kt @@ -9,14 +9,11 @@ import android.graphics.Bitmap import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.Ignore -import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.fixName -import pl.szczodrzynski.edziennik.getNameInitials -import pl.szczodrzynski.edziennik.join +import pl.szczodrzynski.edziennik.* import java.util.* @Entity(tableName = "teachers", - primaryKeys = ["profileId", "teacherId"]) + primaryKeys = ["profileId", "teacherId"]) open class Teacher { companion object { const val TYPE_TEACHER = 0 // 1 @@ -26,6 +23,7 @@ open class Teacher { const val TYPE_SECRETARIAT = 4 // 16 const val TYPE_PRINCIPAL = 5 // 32 const val TYPE_SCHOOL_ADMIN = 6 // 64 + // not teachers const val TYPE_SPECIALIST = 7 // 128 const val TYPE_SUPER_ADMIN = 10 // 1024 @@ -36,7 +34,8 @@ open class Teacher { const val TYPE_OTHER = 24 // 16777216 const val IS_TEACHER_MASK = 127 - val types: List by lazy { listOf( + val types: List by lazy { + listOf( TYPE_TEACHER, TYPE_EDUCATOR, TYPE_PEDAGOGUE, @@ -51,7 +50,8 @@ open class Teacher { TYPE_PARENTS_COUNCIL, TYPE_SCHOOL_PARENTS_COUNCIL, TYPE_OTHER - ) } + ) + } fun typeName(c: Context, type: Int, typeDescription: String? = null): String { val suffix = typeDescription?.let { " ($typeDescription)" } ?: "" @@ -94,6 +94,9 @@ open class Teacher { @ColumnInfo(name = "teacherTypeDescription") var typeDescription: String? = null + @ColumnInfo(name = "teacherSubjects") + var subjects = mutableListOf() + fun isType(checkingType: Int): Boolean { return type and (1 shl checkingType) >= 1 } @@ -105,6 +108,8 @@ open class Teacher { type = type or (1 shl i) } + fun addSubject(subjectId: Long) = subjects.add(subjectId) + fun unsetTeacherType(i: Int) { type = type and (1 shl i).inv() } @@ -128,6 +133,7 @@ open class Teacher { */ @Ignore var recipientDisplayName: CharSequence? = null + /** * Used in Message composing - determining the priority * of search result, based on the search phrase match @@ -142,8 +148,6 @@ open class Teacher { this.id = id } - - @Ignore constructor(profileId: Int, id: Long, name: String, surname: String) { this.profileId = profileId @@ -170,6 +174,7 @@ open class Teacher { this.surname = it.surname this.type = it.type this.typeDescription = it.typeDescription + this.subjects = it.subjects this.image = it.image this.recipientDisplayName = it.recipientDisplayName } @@ -195,6 +200,7 @@ open class Teacher { ", name='" + name + '\'' + ", surname='" + surname + '\'' + ", type=" + dumpType() + + ", subjects=" + subjects.joinToString() + ", typeDescription='" + typeDescription + '\'' + '}' } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration96.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration96.kt new file mode 100644 index 00000000..52f77256 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration96.kt @@ -0,0 +1,15 @@ +/* + * Copyright (c) Antoni Czaplicki 2021-10-17. + */ + +package pl.szczodrzynski.edziennik.data.db.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration96 : Migration(95, 96) { + override fun migrate(database: SupportSQLiteDatabase) { + // teachers - associated subjects list + database.execSQL("ALTER TABLE teachers ADD COLUMN teacherSubjects TEXT NOT NULL;") + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeSuggestionAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeSuggestionAdapter.kt index 83a5ba39..34a142ea 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeSuggestionAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeSuggestionAdapter.kt @@ -30,7 +30,7 @@ class MessagesComposeSuggestionAdapter( private val comparator by lazy { Comparator { o1: Teacher, o2: Teacher -> o1.recipientWeight - o2.recipientWeight } } override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val listItem = convertView ?: LayoutInflater.from(context).inflate(R.layout.messages_compose_suggestion_item, parent, false) + val listItem = convertView ?: LayoutInflater.from(context).inflate(R.layout.teacher_item, parent, false) val teacher = teacherList[position] val name = listItem.findViewById(R.id.name) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/teachers/TeachersAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/teachers/TeachersAdapter.kt new file mode 100644 index 00000000..e1702e14 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/teachers/TeachersAdapter.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) Antoni Czaplicki 2021-10-15. + */ + +package pl.szczodrzynski.edziennik.ui.modules.teachers + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.databinding.TeacherItemBinding +import pl.szczodrzynski.edziennik.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils.getProfileImage +import pl.szczodrzynski.edziennik.utils.BetterLink +import kotlin.coroutines.CoroutineContext + +class TeachersAdapter( + private val activity: AppCompatActivity, + val onItemClick: ((item: Teacher) -> Unit)? = null, +) : RecyclerView.Adapter(), CoroutineScope { + companion object { + private const val TAG = "TeachersAdapter" + } + + private val app = activity.applicationContext as App + // optional: place the manager here + + private val job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + var items = listOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val inflater = LayoutInflater.from(activity) + val view = TeacherItemBinding.inflate(inflater, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = items[position] + val b = holder.b + + b.name.text = item.fullName + b.image.setImageBitmap(item.image?: getProfileImage(48, 24, 16, 12, 1, item.fullName)) + var role = item.getTypeText(activity) + if (item.subjects.isNotNullNorEmpty()) { + val subjects = item.subjects.map { App.db.subjectDao().getByIdNow(App.profileId, it).longName } + role = role.plus(": ").plus(subjects.joinToString()) + } + b.type.text = role + + item.fullName.let { name -> + BetterLink.attach( + b.name, + teachers = mapOf(item.id to name) + ) + } + } + + override fun getItemCount() = items.size + + class ViewHolder(val b: TeacherItemBinding) : RecyclerView.ViewHolder(b.root) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/teachers/TeachersListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/teachers/TeachersListFragment.kt new file mode 100644 index 00000000..e248f01b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/teachers/TeachersListFragment.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) Antoni Czaplicki 2021-10-15. + */ + +package pl.szczodrzynski.edziennik.ui.modules.teachers + +import android.app.Activity +import android.content.Intent +import android.os.AsyncTask +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.databinding.TeachersListFragmentBinding +import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration +import pl.szczodrzynski.edziennik.utils.Utils +import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem +import kotlin.coroutines.CoroutineContext + +class TeachersListFragment : Fragment(), CoroutineScope { + companion object { + private const val TAG = "TeachersListFragment" + } + + private lateinit var app: App + private lateinit var activity: MainActivity + private lateinit var b: TeachersListFragmentBinding + + private val job: Job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + activity = (getActivity() as MainActivity?) ?: return null + context ?: return null + app = activity.application as App + b = TeachersListFragmentBinding.inflate(inflater) + return b.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { startCoroutineTimer(100L) { + if (!isAdded) return@startCoroutineTimer + + val adapter = TeachersAdapter(activity) + + app.db.teacherDao().getAllTeachers(App.profileId).observe(this@TeachersListFragment, Observer { items -> + if (!isAdded) return@Observer + + // load & configure the adapter + adapter.items = items + if (items.isNotNullNorEmpty() && b.list.adapter == null) { + b.list.adapter = adapter + b.list.apply { + setHasFixedSize(true) + layoutManager = LinearLayoutManager(context) + addItemDecoration(SimpleDividerItemDecoration(context)) + } + } + adapter.notifyDataSetChanged() + + // show/hide relevant views + b.progressBar.isVisible = false + if (items.isNullOrEmpty()) { + b.list.isVisible = false + b.noData.isVisible = true + } else { + b.list.isVisible = true + b.noData.isVisible = false + } + }) + }} +} diff --git a/app/src/main/res/layout/messages_compose_suggestion_item.xml b/app/src/main/res/layout/teacher_item.xml similarity index 100% rename from app/src/main/res/layout/messages_compose_suggestion_item.xml rename to app/src/main/res/layout/teacher_item.xml diff --git a/app/src/main/res/layout/teachers_list_fragment.xml b/app/src/main/res/layout/teachers_list_fragment.xml new file mode 100644 index 00000000..9138b401 --- /dev/null +++ b/app/src/main/res/layout/teachers_list_fragment.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 32f33a03..6a6e2942 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -656,6 +656,7 @@ Nachricht Schreiben Sie eine Nachricht Nachrichten + Mehr Verhalten Benachrichtigungen Löschen @@ -1108,6 +1109,7 @@ Administrator / Superadministrator Lehrer Kategorie durchsuchen + Keine Lehrer. Übermorgen Vorgestern Bernstein @@ -1236,4 +1238,6 @@ (Elternteil) Anwendungsentwickler Liste der Szkolny-Entwickler + Lehrer + Addressbuch herunterladen… diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index a971ba01..e6a0294a 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -658,6 +658,7 @@ Message Compose Messages + More Behaviour Notifications Clear @@ -1110,6 +1111,7 @@ SuperAdmin Teacher Browse category + No teachers. overmorrow the day before yesterday Amber @@ -1372,4 +1374,6 @@ In order to be able to save the generated timetable, you must grant access rights to the device\'s memory.\n\nClick OK to grant permissions. (Child) (Parent) + Teachers + Syncing addressbook… diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 06d89915..fae23749 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -301,6 +301,7 @@ Dzięki niej, aplikacja Szkolny.eu może synchronizować dane z e-dziennikiem. Możesz ją zamknąć, ponieważ w tej chwili nic nie robi. Usługa synchronizacji Pobieranie szczegółów konta… + Pobieranie listy odbiorców… Pobieranie ogłoszeń szkolnych… Pobieranie frekwencji ucznia… Pobieranie kategorii obecności… @@ -708,6 +709,7 @@ Wiadomość Napisz wiadomość Wiadomości + Więcej Zachowanie Powiadomienia Usuń wszystkie @@ -716,6 +718,7 @@ Ustawienia Synchronizuj Synchronizuj wszystkie + Nauczyciele Szablon Plan lekcji Edytor planu lekcji @@ -1180,6 +1183,7 @@ Administrator / SuperAdministrator Nauczyciel Przeglądaj kategorię + Brak nauczycieli. pojutrze przedwczoraj Bursztynowy