This commit is contained in:
2024-12-20 23:44:19 +04:00
parent f4d3759d47
commit 44c1f01541
16 changed files with 323 additions and 44 deletions

1
.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
PolytecnicNext

3
.idea/dictionaries/n08i40k.xml generated Normal file
View File

@@ -0,0 +1,3 @@
<component name="ProjectDictionaryState">
<dictionary name="n08i40k" />
</component>

View File

@@ -33,8 +33,8 @@ android {
applicationId = "ru.n08i40k.polytechnic.next"
minSdk = 26
targetSdk = 35
versionCode = 22
versionName = "2.2.1"
versionCode = 23
versionName = "2.3.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

View File

@@ -29,6 +29,7 @@ import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.model.UserRole
import ru.n08i40k.polytechnic.next.ui.widgets.GroupSelector
import ru.n08i40k.polytechnic.next.ui.widgets.RoleSelector
import ru.n08i40k.polytechnic.next.ui.widgets.TeacherNameSelector
@Preview(showBackground = true)
@@ -105,17 +106,26 @@ internal fun RegisterForm(
Spacer(modifier = Modifier.size(10.dp))
OutlinedTextField(
value = username,
singleLine = true,
onValueChange = {
username = it
usernameError = false
},
label = { Text(stringResource(R.string.username)) },
isError = usernameError,
readOnly = loading
)
if (role != UserRole.TEACHER) {
OutlinedTextField(
value = username,
singleLine = true,
onValueChange = {
username = it
usernameError = false
},
label = { Text(stringResource(R.string.username)) },
isError = usernameError,
readOnly = loading
)
} else {
TeacherNameSelector(
value = username,
isError = usernameError,
readOnly = loading,
onValueChange = { username = it ?: "" }
)
}
OutlinedTextField(
value = password,
@@ -135,7 +145,8 @@ internal fun RegisterForm(
GroupSelector(
value = group,
isError = groupError,
readOnly = loading
readOnly = loading,
teacher = role == UserRole.TEACHER
) {
groupError = false
group = it

View File

@@ -8,19 +8,31 @@ import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material.icons.filled.Person
import androidx.compose.ui.graphics.vector.ImageVector
import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.model.UserRole
data class BottomNavItem(
@StringRes val label: Int,
val icon: ImageVector,
val route: String,
val isAdmin: Boolean = false
val requiredRole: UserRole? = null
)
object Constants {
val bottomNavItem = listOf(
BottomNavItem(R.string.profile, Icons.Filled.AccountCircle, "profile"),
BottomNavItem(R.string.replacer, Icons.Filled.Create, "replacer", true),
BottomNavItem(R.string.replacer, Icons.Filled.Create, "replacer", UserRole.ADMIN),
BottomNavItem(
R.string.teacher_schedule,
Icons.Filled.Person,
"teacher-main-schedule",
UserRole.TEACHER
),
BottomNavItem(R.string.schedule, Icons.Filled.DateRange, "schedule"),
BottomNavItem(R.string.teachers, Icons.Filled.Person, "teacher-schedule")
BottomNavItem(
R.string.teachers,
Icons.Filled.Person,
"teacher-user-schedule",
UserRole.STUDENT
)
)
}

View File

@@ -61,6 +61,7 @@ import kotlinx.coroutines.runBlocking
import ru.n08i40k.polytechnic.next.MainViewModel
import ru.n08i40k.polytechnic.next.PolytechnicApplication
import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.model.Profile
import ru.n08i40k.polytechnic.next.model.UserRole
import ru.n08i40k.polytechnic.next.settings.settingsDataStore
import ru.n08i40k.polytechnic.next.ui.icons.AppIcons
@@ -70,7 +71,8 @@ import ru.n08i40k.polytechnic.next.ui.icons.appicons.filled.Telegram
import ru.n08i40k.polytechnic.next.ui.main.profile.ProfileScreen
import ru.n08i40k.polytechnic.next.ui.main.replacer.ReplacerScreen
import ru.n08i40k.polytechnic.next.ui.main.schedule.group.GroupScheduleScreen
import ru.n08i40k.polytechnic.next.ui.main.schedule.teacher.TeacherScheduleScreen
import ru.n08i40k.polytechnic.next.ui.main.schedule.teacher.main.TeacherMainScheduleScreen
import ru.n08i40k.polytechnic.next.ui.main.schedule.teacher.user.TeacherUserScheduleScreen
import ru.n08i40k.polytechnic.next.ui.model.GroupScheduleViewModel
import ru.n08i40k.polytechnic.next.ui.model.ProfileUiState
import ru.n08i40k.polytechnic.next.ui.model.ProfileViewModel
@@ -84,15 +86,27 @@ import ru.n08i40k.polytechnic.next.ui.model.profileViewModel
private fun NavHostContainer(
navController: NavHostController,
padding: PaddingValues,
profileViewModel: ProfileViewModel,
groupScheduleViewModel: GroupScheduleViewModel,
teacherScheduleViewModel: TeacherScheduleViewModel,
scheduleReplacerViewModel: ScheduleReplacerViewModel?
) {
val context = LocalContext.current
val profileUiState by profileViewModel.uiState.collectAsStateWithLifecycle()
val profile: Profile? = when (profileUiState) {
is ProfileUiState.NoProfile -> null
is ProfileUiState.HasProfile ->
(profileUiState as ProfileUiState.HasProfile).profile
}
if (profile == null)
return
NavHost(
navController = navController,
startDestination = "schedule",
startDestination = if (profile.role == UserRole.TEACHER) "teacher-main-schedule" else "schedule",
modifier = Modifier.padding(paddingValues = padding),
enterTransition = {
slideIn(
@@ -126,8 +140,16 @@ private fun NavHostContainer(
GroupScheduleScreen(groupScheduleViewModel) { groupScheduleViewModel.refresh() }
}
composable("teacher-schedule") {
TeacherScheduleScreen(teacherScheduleViewModel) {
composable("teacher-user-schedule") {
TeacherUserScheduleScreen(teacherScheduleViewModel) {
if (it.isNotEmpty()) teacherScheduleViewModel.fetch(
it
)
}
}
composable("teacher-main-schedule") {
TeacherMainScheduleScreen(teacherScheduleViewModel) {
if (it.isNotEmpty()) teacherScheduleViewModel.fetch(
it
)
@@ -231,14 +253,14 @@ private fun TopNavBar(
}
@Composable
private fun BottomNavBar(navController: NavHostController, isAdmin: Boolean) {
private fun BottomNavBar(navController: NavHostController, userRole: UserRole) {
NavigationBar {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
Constants.bottomNavItem.forEach {
if (it.isAdmin && !isAdmin)
if (it.requiredRole != null && it.requiredRole != userRole && userRole != UserRole.ADMIN)
return@forEach
NavigationBarItem(
@@ -303,17 +325,17 @@ fun MainScreen(
// schedule replacer view model
val profileUiState by profileViewModel.uiState.collectAsStateWithLifecycle()
val isAdmin = when (profileUiState) {
is ProfileUiState.NoProfile -> false
is ProfileUiState.HasProfile -> {
val profile = (profileUiState as ProfileUiState.HasProfile).profile
profile.role == UserRole.ADMIN
}
val profile: Profile? = when (profileUiState) {
is ProfileUiState.NoProfile -> null
is ProfileUiState.HasProfile ->
(profileUiState as ProfileUiState.HasProfile).profile
}
if (profile == null)
return
val scheduleReplacerViewModel: ScheduleReplacerViewModel? =
if (isAdmin) hiltViewModel(LocalContext.current as ComponentActivity)
if (profile.role == UserRole.ADMIN) hiltViewModel(LocalContext.current as ComponentActivity)
else null
// nav controller
@@ -321,11 +343,12 @@ fun MainScreen(
val navController = rememberNavController()
Scaffold(
topBar = { TopNavBar(remoteConfigViewModel) },
bottomBar = { BottomNavBar(navController, isAdmin) }
bottomBar = { BottomNavBar(navController, profile.role) }
) { paddingValues ->
NavHostContainer(
navController,
paddingValues,
profileViewModel,
groupScheduleViewModel,
teacherScheduleViewModel,
scheduleReplacerViewModel

View File

@@ -23,6 +23,7 @@ import com.android.volley.ClientError
import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.users.impl.FakeProfileRepository
import ru.n08i40k.polytechnic.next.model.Profile
import ru.n08i40k.polytechnic.next.model.UserRole
import ru.n08i40k.polytechnic.next.network.request.profile.ProfileChangeGroup
import ru.n08i40k.polytechnic.next.ui.widgets.GroupSelector
@@ -65,10 +66,10 @@ internal fun ChangeGroupDialog(
GroupSelector(
value = group,
onValueChange = { group = it },
isError = groupError,
readOnly = processing
)
readOnly = processing,
teacher = profile.role == UserRole.TEACHER
) { group = it }
val focusManager = LocalFocusManager.current
Button(

View File

@@ -61,7 +61,9 @@ fun DayPager(groupOrTeacher: GroupOrTeacher = FakeScheduleRepository.exampleGrou
) { page ->
DayCard(
modifier = Modifier.graphicsLayer {
val offset = pagerState.getOffsetDistanceInPages(page).absoluteValue
val offset = pagerState.getOffsetDistanceInPages(
page.coerceIn(0, pagerState.pageCount - 1)
).absoluteValue
lerp(
start = 1f, stop = 0.95f, fraction = 1f - offset.coerceIn(0f, 1f)

View File

@@ -0,0 +1,112 @@
package ru.n08i40k.polytechnic.next.ui.main.schedule.teacher.main
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.delay
import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.data.MockAppContainer
import ru.n08i40k.polytechnic.next.ui.main.schedule.DayPager
import ru.n08i40k.polytechnic.next.ui.model.ProfileUiState
import ru.n08i40k.polytechnic.next.ui.model.TeacherScheduleUiState
import ru.n08i40k.polytechnic.next.ui.model.TeacherScheduleViewModel
import ru.n08i40k.polytechnic.next.ui.model.profileViewModel
import ru.n08i40k.polytechnic.next.ui.widgets.LoadingContent
@Composable
private fun rememberUpdatedLifecycleOwner(): LifecycleOwner {
val lifecycleOwner = LocalLifecycleOwner.current
return remember { lifecycleOwner }
}
@Preview(showBackground = true, showSystemUi = true)
@Composable
fun TeacherMainScheduleScreen(
teacherScheduleViewModel: TeacherScheduleViewModel = TeacherScheduleViewModel(
MockAppContainer(
LocalContext.current
)
),
fetch: (String) -> Unit = {}
) {
val profileViewModel = LocalContext.current.profileViewModel!!
val profileUiState by profileViewModel.uiState.collectAsStateWithLifecycle()
if (profileUiState is ProfileUiState.NoProfile)
return
val profile = (profileUiState as ProfileUiState.HasProfile).profile
var teacherName = profile.username
val uiState by teacherScheduleViewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(uiState) {
delay(120_000)
fetch(teacherName)
}
val lifecycleOwner = rememberUpdatedLifecycleOwner()
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> {
fetch(teacherName)
}
else -> Unit
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
Column(Modifier.fillMaxSize()) {
LoadingContent(
empty = when (uiState) {
is TeacherScheduleUiState.NoData -> uiState.isLoading
is TeacherScheduleUiState.HasData -> false
},
loading = uiState.isLoading,
) {
when (uiState) {
is TeacherScheduleUiState.HasData -> {
Column {
val hasData = uiState as TeacherScheduleUiState.HasData
DayPager(hasData.teacher)
}
}
is TeacherScheduleUiState.NoData -> {
if (!uiState.isLoading) {
Text(
modifier = Modifier.fillMaxSize(),
text = stringResource(R.string.teacher_not_selected),
textAlign = TextAlign.Center
)
}
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
package ru.n08i40k.polytechnic.next.ui.main.schedule.teacher
package ru.n08i40k.polytechnic.next.ui.main.schedule.teacher.user
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth

View File

@@ -1,4 +1,4 @@
package ru.n08i40k.polytechnic.next.ui.main.schedule.teacher
package ru.n08i40k.polytechnic.next.ui.main.schedule.teacher.user
import android.content.Context
import androidx.compose.runtime.Composable

View File

@@ -1,4 +1,4 @@
package ru.n08i40k.polytechnic.next.ui.main.schedule.teacher
package ru.n08i40k.polytechnic.next.ui.main.schedule.teacher.user
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -39,7 +39,7 @@ private fun rememberUpdatedLifecycleOwner(): LifecycleOwner {
@Preview(showBackground = true, showSystemUi = true)
@Composable
fun TeacherScheduleScreen(
fun TeacherUserScheduleScreen(
teacherScheduleViewModel: TeacherScheduleViewModel = TeacherScheduleViewModel(
MockAppContainer(
LocalContext.current

View File

@@ -27,7 +27,7 @@ import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGetGroupNames
@Composable
private fun getGroups(context: Context, onUpdated: (String?) -> Unit): ArrayList<String?> {
private fun getTeacherNames(context: Context, onUpdated: (String?) -> Unit): ArrayList<String?> {
val groupPlaceholder = stringResource(R.string.loading)
val groups = remember { arrayListOf(null, groupPlaceholder) }
@@ -55,6 +55,7 @@ fun GroupSelector(
value: String? = "ИС-214/24",
isError: Boolean = false,
readOnly: Boolean = false,
teacher: Boolean = false,
onValueChange: (String?) -> Unit = {},
) {
var expanded by remember { mutableStateOf(false) }
@@ -68,10 +69,10 @@ fun GroupSelector(
expanded = !readOnly && !expanded
}
) {
val groups = getGroups(LocalContext.current, onValueChange)
val groups = getTeacherNames(LocalContext.current, onValueChange)
TextField(
label = { Text(stringResource(R.string.group)) },
label = { Text(stringResource(if (teacher) R.string.supervised_group else R.string.group)) },
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryEditable),
value = value ?: groups.getOrElse(1) { "TODO" }!!,
leadingIcon = {
@@ -97,7 +98,7 @@ fun GroupSelector(
DropdownMenuItem(
text = { Text(it) },
onClick = {
if (groups.size > 0 && groups[0] != null)
if (groups.isNotEmpty() && groups[0] != null)
onValueChange(it)
expanded = false
}

View File

@@ -0,0 +1,109 @@
package ru.n08i40k.polytechnic.next.ui.widgets
import android.content.Context
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import ru.n08i40k.polytechnic.next.R
import ru.n08i40k.polytechnic.next.network.request.schedule.ScheduleGetTeacherNames
@Composable
private fun getTeacherNames(context: Context, onUpdated: (String?) -> Unit): ArrayList<String?> {
val groupPlaceholder = stringResource(R.string.loading)
val names = remember { arrayListOf(null, groupPlaceholder) }
LaunchedEffect(names) {
ScheduleGetTeacherNames(context, {
names.clear()
names.addAll(it.names)
onUpdated(names.getOrElse(0) { "TODO" }!!)
}, {
names.clear()
names.add(null)
names.add(context.getString(R.string.failed_to_fetch_teacher_names))
onUpdated(names[1]!!)
}).send()
}
return names
}
@OptIn(ExperimentalMaterial3Api::class)
@Preview(showBackground = true)
@Composable
fun TeacherNameSelector(
value: String? = "Фамилия И.О.",
isError: Boolean = false,
readOnly: Boolean = false,
onValueChange: (String?) -> Unit = {},
) {
var expanded by remember { mutableStateOf(false) }
Box(
modifier = Modifier.wrapContentSize()
) {
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = !readOnly && !expanded
}
) {
val names = getTeacherNames(LocalContext.current, onValueChange)
TextField(
label = { Text(stringResource(R.string.username)) },
modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryEditable),
value = value ?: names.getOrElse(1) { "TODO" }!!,
leadingIcon = {
Icon(
Icons.Filled.Person,
contentDescription = "username"
)
},
onValueChange = {},
isError = isError,
readOnly = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
names.forEach {
if (it == null)
return@forEach
DropdownMenuItem(
text = { Text(it) },
onClick = {
if (names.isNotEmpty() && names[0] != null)
onValueChange(it)
expanded = false
}
)
}
}
}
}
}

View File

@@ -85,4 +85,6 @@
<string name="lesson_type_exam">ЗАЧЁТ</string>
<string name="lesson_type_exam_with_grade">ЗАЧЁТ С ОЦЕНКОЙ</string>
<string name="lesson_type_exam_default">ЭКЗАМЕН</string>
<string name="supervised_group">Курируемая группа</string>
<string name="teacher_schedule">Преподаватель</string>
</resources>

View File

@@ -85,4 +85,6 @@
<string name="lesson_type_exam">EXAM*</string>
<string name="lesson_type_exam_with_grade">EXAM* WITH GRADE</string>
<string name="lesson_type_exam_default">EXAM</string>
<string name="supervised_group">Supervised group</string>
<string name="teacher_schedule">Teacher</string>
</resources>