Merge branch 'develop' into feature/structure-refactor

# Conflicts:
#	app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt
#	app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt
#	app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Teacher.kt
#	app/src/main/java/pl/szczodrzynski/edziennik/ui/teachers/TeachersAdapter.kt
#	app/src/main/java/pl/szczodrzynski/edziennik/ui/teachers/TeachersListFragment.kt
This commit is contained in:
Kuba Szczodrzyński 2021-10-19 13:30:26 +02:00
commit d8abac1917
No known key found for this signature in database
GPG Key ID: 70CB8A85BA1633CB
19 changed files with 672 additions and 262 deletions

View File

@ -0,0 +1,6 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="Copyright (c) Antoni Czaplicki &amp;#36;{today.year}-&amp;#36;{today.month}-&amp;#36;{today.day}. " />
<option name="myName" value="Antoni" />
</copyright>
</component>

View File

@ -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
@ -77,6 +75,7 @@ import pl.szczodrzynski.edziennik.ui.messages.single.MessageFragment
import pl.szczodrzynski.edziennik.ui.notifications.NotificationsListFragment
import pl.szczodrzynski.edziennik.ui.settings.ProfileManagerFragment
import pl.szczodrzynski.edziennik.ui.settings.SettingsFragment
import pl.szczodrzynski.edziennik.ui.teachers.TeachersListFragment
import pl.szczodrzynski.edziennik.ui.timetable.TimetableFragment
import pl.szczodrzynski.edziennik.ui.webpush.WebPushFragment
import pl.szczodrzynski.edziennik.utils.*
@ -117,6 +116,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
@ -131,6 +132,13 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
val navTargetList: List<NavTarget> by lazy {
val list: MutableList<NavTarget> = mutableListOf()
val moreList: MutableList<NavTarget> = 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)
@ -140,7 +148,9 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
.isStatic(true)
.withPopToHome(false)
list += NavTarget(DRAWER_ITEM_TIMETABLE, R.string.menu_timetable, TimetableFragment::class)
list += NavTarget(DRAWER_ITEM_TIMETABLE,
R.string.menu_timetable,
TimetableFragment::class)
.withIcon(CommunityMaterial.Icon3.cmd_timetable)
.withBadgeTypeId(TYPE_LESSON_CHANGE)
.isInDrawer(true)
@ -165,24 +175,38 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
.withBadgeTypeId(TYPE_HOMEWORK)
.isInDrawer(true)
list += NavTarget(DRAWER_ITEM_BEHAVIOUR, R.string.menu_notices, BehaviourFragment::class)
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)
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)
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)
list += NavTarget(DRAWER_ITEM_NOTIFICATIONS,
R.string.menu_notifications,
NotificationsListFragment::class)
.withIcon(CommunityMaterial.Icon.cmd_bell_ring_outline)
.isInDrawer(true)
.isStatic(true)
@ -201,13 +225,17 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
.withDescription(R.string.drawer_add_new_profile_desc)
.isInProfileList(true)
list += NavTarget(DRAWER_PROFILE_MANAGE, R.string.menu_manage_profiles, ProfileManagerFragment::class)
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)
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)
@ -217,10 +245,16 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
// 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)
@ -324,9 +358,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
@ -386,8 +423,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
ProfileConfigDialog(this@MainActivity, appProfile)
}
true
}
else {
} else {
false
}
}
@ -408,7 +444,9 @@ 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(
@ -470,9 +508,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),
@ -519,14 +560,17 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
app.config.appRateSnackbarTime = 0
}
.onNegative { cafeBar ->
Toast.makeText(this, R.string.rate_snackbar_negative_message, Toast.LENGTH_LONG).show()
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
app.config.appRateSnackbarTime =
System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000
}
.autoDismiss(false)
.swipeToDismiss(true)
@ -571,17 +615,22 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
DRAWER_PROFILE_SYNC_ALL -> {
EdziennikTask.sync().enqueue(this)
}
DRAWER_PROFILE_MARK_ALL_AS_READ -> { launch {
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)
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)
}
@ -669,11 +718,13 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
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)
@ -682,6 +733,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
RegisterUnavailableDialog(this, error.status!!)
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onApiTaskStartedEvent(event: ApiTaskStartedEvent) {
swipeRefreshLayout.isRefreshing = true
@ -693,6 +745,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
}
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onProfileListEmptyEvent(event: ProfileListEmptyEvent) {
d(TAG, "Profile list is empty. Launch LoginActivity.")
@ -700,6 +753,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) {
@ -709,11 +763,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)
@ -725,11 +784,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)
@ -746,6 +807,7 @@ 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)
@ -757,7 +819,9 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
.setPositiveButton(R.string.ok) { _, _ ->
try {
for (intent in appManagerIntentList) {
if (packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
if (packageManager.resolveActivity(intent,
PackageManager.MATCH_DEFAULT_ONLY) != null
) {
startActivity(intent)
}
}
@ -766,7 +830,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
startActivity(Intent(Settings.ACTION_SETTINGS))
} catch (e: Exception) {
e.printStackTrace()
Toast.makeText(this, R.string.app_manager_open_failed, Toast.LENGTH_SHORT).show()
Toast.makeText(this, R.string.app_manager_open_failed, Toast.LENGTH_SHORT)
.show()
}
}
}
@ -776,6 +841,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
.setCancelable(false)
.show()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onUserActionRequiredEvent(event: UserActionRequiredEvent) {
app.userActionManager.execute(this, event.profileId, event.type)
@ -809,11 +875,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, "}")
@ -843,7 +910,9 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
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,
@ -914,9 +983,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)
@ -933,10 +1004,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()
@ -945,12 +1018,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()
@ -1011,6 +1086,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()) {
@ -1025,6 +1101,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
loadProfile(profile, navTargetId, null)
return true
}
private fun loadProfile(
id: Int,
drawerSelection: Int,
@ -1056,6 +1133,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
}
return true
}
private fun loadProfile(profile: Profile, drawerSelection: Int, arguments: Bundle?) {
App.profile = profile
MessagesFragment.pageSelection = -1
@ -1085,6 +1163,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
drawer.currentProfile = app.profileId
loadTarget(drawerSelection, arguments, skipBeforeNavigate = true)
}
fun loadTarget(
id: Int,
arguments: Bundle? = null,
@ -1094,15 +1173,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()
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)
} else {
}
target.fragmentClass != null -> {
loadTarget(target, arguments, skipBeforeNavigate)
}
else -> {
false
}
}
}
private fun loadTarget(
target: NavTarget,
args: Bundle? = null,
@ -1121,7 +1213,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
@ -1133,7 +1227,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
@ -1145,8 +1240,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
R.anim.fade_in,
R.anim.fade_out
)
}
else {
} else {
navBackStack.keys().lastIndexOf(target).let {
if (it == -1)
return@let target
@ -1194,7 +1288,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}")
}
@ -1205,9 +1300,13 @@ 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)),
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)
)
@ -1215,6 +1314,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
}
return true
}
fun reloadTarget() = loadTarget(navTarget)
private fun popBackStack(skipBeforeNavigate: Boolean = false): Boolean {
@ -1237,6 +1337,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
}
return true
}
fun navigateUp(skipBeforeNavigate: Boolean = false) {
if (!popBackStack(skipBeforeNavigate)) {
super.onBackPressed()
@ -1288,29 +1389,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
}
@ -1335,7 +1438,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
@ -1368,7 +1475,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()
@ -1377,6 +1485,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()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,6 +26,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 +37,8 @@ open class Teacher {
const val TYPE_OTHER = 24 // 16777216
const val IS_TEACHER_MASK = 127
val types: List<Int> by lazy { listOf(
val types: List<Int> by lazy {
listOf(
TYPE_TEACHER,
TYPE_EDUCATOR,
TYPE_PEDAGOGUE,
@ -51,7 +53,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 +97,9 @@ open class Teacher {
@ColumnInfo(name = "teacherTypeDescription")
var typeDescription: String? = null
@ColumnInfo(name = "teacherSubjects")
var subjects = mutableListOf<Long>()
fun isType(checkingType: Int): Boolean {
return type and (1 shl checkingType) >= 1
}
@ -105,6 +111,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 +136,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 +151,6 @@ open class Teacher {
this.id = id
}
@Ignore
constructor(profileId: Int, id: Long, name: String, surname: String) {
this.profileId = profileId
@ -170,6 +177,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 +203,7 @@ open class Teacher {
", name='" + name + '\'' +
", surname='" + surname + '\'' +
", type=" + dumpType() +
", subjects=" + subjects.joinToString() +
", typeDescription='" + typeDescription + '\'' +
'}'
}

View File

@ -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;")
}
}

View File

@ -15,6 +15,8 @@ import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.core.content.res.ResourcesCompat
import com.mikepenz.iconics.typeface.IIcon
import pl.szczodrzynski.navlib.ImageHolder
fun colorFromName(name: String?): Int {
val i = (name ?: "").crc32()
@ -91,3 +93,5 @@ fun Drawable.setTintColor(color: Int): Drawable {
)
return this
}
fun IIcon.toImageHolder() = ImageHolder(this)

View File

@ -16,6 +16,8 @@ import android.text.style.ForegroundColorSpan
import android.text.style.StrikethroughSpan
import android.text.style.StyleSpan
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import com.mikepenz.materialdrawer.holder.StringHolder
fun CharSequence?.isNotNullNorEmpty(): Boolean {
return this != null && this.isNotEmpty()
@ -336,3 +338,8 @@ fun CharSequence.getWordBounds(position: Int, onlyInWord: Boolean = false): Pair
return null
return rangeStart to rangeEnd
}
fun Int.toStringHolder() = StringHolder(this)
fun CharSequence.toStringHolder() = StringHolder(this)
fun @receiver:StringRes Int.resolveString(context: Context) = context.getString(this)

View File

@ -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<TextView>(R.id.name)

View File

@ -0,0 +1,69 @@
/*
* Copyright (c) Antoni Czaplicki 2021-10-15.
*/
package pl.szczodrzynski.edziennik.ui.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.ext.isNotNullNorEmpty
import pl.szczodrzynski.edziennik.ui.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<TeachersAdapter.ViewHolder>(), 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<Teacher>()
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)
}

View File

@ -0,0 +1,77 @@
/*
* Copyright (c) Antoni Czaplicki 2021-10-15.
*/
package pl.szczodrzynski.edziennik.ui.teachers
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
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.ext.isNotNullNorEmpty
import pl.szczodrzynski.edziennik.ext.startCoroutineTimer
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
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
}
})
}}
}

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Antoni Czaplicki 2021-10-15.
-->
<layout 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">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/noData"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:gravity="center"
android:padding="16dp"
android:text="@string/teachers_no_data"
android:textSize="24sp"
android:visibility="gone"
app:drawableTopCompat="@drawable/ic_archive"
tools:visibility="visible" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
tools:listitem="@layout/teacher_item"
tools:visibility="visible" />
</FrameLayout>
</layout>

View File

@ -656,6 +656,7 @@
<string name="menu_message">Nachricht</string>
<string name="menu_message_compose">Schreiben Sie eine Nachricht</string>
<string name="menu_messages">Nachrichten</string>
<string name="menu_more">Mehr</string>
<string name="menu_notices">Verhalten</string>
<string name="menu_notifications">Benachrichtigungen</string>
<string name="menu_remove_notifications">Löschen</string>
@ -1108,6 +1109,7 @@
<string name="teacher_super_admin">Administrator / Superadministrator</string>
<string name="teacher_teacher">Lehrer</string>
<string name="teachers_browse_category">Kategorie durchsuchen</string>
<string name="teachers_no_data">Keine Lehrer.</string>
<string name="the_day_after">Übermorgen</string>
<string name="the_day_before">Vorgestern</string>
<string name="theme_amber">Bernstein</string>
@ -1236,4 +1238,6 @@
<string name="login_summary_account_parent">(Elternteil)</string>
<string name="settings_about_contributors_text">Anwendungsentwickler</string>
<string name="settings_about_contributors_subtext">Liste der Szkolny-Entwickler</string>
<string name="menu_teachers">Lehrer</string>
<string name="edziennik_progress_endpoint_addressbook">Addressbuch herunterladen…</string>
</resources>

View File

@ -658,6 +658,7 @@
<string name="menu_message">Message</string>
<string name="menu_message_compose">Compose</string>
<string name="menu_messages">Messages</string>
<string name="menu_more">More</string>
<string name="menu_notices">Behaviour</string>
<string name="menu_notifications">Notifications</string>
<string name="menu_remove_notifications">Clear</string>
@ -1110,6 +1111,7 @@
<string name="teacher_super_admin">SuperAdmin</string>
<string name="teacher_teacher">Teacher</string>
<string name="teachers_browse_category">Browse category</string>
<string name="teachers_no_data">No teachers.</string>
<string name="the_day_after">overmorrow</string>
<string name="the_day_before">the day before yesterday</string>
<string name="theme_amber">Amber</string>
@ -1372,4 +1374,6 @@
<string name="permissions_generate_timetable">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.</string>
<string name="login_summary_account_child">(Child)</string>
<string name="login_summary_account_parent">(Parent)</string>
<string name="menu_teachers">Teachers</string>
<string name="edziennik_progress_endpoint_addressbook">Syncing addressbook…</string>
</resources>

View File

@ -301,6 +301,7 @@
<string name="edziennik_notification_api_text">Dzięki niej, aplikacja Szkolny.eu może synchronizować dane z e-dziennikiem. Możesz ją zamknąć, ponieważ w tej chwili nic nie robi.</string>
<string name="edziennik_notification_api_title">Usługa synchronizacji</string>
<string name="edziennik_progress_endpoint_account_details">Pobieranie szczegółów konta…</string>
<string name="edziennik_progress_endpoint_addressbook">Pobieranie listy odbiorców…</string>
<string name="edziennik_progress_endpoint_announcements">Pobieranie ogłoszeń szkolnych…</string>
<string name="edziennik_progress_endpoint_attendance">Pobieranie frekwencji ucznia…</string>
<string name="edziennik_progress_endpoint_attendance_types">Pobieranie kategorii obecności…</string>
@ -708,6 +709,7 @@
<string name="menu_message">Wiadomość</string>
<string name="menu_message_compose">Napisz wiadomość</string>
<string name="menu_messages">Wiadomości</string>
<string name="menu_more">Więcej</string>
<string name="menu_notices">Zachowanie</string>
<string name="menu_notifications">Powiadomienia</string>
<string name="menu_remove_notifications">Usuń wszystkie</string>
@ -716,6 +718,7 @@
<string name="menu_settings">Ustawienia</string>
<string name="menu_sync">Synchronizuj</string>
<string name="menu_sync_all">Synchronizuj wszystkie</string>
<string name="menu_teachers">Nauczyciele</string>
<string name="menu_template">Szablon</string>
<string name="menu_timetable">Plan lekcji</string>
<string name="menu_timetable_manual">Edytor planu lekcji</string>
@ -1180,6 +1183,7 @@
<string name="teacher_super_admin">Administrator / SuperAdministrator</string>
<string name="teacher_teacher">Nauczyciel</string>
<string name="teachers_browse_category">Przeglądaj kategorię</string>
<string name="teachers_no_data">Brak nauczycieli.</string>
<string name="the_day_after">pojutrze</string>
<string name="the_day_before">przedwczoraj</string>
<string name="theme_amber">Bursztynowy</string>