From 70d701825db823f688087619ab2628dfd1080d1a Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Thu, 4 Dec 2025 12:51:03 +0530 Subject: [PATCH 01/58] created the playlist screen. --- .../view/playlists/list/PlaylistsScreen.kt | 321 ++++++++++++++++++ .../composeResources/values/strings.xml | 11 + 2 files changed, 332 insertions(+) create mode 100644 respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/list/PlaylistsScreen.kt diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/list/PlaylistsScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/list/PlaylistsScreen.kt new file mode 100644 index 000000000..e1e86e8e9 --- /dev/null +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/list/PlaylistsScreen.kt @@ -0,0 +1,321 @@ +package world.respect.app.view.playlists.enrollment.list + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.jetbrains.compose.resources.stringResource +import world.respect.shared.generated.resources.Res +import world.respect.shared.generated.resources.apps +import world.respect.shared.generated.resources.playlists +import world.respect.shared.generated.resources.all +import world.respect.shared.generated.resources.school_playlists +import world.respect.shared.generated.resources.my_playlists +import world.respect.shared.generated.resources.add_playlist +import world.respect.shared.generated.resources.add_new +import world.respect.shared.generated.resources.add_from_link +import world.respect.shared.generated.resources.sections +import world.respect.shared.generated.resources.items +import world.respect.shared.generated.resources.created_by +import world.respect.shared.viewmodel.playlists.PlaylistsUiState +import world.respect.shared.viewmodel.playlists.list.PlaylistsViewModel +import world.respect.shared.viewmodel.playlists.PlaylistTab + +@Composable +fun PlaylistsScreenForViewModel( + viewModel: PlaylistsViewModel, + onPlaylistClick: (String) -> Unit, + onCreatePlaylist: () -> Unit, + onAddFromLink: () -> Unit +) { + val uiState by viewModel.uiState.collectAsState() + + PlaylistsScreen( + uiState = uiState, + onTabSelected = viewModel::onTabSelected, + onPlaylistClick = onPlaylistClick, + onCreatePlaylist = onCreatePlaylist, + onAddFromLink = onAddFromLink + ) +} + +@Composable +fun PlaylistsScreen( + uiState: PlaylistsUiState = PlaylistsUiState(), + onTabSelected: (PlaylistTab) -> Unit = {}, + onPlaylistClick: (String) -> Unit = {}, + onCreatePlaylist: () -> Unit = {}, + onAddFromLink: () -> Unit = {} +) { + var showOptions by remember { mutableStateOf(false) } + + Scaffold( + topBar = { + PlaylistsTopBar( + selectedTab = uiState.selectedTab, + onTabSelected = onTabSelected + ) + }, + floatingActionButton = { + FloatingActionButtonWithOptions( + showOptions = showOptions, + onToggleOptions = { showOptions = !showOptions }, + onCreatePlaylist = onCreatePlaylist, + onAddFromLink = onAddFromLink + ) + } + ) { padding -> + Box(modifier = Modifier.fillMaxSize()) { + when { + uiState.isLoading -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) + } + } + uiState.error != null -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = uiState.error ?: stringResource(Res.string.unknown_error), + color = MaterialTheme.colorScheme.error + ) + } + } + else -> { + PlaylistsList( + playlists = uiState.playlists, + onPlaylistClick = onPlaylistClick, + modifier = Modifier.padding(padding) + ) + } + } + } + } +} + +@Composable +private fun PlaylistsTopBar( + selectedTab: PlaylistTab, + onTabSelected: (PlaylistTab) -> Unit +) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Text( + text = stringResource(Res.string.apps), + color = MaterialTheme.colorScheme.onSurface, + fontSize = 16.sp + ) + Text( + text = stringResource(Res.string.playlists), + color = MaterialTheme.colorScheme.onSurface, + fontSize = 16.sp, + fontWeight = FontWeight.Bold + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 12.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + FilterChip( + selected = selectedTab == PlaylistTab.ALL, + onClick = { onTabSelected(PlaylistTab.ALL) }, + label = { Text(stringResource(Res.string.all)) } + ) + FilterChip( + selected = selectedTab == PlaylistTab.SCHOOL, + onClick = { onTabSelected(PlaylistTab.SCHOOL) }, + label = { Text(stringResource(Res.string.school_playlists)) } + ) + FilterChip( + selected = selectedTab == PlaylistTab.MY_PLAYLISTS, + onClick = { onTabSelected(PlaylistTab.MY_PLAYLISTS) }, + label = { Text(stringResource(Res.string.my_playlists)) } + ) + } + } +} + +@Composable +private fun PlaylistsList( + playlists: List, + onPlaylistClick: (String) -> Unit, + modifier: Modifier = Modifier +) { + LazyColumn( + modifier = modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) + ) { + items( + items = playlists, + key = { it.id } + ) { playlist -> + PlaylistItem( + playlist = playlist, + onClick = { onPlaylistClick(playlist.id) } + ) + } + } +} + +@Composable +private fun PlaylistItem( + playlist: world.respect.shared.viewmodel.playlists.PlaylistUiModel, + onClick: () -> Unit +) { + Card( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Surface( + modifier = Modifier.size(56.dp), + shape = RoundedCornerShape(28.dp), + color = MaterialTheme.colorScheme.surfaceVariant + ) { + Box( + contentAlignment = Alignment.Center + ) { + Text( + text = playlist.initials, + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + Spacer(modifier = Modifier.width(12.dp)) + + Column { + Text( + text = playlist.title, + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = stringResource(Res.string.sections_items_count, playlist.sectionCount, playlist.itemCount), + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = stringResource(Res.string.created_by_format, playlist.createdBy), + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } +} + +@Composable +private fun FloatingActionButtonWithOptions( + showOptions: Boolean, + onToggleOptions: () -> Unit, + onCreatePlaylist: () -> Unit, + onAddFromLink: () -> Unit +) { + Column( + horizontalAlignment = Alignment.End, + modifier = Modifier.padding(bottom = 80.dp) + ) { + if (showOptions) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(bottom = 16.dp, end = 16.dp) + ) { + AddOptionButton( + text = stringResource(Res.string.add_new), + onClick = onCreatePlaylist + ) + + AddOptionButton( + text = stringResource(Res.string.add_from_link), + onClick = onAddFromLink + ) + } + } + + FloatingActionButton( + onClick = onToggleOptions, + containerColor = MaterialTheme.colorScheme.primary + ) { + Row( + modifier = Modifier.padding(horizontal = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = stringResource(Res.string.add_playlist) + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = stringResource(Res.string.playlist), + fontSize = 14.sp + ) + } + } + } +} + +@Composable +private fun AddOptionButton( + text: String, + onClick: () -> Unit +) { + Surface( + shape = RoundedCornerShape(24.dp), + color = MaterialTheme.colorScheme.surfaceVariant, + tonalElevation = 4.dp, + modifier = Modifier.clickable(onClick = onClick) + ) { + Row( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + Icons.Filled.Add, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Text( + text = text, + fontSize = 14.sp + ) + } + } +} \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml index 7eea66cfa..b926f571d 100644 --- a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml +++ b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml @@ -461,5 +461,16 @@ Edit enrollment Section name + Playlists + School Playlists + My Playlists + No playlists yet + Create your first playlist to get started + Create Playlist + Error loading playlists + Add new + Add playlist + All sections, items + Created by: From be21a08c4f6f95527ce0a05735eb6903d649127a Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Thu, 4 Dec 2025 14:25:13 +0530 Subject: [PATCH 02/58] created the playlist screen. --- .../view/playlists/list/PlaylistsScreen.kt | 71 ++++++++++++------- 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/list/PlaylistsScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/list/PlaylistsScreen.kt index e1e86e8e9..032d997d4 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/list/PlaylistsScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/list/PlaylistsScreen.kt @@ -24,12 +24,14 @@ import world.respect.shared.generated.resources.my_playlists import world.respect.shared.generated.resources.add_playlist import world.respect.shared.generated.resources.add_new import world.respect.shared.generated.resources.add_from_link -import world.respect.shared.generated.resources.sections -import world.respect.shared.generated.resources.items +import world.respect.shared.generated.resources.all_sections_items import world.respect.shared.generated.resources.created_by -import world.respect.shared.viewmodel.playlists.PlaylistsUiState +import world.respect.shared.generated.resources.unknown_error +import world.respect.shared.viewmodel.playlists.list.MainTab import world.respect.shared.viewmodel.playlists.list.PlaylistsViewModel -import world.respect.shared.viewmodel.playlists.PlaylistTab +import world.respect.shared.viewmodel.playlists.list.PlaylistTab +import world.respect.shared.viewmodel.playlists.list.PlaylistUiModel +import world.respect.shared.viewmodel.playlists.list.PlaylistsUiState @Composable fun PlaylistsScreenForViewModel( @@ -42,6 +44,7 @@ fun PlaylistsScreenForViewModel( PlaylistsScreen( uiState = uiState, + onMainTabSelected = viewModel::onMainTabSelected, onTabSelected = viewModel::onTabSelected, onPlaylistClick = onPlaylistClick, onCreatePlaylist = onCreatePlaylist, @@ -52,6 +55,7 @@ fun PlaylistsScreenForViewModel( @Composable fun PlaylistsScreen( uiState: PlaylistsUiState = PlaylistsUiState(), + onMainTabSelected: (MainTab) -> Unit = {}, onTabSelected: (PlaylistTab) -> Unit = {}, onPlaylistClick: (String) -> Unit = {}, onCreatePlaylist: () -> Unit = {}, @@ -62,7 +66,9 @@ fun PlaylistsScreen( Scaffold( topBar = { PlaylistsTopBar( + selectedMainTab = uiState.selectedMainTab, selectedTab = uiState.selectedTab, + onMainTabSelected = onMainTabSelected, onTabSelected = onTabSelected ) }, @@ -110,7 +116,9 @@ fun PlaylistsScreen( @Composable private fun PlaylistsTopBar( + selectedMainTab: MainTab, selectedTab: PlaylistTab, + onMainTabSelected: (MainTab) -> Unit, onTabSelected: (PlaylistTab) -> Unit ) { Column( @@ -125,13 +133,16 @@ private fun PlaylistsTopBar( Text( text = stringResource(Res.string.apps), color = MaterialTheme.colorScheme.onSurface, - fontSize = 16.sp + fontSize = 16.sp, + fontWeight = if (selectedMainTab == MainTab.APPS) FontWeight.Bold else FontWeight.Normal, + modifier = Modifier.clickable { onMainTabSelected(MainTab.APPS) } ) Text( text = stringResource(Res.string.playlists), color = MaterialTheme.colorScheme.onSurface, fontSize = 16.sp, - fontWeight = FontWeight.Bold + fontWeight = if (selectedMainTab == MainTab.PLAYLISTS) FontWeight.Bold else FontWeight.Normal, + modifier = Modifier.clickable { onMainTabSelected(MainTab.PLAYLISTS) } ) } @@ -162,7 +173,7 @@ private fun PlaylistsTopBar( @Composable private fun PlaylistsList( - playlists: List, + playlists: List, onPlaylistClick: (String) -> Unit, modifier: Modifier = Modifier ) { @@ -185,7 +196,7 @@ private fun PlaylistsList( @Composable private fun PlaylistItem( - playlist: world.respect.shared.viewmodel.playlists.PlaylistUiModel, + playlist: PlaylistUiModel, onClick: () -> Unit ) { Card( @@ -226,13 +237,13 @@ private fun PlaylistItem( ) Spacer(modifier = Modifier.height(2.dp)) Text( - text = stringResource(Res.string.sections_items_count, playlist.sectionCount, playlist.itemCount), + text = stringResource(Res.string.all_sections_items, playlist.sectionCount, playlist.itemCount), fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier = Modifier.height(2.dp)) Text( - text = stringResource(Res.string.created_by_format, playlist.createdBy), + text = stringResource(Res.string.created_by, playlist.createdBy), fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -255,16 +266,23 @@ private fun FloatingActionButtonWithOptions( if (showOptions) { Column( verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.padding(bottom = 16.dp, end = 16.dp) + horizontalAlignment = Alignment.End, + modifier = Modifier.padding(bottom = 12.dp) ) { AddOptionButton( text = stringResource(Res.string.add_new), - onClick = onCreatePlaylist + onClick = { + onCreatePlaylist() + onToggleOptions() + } ) AddOptionButton( text = stringResource(Res.string.add_from_link), - onClick = onAddFromLink + onClick = { + onAddFromLink() + onToggleOptions() + } ) } } @@ -274,17 +292,19 @@ private fun FloatingActionButtonWithOptions( containerColor = MaterialTheme.colorScheme.primary ) { Row( - modifier = Modifier.padding(horizontal = 8.dp), - verticalAlignment = Alignment.CenterVertically + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Icon( imageVector = Icons.Filled.Add, - contentDescription = stringResource(Res.string.add_playlist) + contentDescription = stringResource(Res.string.add_playlist), + modifier = Modifier.size(20.dp) ) - Spacer(modifier = Modifier.width(4.dp)) Text( - text = stringResource(Res.string.playlist), - fontSize = 14.sp + text = stringResource(Res.string.playlists), + fontSize = 14.sp, + fontWeight = FontWeight.Medium ) } } @@ -299,22 +319,25 @@ private fun AddOptionButton( Surface( shape = RoundedCornerShape(24.dp), color = MaterialTheme.colorScheme.surfaceVariant, - tonalElevation = 4.dp, + tonalElevation = 2.dp, modifier = Modifier.clickable(onClick = onClick) ) { Row( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + modifier = Modifier.padding(horizontal = 20.dp, vertical = 14.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(12.dp) ) { Icon( Icons.Filled.Add, contentDescription = null, - modifier = Modifier.size(20.dp) + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant ) Text( text = text, - fontSize = 14.sp + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } } From 049835622a8fbe6c1087ac9b11f54d618a2fd330 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Fri, 5 Dec 2025 09:24:14 +0530 Subject: [PATCH 03/58] Enable reordering for both sections and items. --- .../edit/CurriculumMappingEditScreen.kt | 218 ++++++++++++------ .../edit/CurriculumMappingEditViewModel.kt | 52 +++++ 2 files changed, 197 insertions(+), 73 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt index ed08be68a..551b35bd0 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt @@ -67,6 +67,7 @@ fun CurriculumMappingEditScreenForViewModel( onSectionMoved = viewModel::onSectionMoved, onClickAddLesson = viewModel::onClickAddLesson, onClickRemoveLesson = viewModel::onClickRemoveLesson, + onLessonMovedBetweenSections = viewModel::onLessonMovedBetweenSections, ) } @@ -82,6 +83,7 @@ fun CurriculumMappingEditScreen( onSectionMoved: (Int, Int) -> Unit = { _, _ -> }, onClickAddLesson: (Int) -> Unit = {}, onClickRemoveLesson: (Int, Int) -> Unit = { _, _ -> }, + onLessonMovedBetweenSections: (Int, Int, Int, Int) -> Unit = { _, _, _, _ -> }, ) { val haptic = LocalHapticFeedback.current val lazyListState = rememberLazyListState() @@ -92,11 +94,54 @@ fun CurriculumMappingEditScreen( val fromIndex = from.index - headerItemCount val toIndex = to.index - headerItemCount - if (fromIndex >= 0 && toIndex >= 0 && - fromIndex < uiState.sections.size && - toIndex < uiState.sections.size) { - onSectionMoved(fromIndex, toIndex) - haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) + if (fromIndex >= 0 && toIndex >= 0) { + var currentItemCount = 0 + var fromSectionIndex = -1 + var fromLessonIndex = -1 + var toSectionIndex = -1 + var toLessonIndex = -1 + + for (sectionIndex in uiState.sections.indices) { + val section = uiState.sections[sectionIndex] + val sectionHeaderIndex = currentItemCount + val lessonStartIndex = currentItemCount + 1 + val lessonEndIndex = lessonStartIndex + section.items.size + + if (fromIndex == sectionHeaderIndex) { + fromSectionIndex = sectionIndex + fromLessonIndex = -1 + } else if (fromIndex in lessonStartIndex until lessonEndIndex) { + fromSectionIndex = sectionIndex + fromLessonIndex = fromIndex - lessonStartIndex + } + + if (toIndex == sectionHeaderIndex) { + toSectionIndex = sectionIndex + toLessonIndex = -1 + } else if (toIndex in lessonStartIndex until lessonEndIndex) { + toSectionIndex = sectionIndex + toLessonIndex = toIndex - lessonStartIndex + } + + currentItemCount = lessonEndIndex + } + + when { + fromLessonIndex >= 0 && toLessonIndex >= 0 -> { + onLessonMovedBetweenSections( + fromSectionIndex, + fromLessonIndex, + toSectionIndex, + toLessonIndex + ) + haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) + } + fromLessonIndex == -1 && toLessonIndex == -1 && + fromSectionIndex >= 0 && toSectionIndex >= 0 -> { + onSectionMoved(fromSectionIndex, toSectionIndex) + haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) + } + } } } ) @@ -184,32 +229,55 @@ fun CurriculumMappingEditScreen( } } } else { - itemsIndexed( - items = uiState.sections, - key = { _, section -> section.uid } - ) { sectionIndex, section -> - ReorderableItem( - state = reorderableLazyListState, - key = section.uid - ) { isDragging -> - SectionItem( - section = section, - sectionLinkUiState = sectionLinkUiState, - sectionIndex = sectionIndex, - isDragging = isDragging, - onSectionTitleChanged = onSectionTitleChanged, - onClickRemoveSection = onClickRemoveSection, - onClickAddLesson = onClickAddLesson, - onClickRemoveLesson = onClickRemoveLesson, - dragModifier = Modifier.draggableHandle( - onDragStarted = { - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - }, - onDragStopped = { - haptic.performHapticFeedback(HapticFeedbackType.LongPress) - } + uiState.sections.forEachIndexed { sectionIndex, section -> + item(key = "section_header_${section.uid}") { + ReorderableItem( + state = reorderableLazyListState, + key = "section_header_${section.uid}" + ) { isDragging -> + SectionItem( + section = section, + sectionIndex = sectionIndex, + isDragging = isDragging, + onSectionTitleChanged = onSectionTitleChanged, + onClickRemoveSection = onClickRemoveSection, + onClickAddLesson = onClickAddLesson, + dragModifier = Modifier.draggableHandle( + onDragStarted = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + }, + onDragStopped = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + } + ) ) - ) + } + } + + section.items.forEachIndexed { linkIndex, link -> + item(key = "lesson_${section.uid}_${link.href}_$linkIndex") { + ReorderableItem( + state = reorderableLazyListState, + key = "lesson_${section.uid}_${link.href}_$linkIndex" + ) { isDragging -> + LessonItem( + link = link, + sectionLinkUiState = sectionLinkUiState, + sectionIndex = sectionIndex, + linkIndex = linkIndex, + onClickRemoveLesson = onClickRemoveLesson, + isDragging = isDragging, + dragModifier = Modifier.draggableHandle( + onDragStarted = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + }, + onDragStopped = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + } + ) + ) + } + } } } } @@ -220,13 +288,11 @@ fun CurriculumMappingEditScreen( @Composable private fun SectionItem( section: CurriculumMappingSection, - sectionLinkUiState: (CurriculumMappingSectionLink) -> Flow>, sectionIndex: Int, isDragging: Boolean, onSectionTitleChanged: (Int, String) -> Unit, onClickRemoveSection: (Int) -> Unit, onClickAddLesson: (Int) -> Unit, - onClickRemoveLesson: (Int, Int) -> Unit, dragModifier: Modifier = Modifier ) { Card( @@ -305,20 +371,6 @@ private fun SectionItem( Text(stringResource(Res.string.lesson)) } } - - section.items.forEachIndexed { linkIndex, link -> - LessonItem( - link = link, - sectionLinkUiState = sectionLinkUiState, - sectionIndex = sectionIndex, - linkIndex = linkIndex, - onClickRemoveLesson = onClickRemoveLesson, - enabled = !isDragging - ) - if (linkIndex < section.items.size - 1) { - Spacer(modifier = Modifier.height(8.dp)) - } - } } } } @@ -330,7 +382,8 @@ private fun LessonItem( sectionIndex: Int, linkIndex: Int, onClickRemoveLesson: (Int, Int) -> Unit, - enabled: Boolean + isDragging: Boolean, + dragModifier: Modifier = Modifier ) { val stateFlow = remember(link.href) { @@ -339,41 +392,60 @@ private fun LessonItem( val linkUiState by stateFlow.collectAsState(initial = DataLoadingState()) - Row( + Card( modifier = Modifier .fillMaxWidth() - .padding(start = 32.dp, top = 8.dp), - verticalAlignment = Alignment.CenterVertically + .padding(start = 48.dp, end = 16.dp, bottom = 8.dp), + elevation = CardDefaults.cardElevation( + defaultElevation = if (isDragging) 8.dp else 1.dp + ) ) { - linkUiState.dataOrNull()?.icon?.also { iconUrl -> - RespectAsyncImage( - uri = iconUrl.toString(), - contentDescription = "", - contentScale = ContentScale.Crop, - modifier = Modifier - .size(36.dp) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Filled.DragHandle, + contentDescription = stringResource(Res.string.drag), + modifier = dragModifier.size(20.dp), + tint = if (isDragging) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurfaceVariant ) - } - Spacer(Modifier.width(16.dp)) + Spacer(Modifier.width(12.dp)) - Text( - text = link.title ?: "${stringResource(Res.string.lesson)} ${linkIndex + 1}", - modifier = Modifier.weight(1f) - ) + linkUiState.dataOrNull()?.icon?.also { iconUrl -> + RespectAsyncImage( + uri = iconUrl.toString(), + contentDescription = "", + contentScale = ContentScale.Crop, + modifier = Modifier + .size(36.dp) + ) + } - Spacer(Modifier.width(16.dp)) + Spacer(Modifier.width(16.dp)) - IconButton( - onClick = { onClickRemoveLesson(sectionIndex, linkIndex) }, - modifier = Modifier.size(24.dp), - enabled = enabled - ) { - Icon( - Icons.Filled.Close, - contentDescription = stringResource(Res.string.remove_lesson), - modifier = Modifier.size(16.dp) + Text( + text = link.title ?: "${stringResource(Res.string.lesson)} ${linkIndex + 1}", + modifier = Modifier.weight(1f) ) + + Spacer(Modifier.width(16.dp)) + + IconButton( + onClick = { onClickRemoveLesson(sectionIndex, linkIndex) }, + modifier = Modifier.size(24.dp), + enabled = !isDragging + ) { + Icon( + Icons.Filled.Close, + contentDescription = stringResource(Res.string.remove_lesson), + modifier = Modifier.size(16.dp) + ) + } } } } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt index 10d7e6333..0bfb8b618 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt @@ -198,6 +198,58 @@ class CurriculumMappingEditViewModel( } } + fun onLessonMovedBetweenSections( + fromSectionIndex: Int, + fromLinkIndex: Int, + toSectionIndex: Int, + toLinkIndex: Int + ) { + updateUiStateAndCommit { prev -> + val mapping = prev.mapping + if (mapping == null) { + prev + } else if (fromSectionIndex == toSectionIndex) { + prev.copy( + mapping = mapping.copy( + sections = mapping.sections.updateAtIndex(fromSectionIndex) { section -> + section.copy( + items = section.items.moveItem(from = fromLinkIndex, to = toLinkIndex) + ) + } + ) + ) + } else { + val fromSection = mapping.sections[fromSectionIndex] + val lessonToMove = fromSection.items[fromLinkIndex] + + val updatedFromSection = fromSection.copy( + items = fromSection.items.filterIndexed { index, _ -> index != fromLinkIndex } + ) + + val toSection = mapping.sections[toSectionIndex] + val updatedToSection = toSection.copy( + items = buildList { + addAll(toSection.items.take(toLinkIndex)) + add(lessonToMove) + addAll(toSection.items.drop(toLinkIndex)) + } + ) + + prev.copy( + mapping = mapping.copy( + sections = mapping.sections.mapIndexed { index, section -> + when (index) { + fromSectionIndex -> updatedFromSection + toSectionIndex -> updatedToSection + else -> section + } + } + ) + ) + } + } + } + fun onClickAddLesson(sectionIndex: Int) { _uiState.update { it.copy(pendingLessonSectionIndex = sectionIndex) } _navCommandFlow.tryEmit( From ee3cdbfd947cda89037f47b14c064772aa818130 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Fri, 5 Dec 2025 13:10:06 +0530 Subject: [PATCH 04/58] Refactor and improve lesson item reordering behavior --- .../mapping/edit/CurriculumMappingEditScreen.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt index 551b35bd0..39864846a 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt @@ -136,6 +136,15 @@ fun CurriculumMappingEditScreen( ) haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) } + fromLessonIndex >= 0 && toLessonIndex == -1 && toSectionIndex >= 0 -> { + onLessonMovedBetweenSections( + fromSectionIndex, + fromLessonIndex, + toSectionIndex, + 0 + ) + haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) + } fromLessonIndex == -1 && toLessonIndex == -1 && fromSectionIndex >= 0 && toSectionIndex >= 0 -> { onSectionMoved(fromSectionIndex, toSectionIndex) From d6d662b43acad522f1b8a9dd3db327c0c524e0d1 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Mon, 8 Dec 2025 09:41:12 +0530 Subject: [PATCH 05/58] add semi-transparent state to items when dragging a section --- .../edit/CurriculumMappingEditScreen.kt | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt index 39864846a..d79a9e8d9 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt @@ -48,6 +48,7 @@ import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingE import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingSectionUiState import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMappingSection import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMappingSectionLink +import androidx.compose.ui.draw.alpha @Composable @@ -87,6 +88,8 @@ fun CurriculumMappingEditScreen( ) { val haptic = LocalHapticFeedback.current val lazyListState = rememberLazyListState() + var draggingSectionIndex by remember { mutableStateOf(null) } + var isDraggingAnySection by remember { mutableStateOf(false) } val reorderableLazyListState = rememberReorderableLazyListState( lazyListState = lazyListState, onMove = { from, to -> @@ -244,6 +247,16 @@ fun CurriculumMappingEditScreen( state = reorderableLazyListState, key = "section_header_${section.uid}" ) { isDragging -> + LaunchedEffect(isDragging) { + if (isDragging) { + draggingSectionIndex = sectionIndex + isDraggingAnySection = true + } else { + draggingSectionIndex = null + isDraggingAnySection = false + } + } + SectionItem( section = section, sectionIndex = sectionIndex, @@ -267,8 +280,10 @@ fun CurriculumMappingEditScreen( item(key = "lesson_${section.uid}_${link.href}_$linkIndex") { ReorderableItem( state = reorderableLazyListState, - key = "lesson_${section.uid}_${link.href}_$linkIndex" + key = "lesson_${section.uid}_${link.href}_$linkIndex", + enabled = !isDraggingAnySection ) { isDragging -> + val isParentSectionDragging = draggingSectionIndex == sectionIndex LessonItem( link = link, sectionLinkUiState = sectionLinkUiState, @@ -276,7 +291,9 @@ fun CurriculumMappingEditScreen( linkIndex = linkIndex, onClickRemoveLesson = onClickRemoveLesson, isDragging = isDragging, + isParentSectionDragging = isParentSectionDragging, dragModifier = Modifier.draggableHandle( + enabled = !isDraggingAnySection, onDragStarted = { haptic.performHapticFeedback(HapticFeedbackType.LongPress) }, @@ -392,6 +409,7 @@ private fun LessonItem( linkIndex: Int, onClickRemoveLesson: (Int, Int) -> Unit, isDragging: Boolean, + isParentSectionDragging: Boolean = false, dragModifier: Modifier = Modifier ) { @@ -404,7 +422,14 @@ private fun LessonItem( Card( modifier = Modifier .fillMaxWidth() - .padding(start = 48.dp, end = 16.dp, bottom = 8.dp), + .padding(start = 48.dp, end = 16.dp, bottom = 8.dp) + .then( + if (isParentSectionDragging) { + Modifier.alpha(0.5f) + } else { + Modifier + } + ), elevation = CardDefaults.cardElevation( defaultElevation = if (isDragging) 8.dp else 1.dp ) @@ -420,6 +445,7 @@ private fun LessonItem( contentDescription = stringResource(Res.string.drag), modifier = dragModifier.size(20.dp), tint = if (isDragging) MaterialTheme.colorScheme.primary + else if (isParentSectionDragging) MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) else MaterialTheme.colorScheme.onSurfaceVariant ) @@ -439,7 +465,12 @@ private fun LessonItem( Text( text = link.title ?: "${stringResource(Res.string.lesson)} ${linkIndex + 1}", - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f), + color = if (isParentSectionDragging) { + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f) + } else { + MaterialTheme.colorScheme.onSurface + } ) Spacer(Modifier.width(16.dp)) @@ -447,7 +478,7 @@ private fun LessonItem( IconButton( onClick = { onClickRemoveLesson(sectionIndex, linkIndex) }, modifier = Modifier.size(24.dp), - enabled = !isDragging + enabled = !isDragging && !isParentSectionDragging ) { Icon( Icons.Filled.Close, From 05d7a2453d828dfb4795f56a1ecba3580ac50bb3 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Thu, 11 Dec 2025 09:01:43 +0530 Subject: [PATCH 06/58] update te curriculum list screen as per the new prototype --- .../list/CurriculumMappingListScreen.kt | 315 +++++++++++++----- .../edit/CurriculumMappingEditViewModel.kt | 5 +- .../list/CurriculumMappingListViewModel.kt | 13 +- 3 files changed, 236 insertions(+), 97 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/CurriculumMappingListScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/CurriculumMappingListScreen.kt index 2458caa36..eefc9bdd2 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/CurriculumMappingListScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/CurriculumMappingListScreen.kt @@ -1,28 +1,41 @@ package world.respect.app.view.curriculum.mapping.list +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Book -import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Link import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource import world.respect.shared.generated.resources.Res -import world.respect.shared.generated.resources.map -import world.respect.shared.generated.resources.more_options -import world.respect.shared.generated.resources.no_textbooks_available -import world.respect.shared.generated.resources.textbooks +import world.respect.shared.generated.resources.add +import world.respect.shared.generated.resources.add_from_link +import world.respect.shared.generated.resources.add_new +import world.respect.shared.generated.resources.add_playlist +import world.respect.shared.generated.resources.all +import world.respect.shared.generated.resources.apps +import world.respect.shared.generated.resources.my_playlists +import world.respect.shared.generated.resources.no_playlists_yet +import world.respect.shared.generated.resources.playlists +import world.respect.shared.generated.resources.school_playlists import world.respect.shared.viewmodel.curriculum.mapping.list.CurriculumMappingListUiState import world.respect.shared.viewmodel.curriculum.mapping.list.CurriculumMappingListViewModel import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping @@ -32,119 +45,238 @@ fun CurriculumMappingListScreen( uiState: CurriculumMappingListUiState = CurriculumMappingListUiState(), onClickMapping: (CurriculumMapping) -> Unit = {}, onClickMoreOptions: (CurriculumMapping) -> Unit = {}, - onClickMap: () -> Unit = {}, + onClickAdd: () -> Unit = {}, + onClickAddFromLink: () -> Unit = {}, ) { - Box(modifier = Modifier.fillMaxSize()) { + var selectedMainTabIndex by remember { mutableIntStateOf(1) } + var selectedFilterChipIndex by remember { mutableIntStateOf(0) } + var isFabMenuExpanded by remember { mutableStateOf(false) } + + val mainTabs = listOf( + stringResource(Res.string.apps), + stringResource(Res.string.playlists) + ) + val filterChips = listOf( + stringResource(Res.string.all), + stringResource(Res.string.school_playlists), + stringResource(Res.string.my_playlists) + ) + + Scaffold( + floatingActionButton = { + if (selectedMainTabIndex == 1) { + Column( + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + if (isFabMenuExpanded) { + FloatingActionButton( + onClick = { + onClickAdd() + isFabMenuExpanded = false + }, + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + shape = RoundedCornerShape(16.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + Icons.Filled.Add, + contentDescription = stringResource(Res.string.add_new), + modifier = Modifier.size(20.dp) + ) + Text( + text = stringResource(Res.string.add_new), + fontSize = 14.sp, + fontWeight = FontWeight.Medium + ) + } + } + + FloatingActionButton( + onClick = { + onClickAddFromLink() + isFabMenuExpanded = false + }, + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + shape = RoundedCornerShape(16.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + Icons.Filled.Link, + contentDescription = stringResource(Res.string.add_from_link), + modifier = Modifier.size(20.dp) + ) + Text( + text = stringResource(Res.string.add_from_link), + fontSize = 14.sp, + fontWeight = FontWeight.Medium + ) + } + } + } + + FloatingActionButton( + onClick = { isFabMenuExpanded = !isFabMenuExpanded }, + shape = RoundedCornerShape(16.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + Icons.Filled.Add, + contentDescription = stringResource(Res.string.add), + modifier = Modifier.size(20.dp) + ) + Text( + text = stringResource(Res.string.add_playlist), + fontSize = 14.sp, + fontWeight = FontWeight.Medium + ) + } + } + } + } + } + ) { paddingValues -> Column( modifier = Modifier .fillMaxSize() - .padding(16.dp) + .padding(paddingValues) ) { - if (uiState.mappings.isEmpty()) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text( - text = stringResource(Res.string.no_textbooks_available), - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center + TabRow( + selectedTabIndex = selectedMainTabIndex, + modifier = Modifier.fillMaxWidth(), + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface + ) { + mainTabs.forEachIndexed { index, title -> + Tab( + selected = selectedMainTabIndex == index, + onClick = { selectedMainTabIndex = index }, + text = { + Text( + text = title, + style = MaterialTheme.typography.titleMedium + ) + } ) } - } else { - LazyVerticalGrid( - columns = GridCells.Fixed(2), - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.fillMaxWidth() - ) { - items( - items = uiState.mappings, - key = { mapping -> mapping.uid } - ) { mapping -> - MappingCard( - mapping = mapping, - onClickMapping = onClickMapping, - onClickMoreOptions = onClickMoreOptions + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + filterChips.forEachIndexed { index, label -> + FilterChip( + selected = selectedFilterChipIndex == index, + onClick = { selectedFilterChipIndex = index }, + label = { Text(label) } + ) + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .weight(1f) + ) { + if (uiState.mappings.isEmpty()) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(Res.string.no_playlists_yet), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center ) } + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(vertical = 8.dp) + ) { + items( + items = uiState.mappings, + key = { mapping -> mapping.uid } + ) { mapping -> + MappingListItem( + mapping = mapping, + onClickMapping = onClickMapping + ) + } + } } } } } } -@OptIn(ExperimentalMaterial3Api::class) @Composable -private fun MappingCard( +private fun MappingListItem( mapping: CurriculumMapping, - onClickMapping: (CurriculumMapping) -> Unit, - onClickMoreOptions: (CurriculumMapping) -> Unit + onClickMapping: (CurriculumMapping) -> Unit ) { - Card( - onClick = { onClickMapping(mapping) }, + Row( modifier = Modifier .fillMaxWidth() - .height(200.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), - shape = RoundedCornerShape(12.dp) + .clickable { onClickMapping(mapping) } + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) ) { + Icon( + Icons.Filled.Book, + contentDescription = null, + modifier = Modifier.size(40.dp), + tint = MaterialTheme.colorScheme.primary + ) + Column( - modifier = Modifier - .fillMaxSize() - .padding(12.dp) + modifier = Modifier.weight(1f) ) { + Text( + text = mapping.title, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Spacer(modifier = Modifier.height(4.dp)) + Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth() + horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Icon( Icons.Filled.Book, - contentDescription = stringResource(Res.string.textbooks), - modifier = Modifier.size(20.dp) + contentDescription = null, + modifier = Modifier.size(12.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant ) - Spacer(modifier = Modifier.width(4.dp)) + Text( - text = mapping.title, - style = MaterialTheme.typography.titleSmall, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f) + text = "${mapping.sections.size} section, ${mapping.sections.sumOf { it.items.size }} items", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant ) - - IconButton( - onClick = { onClickMoreOptions(mapping) }, - modifier = Modifier.size(24.dp) - ) { - Icon( - Icons.Filled.MoreVert, - contentDescription = stringResource(Res.string.more_options) - ) - } - } - - Spacer(modifier = Modifier.height(8.dp)) - - Box( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - contentAlignment = Alignment.Center - ) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text( - text = mapping.title.split(" ") - .mapNotNull { it.firstOrNull()?.uppercase() } - .take(2) - .joinToString(""), - style = MaterialTheme.typography.headlineLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } } } } @@ -160,6 +292,7 @@ fun CurriculumMappingListScreenForViewModel( uiState = uiState, onClickMapping = viewModel::onClickMapping, onClickMoreOptions = viewModel::onClickMoreOptions, - onClickMap = viewModel::onClickMap + onClickAdd = viewModel::onClickMap, + onClickAddFromLink = viewModel::onClickAddFromLink ) } \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt index 0bfb8b618..ff91b8b57 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt @@ -23,6 +23,7 @@ import world.respect.libutil.ext.updateAtIndex import world.respect.libutil.ext.resolve import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.edit_mapping +import world.respect.shared.generated.resources.edit_playlist import world.respect.shared.generated.resources.required_field import world.respect.shared.generated.resources.save import world.respect.shared.navigation.CurriculumMappingEdit @@ -91,10 +92,10 @@ class CurriculumMappingEditViewModel( init { _appUiState.update { prev -> prev.copy( - title = Res.string.edit_mapping.asUiText(), + title = Res.string.edit_playlist.asUiText(), userAccountIconVisible = false, actionBarButtonState = ActionBarButtonUiState( - visible = false, + visible = true, text = Res.string.save.asUiText(), onClick = ::onClickSave ), diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/CurriculumMappingListViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/CurriculumMappingListViewModel.kt index cbc3c75d2..56c3327b8 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/CurriculumMappingListViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/CurriculumMappingListViewModel.kt @@ -14,6 +14,7 @@ import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.error_unexpected_result_type import world.respect.shared.generated.resources.mapping import world.respect.shared.generated.resources.mappings +import world.respect.shared.navigation.CurriculumMappingDetail import world.respect.shared.navigation.CurriculumMappingEdit import world.respect.shared.navigation.NavCommand import world.respect.shared.navigation.NavResultReturner @@ -47,13 +48,15 @@ class CurriculumMappingListViewModel( prev.copy( title = Res.string.mappings.asUiText(), userAccountIconVisible = true, + /* fabState = FabUiState( visible = true, icon = FabUiState.FabIcon.ADD, text = Res.string.mapping.asUiText(), onClick = ::onClickMap, ), - hideBottomNavigation = true, + */ + hideBottomNavigation = false, ) } viewModelScope.launch { @@ -109,7 +112,7 @@ class CurriculumMappingListViewModel( fun onClickMapping(mapping: CurriculumMapping) { _navCommandFlow.tryEmit( NavCommand.Navigate( - CurriculumMappingEdit.create( + CurriculumMappingDetail.create( uid = mapping.uid, mappingData = mapping ) @@ -124,7 +127,9 @@ class CurriculumMappingListViewModel( ) ) } - + fun onClickAddFromLink() { + // TODO: Implement add from link functionality + } fun onClickMoreOptions(mapping: CurriculumMapping) { // TODO } @@ -140,6 +145,6 @@ class CurriculumMappingListViewModel( } companion object { - private const val KEY_MAPPINGS_LIST = "mappings_list" + const val KEY_MAPPINGS_LIST = "mappings_list" } } \ No newline at end of file From cff7ac57260bd6b5c09213f01e832e5e19322fe5 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Thu, 11 Dec 2025 11:05:16 +0530 Subject: [PATCH 07/58] Create the curriculum detail screen and navigate to curriculum edit screen --- .../kotlin/world/respect/AppKoinModule.kt | 2 + .../detail/CurriculumMappingDetailScreen.kt | 354 ++++++++++++++++++ .../view/playlists/list/PlaylistsScreen.kt | 344 ----------------- .../composeResources/values/strings.xml | 5 +- .../respect/shared/navigation/AppRoutes.kt | 30 ++ .../CurriculumMappingDetailViewModel.kt | 143 +++++++ .../edit/CurriculumMappingEditViewModel.kt | 3 +- .../mapping/model/CurriculumMapping.kt | 3 + .../model/CurriculumMappingSectionLink.kt | 3 +- 9 files changed, 540 insertions(+), 347 deletions(-) create mode 100644 respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/detail/CurriculumMappingDetailScreen.kt delete mode 100644 respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/list/PlaylistsScreen.kt create mode 100644 respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/detail/CurriculumMappingDetailViewModel.kt diff --git a/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt b/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt index a188e743a..42a06e118 100644 --- a/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt +++ b/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt @@ -210,6 +210,7 @@ import world.respect.shared.viewmodel.curriculum.mapping.list.CurriculumMappingL import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel import world.respect.shared.viewmodel.schooldirectory.edit.SchoolDirectoryEditViewModel import world.respect.shared.viewmodel.schooldirectory.list.SchoolDirectoryListViewModel +import world.respect.shared.viewmodel.curriculum.mapping.detail.CurriculumMappingDetailViewModel const val SHARED_PREF_SETTINGS_NAME = "respect_settings3_" @@ -328,6 +329,7 @@ val appKoinModule = module { viewModelOf(::AssignmentDetailViewModel) viewModelOf(::EnrollmentListViewModel) viewModelOf(::EnrollmentEditViewModel) + viewModelOf(::CurriculumMappingDetailViewModel) single { diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/detail/CurriculumMappingDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/detail/CurriculumMappingDetailScreen.kt new file mode 100644 index 000000000..ad37d504b --- /dev/null +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/detail/CurriculumMappingDetailScreen.kt @@ -0,0 +1,354 @@ +package world.respect.app.view.curriculum.mapping.detail + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Book +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.outlined.ContentCopy +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.KeyboardArrowDown +import androidx.compose.material.icons.outlined.Share +import androidx.compose.material.icons.outlined.Task +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import kotlinx.coroutines.flow.Flow +import org.jetbrains.compose.resources.stringResource +import world.respect.app.app.RespectAsyncImage +import world.respect.datalayer.DataLoadState +import world.respect.datalayer.DataLoadingState +import world.respect.datalayer.ext.dataOrNull +import world.respect.shared.generated.resources.Res +import world.respect.shared.generated.resources.assign +import world.respect.shared.generated.resources.copy_playlist +import world.respect.shared.generated.resources.delete +import world.respect.shared.generated.resources.edit +import world.respect.shared.generated.resources.share +import world.respect.shared.viewmodel.curriculum.mapping.detail.CurriculumMappingDetailUiState +import world.respect.shared.viewmodel.curriculum.mapping.detail.CurriculumMappingDetailViewModel +import world.respect.shared.viewmodel.curriculum.mapping.detail.CurriculumMappingLessonUiState +import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping +import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMappingSection +import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMappingSectionLink + +@Composable +fun CurriculumMappingDetailScreen( + uiState: CurriculumMappingDetailUiState = CurriculumMappingDetailUiState(), + lessonUiStateFor: (CurriculumMappingSectionLink) -> Flow>, + onClickEdit: () -> Unit = {}, + onClickShare: () -> Unit = {}, + onClickCopyPlaylist: () -> Unit = {}, + onClickAssign: () -> Unit = {}, + onClickDelete: () -> Unit = {}, + onClickLesson: (String) -> Unit = {}, +) { + Scaffold( + floatingActionButton = { + FloatingActionButton( + onClick = onClickEdit, + shape = RoundedCornerShape(16.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + Icons.Filled.Edit, + contentDescription = stringResource(Res.string.edit), + modifier = Modifier.size(20.dp) + ) + Text( + text = stringResource(Res.string.edit), + fontSize = 14.sp, + fontWeight = FontWeight.Medium + ) + } + } + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + contentPadding = PaddingValues(vertical = 8.dp) + ) { + item { + MappingHeaderCard( + mapping = uiState.mapping, + onClickShare = onClickShare, + onClickCopyPlaylist = onClickCopyPlaylist, + onClickAssign = onClickAssign, + onClickDelete = onClickDelete + ) + } + + uiState.mapping?.sections?.forEach { section -> + item(key = "section_${section.uid}") { + SectionHeader(section = section) + } + + items( + items = section.items, + key = { link -> link.href } + ) { link -> + LessonItem( + link = link, + lessonUiStateFor = lessonUiStateFor, + onClickLesson = onClickLesson + ) + } + } + } + } +} + +@Composable +private fun MappingHeaderCard( + mapping: CurriculumMapping?, + onClickShare: () -> Unit, + onClickCopyPlaylist: () -> Unit, + onClickAssign: () -> Unit, + onClickDelete: () -> Unit +) { + if (mapping == null) return + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface + ), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + Icons.Filled.Book, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.primary + ) + + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = mapping.description.orEmpty(), + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + if (!mapping.subject.isNullOrEmpty()) { + Text( + text = mapping.subject!!, + fontSize = 11.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + if (!mapping.grade.isNullOrEmpty()) { + Text( + text = mapping.grade!!, + fontSize = 11.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + if (!mapping.language.isNullOrEmpty()) { + Text( + text = mapping.language!!, + fontSize = 11.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + ActionButton( + icon = Icons.Outlined.Share, + label = stringResource(Res.string.share), + onClick = onClickShare + ) + ActionButton( + icon = Icons.Outlined.ContentCopy, + label = stringResource(Res.string.copy_playlist), + onClick = onClickCopyPlaylist + ) + ActionButton( + icon = Icons.Outlined.Task, + label = stringResource(Res.string.assign), + onClick = onClickAssign + ) + ActionButton( + icon = Icons.Outlined.Delete, + label = stringResource(Res.string.delete), + onClick = onClickDelete + ) + } + } + } +} + +@Composable +private fun ActionButton( + icon: androidx.compose.ui.graphics.vector.ImageVector, + label: String, + onClick: () -> Unit +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.clickable(onClick = onClick) + ) { + Icon( + icon, + contentDescription = label, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = label, + fontSize = 11.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} + +@Composable +private fun SectionHeader(section: CurriculumMappingSection) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = section.title, + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + Icons.Outlined.ContentCopy, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + Icon( + Icons.Outlined.KeyboardArrowDown, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +@Composable +private fun LessonItem( + link: CurriculumMappingSectionLink, + lessonUiStateFor: (CurriculumMappingSectionLink) -> Flow>, + onClickLesson: (String) -> Unit +) { + val stateFlow = remember(link.href) { + lessonUiStateFor(link) + } + + val lessonUiState by stateFlow.collectAsState(initial = DataLoadingState()) + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onClickLesson(link.href) } + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + lessonUiState.dataOrNull()?.icon?.also { iconUrl -> + link.title?.let { + RespectAsyncImage( + uri = iconUrl.toString(), + contentDescription = it, + contentScale = ContentScale.Crop, + modifier = Modifier.size(40.dp) + ) + } + } ?: run { + Icon( + Icons.Filled.Book, + contentDescription = null, + modifier = Modifier.size(40.dp), + tint = MaterialTheme.colorScheme.primary + ) + } + + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = link.title.orEmpty(), + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } +} + +@Composable +fun CurriculumMappingDetailScreenForViewModel( + viewModel: CurriculumMappingDetailViewModel +) { + val uiState by viewModel.uiState.collectAsState() + + CurriculumMappingDetailScreen( + uiState = uiState, + lessonUiStateFor = viewModel::lessonUiStateFor, + onClickEdit = viewModel::onClickEdit, + onClickShare = viewModel::onClickShare, + onClickCopyPlaylist = viewModel::onClickCopyPlaylist, + onClickAssign = viewModel::onClickAssign, + onClickDelete = viewModel::onClickDelete, + onClickLesson = viewModel::onClickLesson + ) +} \ No newline at end of file diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/list/PlaylistsScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/list/PlaylistsScreen.kt deleted file mode 100644 index 032d997d4..000000000 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/list/PlaylistsScreen.kt +++ /dev/null @@ -1,344 +0,0 @@ -package world.respect.app.view.playlists.enrollment.list - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import org.jetbrains.compose.resources.stringResource -import world.respect.shared.generated.resources.Res -import world.respect.shared.generated.resources.apps -import world.respect.shared.generated.resources.playlists -import world.respect.shared.generated.resources.all -import world.respect.shared.generated.resources.school_playlists -import world.respect.shared.generated.resources.my_playlists -import world.respect.shared.generated.resources.add_playlist -import world.respect.shared.generated.resources.add_new -import world.respect.shared.generated.resources.add_from_link -import world.respect.shared.generated.resources.all_sections_items -import world.respect.shared.generated.resources.created_by -import world.respect.shared.generated.resources.unknown_error -import world.respect.shared.viewmodel.playlists.list.MainTab -import world.respect.shared.viewmodel.playlists.list.PlaylistsViewModel -import world.respect.shared.viewmodel.playlists.list.PlaylistTab -import world.respect.shared.viewmodel.playlists.list.PlaylistUiModel -import world.respect.shared.viewmodel.playlists.list.PlaylistsUiState - -@Composable -fun PlaylistsScreenForViewModel( - viewModel: PlaylistsViewModel, - onPlaylistClick: (String) -> Unit, - onCreatePlaylist: () -> Unit, - onAddFromLink: () -> Unit -) { - val uiState by viewModel.uiState.collectAsState() - - PlaylistsScreen( - uiState = uiState, - onMainTabSelected = viewModel::onMainTabSelected, - onTabSelected = viewModel::onTabSelected, - onPlaylistClick = onPlaylistClick, - onCreatePlaylist = onCreatePlaylist, - onAddFromLink = onAddFromLink - ) -} - -@Composable -fun PlaylistsScreen( - uiState: PlaylistsUiState = PlaylistsUiState(), - onMainTabSelected: (MainTab) -> Unit = {}, - onTabSelected: (PlaylistTab) -> Unit = {}, - onPlaylistClick: (String) -> Unit = {}, - onCreatePlaylist: () -> Unit = {}, - onAddFromLink: () -> Unit = {} -) { - var showOptions by remember { mutableStateOf(false) } - - Scaffold( - topBar = { - PlaylistsTopBar( - selectedMainTab = uiState.selectedMainTab, - selectedTab = uiState.selectedTab, - onMainTabSelected = onMainTabSelected, - onTabSelected = onTabSelected - ) - }, - floatingActionButton = { - FloatingActionButtonWithOptions( - showOptions = showOptions, - onToggleOptions = { showOptions = !showOptions }, - onCreatePlaylist = onCreatePlaylist, - onAddFromLink = onAddFromLink - ) - } - ) { padding -> - Box(modifier = Modifier.fillMaxSize()) { - when { - uiState.isLoading -> { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) - } - } - uiState.error != null -> { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text( - text = uiState.error ?: stringResource(Res.string.unknown_error), - color = MaterialTheme.colorScheme.error - ) - } - } - else -> { - PlaylistsList( - playlists = uiState.playlists, - onPlaylistClick = onPlaylistClick, - modifier = Modifier.padding(padding) - ) - } - } - } - } -} - -@Composable -private fun PlaylistsTopBar( - selectedMainTab: MainTab, - selectedTab: PlaylistTab, - onMainTabSelected: (MainTab) -> Unit, - onTabSelected: (PlaylistTab) -> Unit -) { - Column( - modifier = Modifier.fillMaxWidth() - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 12.dp), - horizontalArrangement = Arrangement.SpaceEvenly - ) { - Text( - text = stringResource(Res.string.apps), - color = MaterialTheme.colorScheme.onSurface, - fontSize = 16.sp, - fontWeight = if (selectedMainTab == MainTab.APPS) FontWeight.Bold else FontWeight.Normal, - modifier = Modifier.clickable { onMainTabSelected(MainTab.APPS) } - ) - Text( - text = stringResource(Res.string.playlists), - color = MaterialTheme.colorScheme.onSurface, - fontSize = 16.sp, - fontWeight = if (selectedMainTab == MainTab.PLAYLISTS) FontWeight.Bold else FontWeight.Normal, - modifier = Modifier.clickable { onMainTabSelected(MainTab.PLAYLISTS) } - ) - } - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, bottom = 12.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - FilterChip( - selected = selectedTab == PlaylistTab.ALL, - onClick = { onTabSelected(PlaylistTab.ALL) }, - label = { Text(stringResource(Res.string.all)) } - ) - FilterChip( - selected = selectedTab == PlaylistTab.SCHOOL, - onClick = { onTabSelected(PlaylistTab.SCHOOL) }, - label = { Text(stringResource(Res.string.school_playlists)) } - ) - FilterChip( - selected = selectedTab == PlaylistTab.MY_PLAYLISTS, - onClick = { onTabSelected(PlaylistTab.MY_PLAYLISTS) }, - label = { Text(stringResource(Res.string.my_playlists)) } - ) - } - } -} - -@Composable -private fun PlaylistsList( - playlists: List, - onPlaylistClick: (String) -> Unit, - modifier: Modifier = Modifier -) { - LazyColumn( - modifier = modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) - ) { - items( - items = playlists, - key = { it.id } - ) { playlist -> - PlaylistItem( - playlist = playlist, - onClick = { onPlaylistClick(playlist.id) } - ) - } - } -} - -@Composable -private fun PlaylistItem( - playlist: PlaylistUiModel, - onClick: () -> Unit -) { - Card( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onClick), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) - ) { - Row( - modifier = Modifier.padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Surface( - modifier = Modifier.size(56.dp), - shape = RoundedCornerShape(28.dp), - color = MaterialTheme.colorScheme.surfaceVariant - ) { - Box( - contentAlignment = Alignment.Center - ) { - Text( - text = playlist.initials, - fontSize = 20.sp, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - Spacer(modifier = Modifier.width(12.dp)) - - Column { - Text( - text = playlist.title, - fontSize = 16.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ) - Spacer(modifier = Modifier.height(2.dp)) - Text( - text = stringResource(Res.string.all_sections_items, playlist.sectionCount, playlist.itemCount), - fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.height(2.dp)) - Text( - text = stringResource(Res.string.created_by, playlist.createdBy), - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } -} - -@Composable -private fun FloatingActionButtonWithOptions( - showOptions: Boolean, - onToggleOptions: () -> Unit, - onCreatePlaylist: () -> Unit, - onAddFromLink: () -> Unit -) { - Column( - horizontalAlignment = Alignment.End, - modifier = Modifier.padding(bottom = 80.dp) - ) { - if (showOptions) { - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalAlignment = Alignment.End, - modifier = Modifier.padding(bottom = 12.dp) - ) { - AddOptionButton( - text = stringResource(Res.string.add_new), - onClick = { - onCreatePlaylist() - onToggleOptions() - } - ) - - AddOptionButton( - text = stringResource(Res.string.add_from_link), - onClick = { - onAddFromLink() - onToggleOptions() - } - ) - } - } - - FloatingActionButton( - onClick = onToggleOptions, - containerColor = MaterialTheme.colorScheme.primary - ) { - Row( - modifier = Modifier.padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - imageVector = Icons.Filled.Add, - contentDescription = stringResource(Res.string.add_playlist), - modifier = Modifier.size(20.dp) - ) - Text( - text = stringResource(Res.string.playlists), - fontSize = 14.sp, - fontWeight = FontWeight.Medium - ) - } - } - } -} - -@Composable -private fun AddOptionButton( - text: String, - onClick: () -> Unit -) { - Surface( - shape = RoundedCornerShape(24.dp), - color = MaterialTheme.colorScheme.surfaceVariant, - tonalElevation = 2.dp, - modifier = Modifier.clickable(onClick = onClick) - ) { - Row( - modifier = Modifier.padding(horizontal = 20.dp, vertical = 14.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - Icon( - Icons.Filled.Add, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = text, - fontSize = 14.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } -} \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml index b926f571d..bb2bae4df 100644 --- a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml +++ b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml @@ -469,8 +469,11 @@ Create Playlist Error loading playlists Add new - Add playlist + Add from a link + Playlist + Copy playlist All sections, items Created by: + Edit playlist diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index 13ff34499..6f91441ab 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -669,6 +669,36 @@ data class CurriculumMappingEdit( ) } } +@Serializable +data class CurriculumMappingDetail( + val uid: Long, + private val mappingDataJson: String? = null +) : RespectAppRoute { + + @Transient + val mappingData: CurriculumMapping? = mappingDataJson?.let { jsonString -> + try { + Json.decodeFromString(CurriculumMapping.serializer(), jsonString) + } catch (e: Exception) { + null + } + } + + companion object { + fun create( + uid: Long, + mappingData: CurriculumMapping + ) = CurriculumMappingDetail( + uid = uid, + mappingDataJson = try { + Json.encodeToString(CurriculumMapping.serializer(), mappingData) + } catch (e: Exception) { + null + } + ) + } +} + @Serializable data class SetUsernameAndPassword( val guid: String diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/detail/CurriculumMappingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/detail/CurriculumMappingDetailViewModel.kt new file mode 100644 index 000000000..694c38a0b --- /dev/null +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/detail/CurriculumMappingDetailViewModel.kt @@ -0,0 +1,143 @@ +package world.respect.shared.viewmodel.curriculum.mapping.detail + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute +import io.ktor.http.Url +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json +import world.respect.datalayer.DataLoadParams +import world.respect.datalayer.DataLoadState +import world.respect.datalayer.RespectAppDataSource +import world.respect.datalayer.ext.map +import world.respect.lib.opds.model.findIcons +import world.respect.libutil.ext.resolve +import world.respect.shared.navigation.CurriculumMappingDetail +import world.respect.shared.navigation.CurriculumMappingEdit +import world.respect.shared.navigation.NavCommand +import world.respect.shared.navigation.NavResultReturner +import world.respect.shared.util.ext.asUiText +import world.respect.shared.viewmodel.RespectViewModel +import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel +import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping +import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMappingSectionLink + +data class CurriculumMappingDetailUiState( + val mapping: CurriculumMapping? = null, +) + +data class CurriculumMappingLessonUiState( + val icon: Url? = null, +) + +class CurriculumMappingDetailViewModel( + savedStateHandle: SavedStateHandle, + private val json: Json, + private val resultReturner: NavResultReturner, + private val respectAppDataSource: RespectAppDataSource, +) : RespectViewModel(savedStateHandle) { + + private val route: CurriculumMappingDetail = savedStateHandle.toRoute() + private val mappingUid = route.uid + private val mappingData = route.mappingData + + private val _uiState = MutableStateFlow(CurriculumMappingDetailUiState()) + val uiState = _uiState.asStateFlow() + + init { + _appUiState.update { prev -> + prev.copy( + userAccountIconVisible = true, + hideBottomNavigation = false, + ) + } + + loadMapping() + + viewModelScope.launch { + resultReturner.resultFlowForKey( + CurriculumMappingEditViewModel.KEY_SAVED_MAPPING + ).collect { result -> + val savedMapping = result.result as? CurriculumMapping + if (savedMapping != null && savedMapping.uid == mappingUid) { + updateMapping(savedMapping) + } + } + } + } + + private fun loadMapping() { + if (mappingData != null) { + updateMapping(mappingData) + } + } + + private fun updateMapping(mapping: CurriculumMapping) { + _uiState.update { + it.copy(mapping = mapping) + } + + _appUiState.update { prev -> + prev.copy( + title = mapping.title.asUiText() + ) + } + } + + fun lessonUiStateFor( + link: CurriculumMappingSectionLink + ): Flow> { + val publicationUrl = Url(link.href) + return respectAppDataSource.opdsDataSource.loadOpdsPublication( + url = publicationUrl, + params = DataLoadParams(), + referrerUrl = null, + expectedPublicationId = null, + ).map { opdsLoadState -> + opdsLoadState.map { publication -> + CurriculumMappingLessonUiState( + icon = publication.findIcons().firstOrNull()?.let { + publicationUrl.resolve(it.href) + } + ) + } + } + } + + fun onClickEdit() { + val mapping = _uiState.value.mapping ?: return + _navCommandFlow.tryEmit( + NavCommand.Navigate( + CurriculumMappingEdit.create( + uid = mapping.uid, + mappingData = mapping + ) + ) + ) + } + + fun onClickShare() { + // TODO: Implement share functionality + } + + fun onClickCopyPlaylist() { + // TODO: Implement copy playlist functionality + } + + fun onClickAssign() { + // TODO: Implement assign functionality + } + + fun onClickDelete() { + // TODO: Implement delete functionality + } + + fun onClickLesson(lessonHref: String) { + // TODO: Implement lesson click functionality + } +} \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt index ff91b8b57..6327d1d9a 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt @@ -79,10 +79,11 @@ class CurriculumMappingEditViewModel( private val route: CurriculumMappingEdit = savedStateHandle.toRoute() private val mappingUid = route.textbookUid + private val mappingData = route.mappingData private val _uiState = MutableStateFlow( CurriculumMappingEditUiState( - mapping = CurriculumMapping(uid = mappingUid), + mapping = mappingData ?: CurriculumMapping(uid = mappingUid), isNew = mappingUid == 0L ) ) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMapping.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMapping.kt index 34253f1bc..631a44e69 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMapping.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMapping.kt @@ -7,5 +7,8 @@ data class CurriculumMapping( val uid: Long = System.currentTimeMillis(), val title: String = "", val description: String = "", + val subject: String? = null, + val grade: String? = null, + val language: String? = null, val sections: List = emptyList() ) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMappingSectionLink.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMappingSectionLink.kt index 8003e7ae9..2fe468b6c 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMappingSectionLink.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMappingSectionLink.kt @@ -9,5 +9,6 @@ import kotlinx.serialization.Serializable data class CurriculumMappingSectionLink( val uid: Long = System.currentTimeMillis(), val href: String, - val title: String? = "" + val title: String? = "", + val description: String? = null ) From c44c14fe08f3c0628404a8a3e1e337f19255dc97 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Thu, 11 Dec 2025 11:05:30 +0530 Subject: [PATCH 08/58] Create the curriculum detail screen and navigate to curriculum edit screen --- .../kotlin/world/respect/app/app/AppNavHost.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt index 52ae68b6e..4f4b1df92 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt @@ -17,6 +17,7 @@ import world.respect.app.view.assignment.list.AssignmentListScreen import world.respect.app.view.clazz.list.ClazzListScreen import world.respect.app.view.clazz.edit.ClazzEditScreen import world.respect.app.view.clazz.detail.ClazzDetailScreen +import world.respect.app.view.curriculum.mapping.detail.CurriculumMappingDetailScreenForViewModel import world.respect.app.view.enrollment.edit.EnrollmentEditScreen import world.respect.app.view.enrollment.list.EnrollmentListScreen import world.respect.app.view.learningunit.detail.LearningUnitDetailScreen @@ -136,12 +137,14 @@ import world.respect.shared.viewmodel.manageuser.signup.CreateAccountViewModel import world.respect.app.view.settings.SettingsScreenForViewModel import world.respect.app.view.curriculum.mapping.list.CurriculumMappingListScreenForViewModel import world.respect.app.view.curriculum.mapping.edit.CurriculumMappingEditScreenForViewModel +import world.respect.shared.navigation.CurriculumMappingDetail import world.respect.shared.viewmodel.settings.SettingsViewModel import world.respect.shared.viewmodel.curriculum.mapping.list.CurriculumMappingListViewModel import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel import world.respect.shared.navigation.Settings import world.respect.shared.navigation.CurriculumMappingList import world.respect.shared.navigation.CurriculumMappingEdit +import world.respect.shared.viewmodel.curriculum.mapping.detail.CurriculumMappingDetailViewModel import world.respect.shared.viewmodel.onboarding.OnboardingViewModel import world.respect.shared.viewmodel.schooldirectory.edit.SchoolDirectoryEditViewModel import world.respect.shared.viewmodel.schooldirectory.list.SchoolDirectoryListViewModel @@ -558,6 +561,15 @@ fun AppNavHost( viewModel = viewModel ) } + composable { + val viewModel: CurriculumMappingDetailViewModel = respectViewModel( + onSetAppUiState = onSetAppUiState, + navController = respectNavController + ) + CurriculumMappingDetailScreenForViewModel( + viewModel = viewModel + ) + } composable{ val viewModel: SchoolDirectoryListViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, From 55d19dbb43ea863fb23ff62ac27e60dfb51a4e17 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Fri, 12 Dec 2025 12:59:50 +0530 Subject: [PATCH 09/58] Remove the curriculumDetailScreen --- .../detail/CurriculumMappingDetailScreen.kt | 354 ------------------ .../CurriculumMappingDetailViewModel.kt | 143 ------- .../mapping/model/CurriculumMapping.kt | 3 - .../model/CurriculumMappingSectionLink.kt | 3 +- 4 files changed, 1 insertion(+), 502 deletions(-) delete mode 100644 respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/detail/CurriculumMappingDetailScreen.kt delete mode 100644 respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/detail/CurriculumMappingDetailViewModel.kt diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/detail/CurriculumMappingDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/detail/CurriculumMappingDetailScreen.kt deleted file mode 100644 index ad37d504b..000000000 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/detail/CurriculumMappingDetailScreen.kt +++ /dev/null @@ -1,354 +0,0 @@ -package world.respect.app.view.curriculum.mapping.detail - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Book -import androidx.compose.material.icons.filled.Edit -import androidx.compose.material.icons.outlined.ContentCopy -import androidx.compose.material.icons.outlined.Delete -import androidx.compose.material.icons.outlined.KeyboardArrowDown -import androidx.compose.material.icons.outlined.Share -import androidx.compose.material.icons.outlined.Task -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import kotlinx.coroutines.flow.Flow -import org.jetbrains.compose.resources.stringResource -import world.respect.app.app.RespectAsyncImage -import world.respect.datalayer.DataLoadState -import world.respect.datalayer.DataLoadingState -import world.respect.datalayer.ext.dataOrNull -import world.respect.shared.generated.resources.Res -import world.respect.shared.generated.resources.assign -import world.respect.shared.generated.resources.copy_playlist -import world.respect.shared.generated.resources.delete -import world.respect.shared.generated.resources.edit -import world.respect.shared.generated.resources.share -import world.respect.shared.viewmodel.curriculum.mapping.detail.CurriculumMappingDetailUiState -import world.respect.shared.viewmodel.curriculum.mapping.detail.CurriculumMappingDetailViewModel -import world.respect.shared.viewmodel.curriculum.mapping.detail.CurriculumMappingLessonUiState -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMappingSection -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMappingSectionLink - -@Composable -fun CurriculumMappingDetailScreen( - uiState: CurriculumMappingDetailUiState = CurriculumMappingDetailUiState(), - lessonUiStateFor: (CurriculumMappingSectionLink) -> Flow>, - onClickEdit: () -> Unit = {}, - onClickShare: () -> Unit = {}, - onClickCopyPlaylist: () -> Unit = {}, - onClickAssign: () -> Unit = {}, - onClickDelete: () -> Unit = {}, - onClickLesson: (String) -> Unit = {}, -) { - Scaffold( - floatingActionButton = { - FloatingActionButton( - onClick = onClickEdit, - shape = RoundedCornerShape(16.dp) - ) { - Row( - modifier = Modifier.padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - Icons.Filled.Edit, - contentDescription = stringResource(Res.string.edit), - modifier = Modifier.size(20.dp) - ) - Text( - text = stringResource(Res.string.edit), - fontSize = 14.sp, - fontWeight = FontWeight.Medium - ) - } - } - } - ) { paddingValues -> - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues), - contentPadding = PaddingValues(vertical = 8.dp) - ) { - item { - MappingHeaderCard( - mapping = uiState.mapping, - onClickShare = onClickShare, - onClickCopyPlaylist = onClickCopyPlaylist, - onClickAssign = onClickAssign, - onClickDelete = onClickDelete - ) - } - - uiState.mapping?.sections?.forEach { section -> - item(key = "section_${section.uid}") { - SectionHeader(section = section) - } - - items( - items = section.items, - key = { link -> link.href } - ) { link -> - LessonItem( - link = link, - lessonUiStateFor = lessonUiStateFor, - onClickLesson = onClickLesson - ) - } - } - } - } -} - -@Composable -private fun MappingHeaderCard( - mapping: CurriculumMapping?, - onClickShare: () -> Unit, - onClickCopyPlaylist: () -> Unit, - onClickAssign: () -> Unit, - onClickDelete: () -> Unit -) { - if (mapping == null) return - - Card( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - Icon( - Icons.Filled.Book, - contentDescription = null, - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.primary - ) - - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = mapping.description.orEmpty(), - fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - - Spacer(modifier = Modifier.height(4.dp)) - - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp) - ) { - if (!mapping.subject.isNullOrEmpty()) { - Text( - text = mapping.subject!!, - fontSize = 11.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - if (!mapping.grade.isNullOrEmpty()) { - Text( - text = mapping.grade!!, - fontSize = 11.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - if (!mapping.language.isNullOrEmpty()) { - Text( - text = mapping.language!!, - fontSize = 11.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } - - Spacer(modifier = Modifier.height(16.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly - ) { - ActionButton( - icon = Icons.Outlined.Share, - label = stringResource(Res.string.share), - onClick = onClickShare - ) - ActionButton( - icon = Icons.Outlined.ContentCopy, - label = stringResource(Res.string.copy_playlist), - onClick = onClickCopyPlaylist - ) - ActionButton( - icon = Icons.Outlined.Task, - label = stringResource(Res.string.assign), - onClick = onClickAssign - ) - ActionButton( - icon = Icons.Outlined.Delete, - label = stringResource(Res.string.delete), - onClick = onClickDelete - ) - } - } - } -} - -@Composable -private fun ActionButton( - icon: androidx.compose.ui.graphics.vector.ImageVector, - label: String, - onClick: () -> Unit -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.clickable(onClick = onClick) - ) { - Icon( - icon, - contentDescription = label, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = label, - fontSize = 11.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } -} - -@Composable -private fun SectionHeader(section: CurriculumMappingSection) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = section.title, - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface - ) - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - Icon( - Icons.Outlined.ContentCopy, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - Icon( - Icons.Outlined.KeyboardArrowDown, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } -} - -@Composable -private fun LessonItem( - link: CurriculumMappingSectionLink, - lessonUiStateFor: (CurriculumMappingSectionLink) -> Flow>, - onClickLesson: (String) -> Unit -) { - val stateFlow = remember(link.href) { - lessonUiStateFor(link) - } - - val lessonUiState by stateFlow.collectAsState(initial = DataLoadingState()) - - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { onClickLesson(link.href) } - .padding(horizontal = 16.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - lessonUiState.dataOrNull()?.icon?.also { iconUrl -> - link.title?.let { - RespectAsyncImage( - uri = iconUrl.toString(), - contentDescription = it, - contentScale = ContentScale.Crop, - modifier = Modifier.size(40.dp) - ) - } - } ?: run { - Icon( - Icons.Filled.Book, - contentDescription = null, - modifier = Modifier.size(40.dp), - tint = MaterialTheme.colorScheme.primary - ) - } - - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = link.title.orEmpty(), - fontSize = 15.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } -} - -@Composable -fun CurriculumMappingDetailScreenForViewModel( - viewModel: CurriculumMappingDetailViewModel -) { - val uiState by viewModel.uiState.collectAsState() - - CurriculumMappingDetailScreen( - uiState = uiState, - lessonUiStateFor = viewModel::lessonUiStateFor, - onClickEdit = viewModel::onClickEdit, - onClickShare = viewModel::onClickShare, - onClickCopyPlaylist = viewModel::onClickCopyPlaylist, - onClickAssign = viewModel::onClickAssign, - onClickDelete = viewModel::onClickDelete, - onClickLesson = viewModel::onClickLesson - ) -} \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/detail/CurriculumMappingDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/detail/CurriculumMappingDetailViewModel.kt deleted file mode 100644 index 694c38a0b..000000000 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/detail/CurriculumMappingDetailViewModel.kt +++ /dev/null @@ -1,143 +0,0 @@ -package world.respect.shared.viewmodel.curriculum.mapping.detail - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.viewModelScope -import androidx.navigation.toRoute -import io.ktor.http.Url -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.serialization.json.Json -import world.respect.datalayer.DataLoadParams -import world.respect.datalayer.DataLoadState -import world.respect.datalayer.RespectAppDataSource -import world.respect.datalayer.ext.map -import world.respect.lib.opds.model.findIcons -import world.respect.libutil.ext.resolve -import world.respect.shared.navigation.CurriculumMappingDetail -import world.respect.shared.navigation.CurriculumMappingEdit -import world.respect.shared.navigation.NavCommand -import world.respect.shared.navigation.NavResultReturner -import world.respect.shared.util.ext.asUiText -import world.respect.shared.viewmodel.RespectViewModel -import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMappingSectionLink - -data class CurriculumMappingDetailUiState( - val mapping: CurriculumMapping? = null, -) - -data class CurriculumMappingLessonUiState( - val icon: Url? = null, -) - -class CurriculumMappingDetailViewModel( - savedStateHandle: SavedStateHandle, - private val json: Json, - private val resultReturner: NavResultReturner, - private val respectAppDataSource: RespectAppDataSource, -) : RespectViewModel(savedStateHandle) { - - private val route: CurriculumMappingDetail = savedStateHandle.toRoute() - private val mappingUid = route.uid - private val mappingData = route.mappingData - - private val _uiState = MutableStateFlow(CurriculumMappingDetailUiState()) - val uiState = _uiState.asStateFlow() - - init { - _appUiState.update { prev -> - prev.copy( - userAccountIconVisible = true, - hideBottomNavigation = false, - ) - } - - loadMapping() - - viewModelScope.launch { - resultReturner.resultFlowForKey( - CurriculumMappingEditViewModel.KEY_SAVED_MAPPING - ).collect { result -> - val savedMapping = result.result as? CurriculumMapping - if (savedMapping != null && savedMapping.uid == mappingUid) { - updateMapping(savedMapping) - } - } - } - } - - private fun loadMapping() { - if (mappingData != null) { - updateMapping(mappingData) - } - } - - private fun updateMapping(mapping: CurriculumMapping) { - _uiState.update { - it.copy(mapping = mapping) - } - - _appUiState.update { prev -> - prev.copy( - title = mapping.title.asUiText() - ) - } - } - - fun lessonUiStateFor( - link: CurriculumMappingSectionLink - ): Flow> { - val publicationUrl = Url(link.href) - return respectAppDataSource.opdsDataSource.loadOpdsPublication( - url = publicationUrl, - params = DataLoadParams(), - referrerUrl = null, - expectedPublicationId = null, - ).map { opdsLoadState -> - opdsLoadState.map { publication -> - CurriculumMappingLessonUiState( - icon = publication.findIcons().firstOrNull()?.let { - publicationUrl.resolve(it.href) - } - ) - } - } - } - - fun onClickEdit() { - val mapping = _uiState.value.mapping ?: return - _navCommandFlow.tryEmit( - NavCommand.Navigate( - CurriculumMappingEdit.create( - uid = mapping.uid, - mappingData = mapping - ) - ) - ) - } - - fun onClickShare() { - // TODO: Implement share functionality - } - - fun onClickCopyPlaylist() { - // TODO: Implement copy playlist functionality - } - - fun onClickAssign() { - // TODO: Implement assign functionality - } - - fun onClickDelete() { - // TODO: Implement delete functionality - } - - fun onClickLesson(lessonHref: String) { - // TODO: Implement lesson click functionality - } -} \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMapping.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMapping.kt index 631a44e69..34253f1bc 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMapping.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMapping.kt @@ -7,8 +7,5 @@ data class CurriculumMapping( val uid: Long = System.currentTimeMillis(), val title: String = "", val description: String = "", - val subject: String? = null, - val grade: String? = null, - val language: String? = null, val sections: List = emptyList() ) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMappingSectionLink.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMappingSectionLink.kt index 2fe468b6c..8003e7ae9 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMappingSectionLink.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMappingSectionLink.kt @@ -9,6 +9,5 @@ import kotlinx.serialization.Serializable data class CurriculumMappingSectionLink( val uid: Long = System.currentTimeMillis(), val href: String, - val title: String? = "", - val description: String? = null + val title: String? = "" ) From f013ad2355f53faffffef7403bbf52928a0ba6c2 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Mon, 15 Dec 2025 11:06:17 +0530 Subject: [PATCH 10/58] added two tabs apps and play lists in Home screen --- .../kotlin/world/respect/AppKoinModule.kt | 2 - .../kotlin/world/respect/app/app/App.kt | 6 +- .../world/respect/app/app/AppNavHost.kt | 21 -- .../view/apps/launcher/AppLauncherScreen.kt | 312 +++++++++++++++++- .../composeResources/values/strings.xml | 1 + .../apps/launcher/AppLauncherViewModel.kt | 159 +++++++-- 6 files changed, 452 insertions(+), 49 deletions(-) diff --git a/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt b/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt index 42a06e118..a188e743a 100644 --- a/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt +++ b/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt @@ -210,7 +210,6 @@ import world.respect.shared.viewmodel.curriculum.mapping.list.CurriculumMappingL import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel import world.respect.shared.viewmodel.schooldirectory.edit.SchoolDirectoryEditViewModel import world.respect.shared.viewmodel.schooldirectory.list.SchoolDirectoryListViewModel -import world.respect.shared.viewmodel.curriculum.mapping.detail.CurriculumMappingDetailViewModel const val SHARED_PREF_SETTINGS_NAME = "respect_settings3_" @@ -329,7 +328,6 @@ val appKoinModule = module { viewModelOf(::AssignmentDetailViewModel) viewModelOf(::EnrollmentListViewModel) viewModelOf(::EnrollmentEditViewModel) - viewModelOf(::CurriculumMappingDetailViewModel) single { diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/App.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/App.kt index de82cb311..c6c647604 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/App.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/App.kt @@ -25,6 +25,7 @@ import androidx.compose.material.icons.automirrored.filled.LibraryBooks import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.GridView +import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.ImportContacts import androidx.compose.material.icons.filled.Person import androidx.compose.material3.Icon @@ -41,6 +42,7 @@ import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.apps import world.respect.shared.generated.resources.assignments import world.respect.shared.generated.resources.classes +import world.respect.shared.generated.resources.home import world.respect.shared.generated.resources.people import world.respect.shared.navigation.AccountList import world.respect.shared.navigation.RespectAppLauncher @@ -70,8 +72,8 @@ private val routeNamePrefix = "world.respect.shared.navigation" val APP_TOP_LEVEL_NAV_ITEMS = listOf( TopNavigationItem( destRoute = RespectAppLauncher(), - icon = Icons.Filled.GridView, - label = Res.string.apps, + icon = Icons.Filled.Home, + label = Res.string.home, routeName = "$routeNamePrefix.RespectAppLauncher", ), TopNavigationItem( diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt index 4f4b1df92..79877d1c1 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt @@ -17,7 +17,6 @@ import world.respect.app.view.assignment.list.AssignmentListScreen import world.respect.app.view.clazz.list.ClazzListScreen import world.respect.app.view.clazz.edit.ClazzEditScreen import world.respect.app.view.clazz.detail.ClazzDetailScreen -import world.respect.app.view.curriculum.mapping.detail.CurriculumMappingDetailScreenForViewModel import world.respect.app.view.enrollment.edit.EnrollmentEditScreen import world.respect.app.view.enrollment.list.EnrollmentListScreen import world.respect.app.view.learningunit.detail.LearningUnitDetailScreen @@ -137,14 +136,12 @@ import world.respect.shared.viewmodel.manageuser.signup.CreateAccountViewModel import world.respect.app.view.settings.SettingsScreenForViewModel import world.respect.app.view.curriculum.mapping.list.CurriculumMappingListScreenForViewModel import world.respect.app.view.curriculum.mapping.edit.CurriculumMappingEditScreenForViewModel -import world.respect.shared.navigation.CurriculumMappingDetail import world.respect.shared.viewmodel.settings.SettingsViewModel import world.respect.shared.viewmodel.curriculum.mapping.list.CurriculumMappingListViewModel import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel import world.respect.shared.navigation.Settings import world.respect.shared.navigation.CurriculumMappingList import world.respect.shared.navigation.CurriculumMappingEdit -import world.respect.shared.viewmodel.curriculum.mapping.detail.CurriculumMappingDetailViewModel import world.respect.shared.viewmodel.onboarding.OnboardingViewModel import world.respect.shared.viewmodel.schooldirectory.edit.SchoolDirectoryEditViewModel import world.respect.shared.viewmodel.schooldirectory.list.SchoolDirectoryListViewModel @@ -542,15 +539,6 @@ fun AppNavHost( ) } - composable { - val viewModel: CurriculumMappingListViewModel = respectViewModel( - onSetAppUiState = onSetAppUiState, - navController = respectNavController - ) - CurriculumMappingListScreenForViewModel( - viewModel = viewModel - ) - } composable { val viewModel: CurriculumMappingEditViewModel = respectViewModel( @@ -561,15 +549,6 @@ fun AppNavHost( viewModel = viewModel ) } - composable { - val viewModel: CurriculumMappingDetailViewModel = respectViewModel( - onSetAppUiState = onSetAppUiState, - navController = respectNavController - ) - CurriculumMappingDetailScreenForViewModel( - viewModel = viewModel - ) - } composable{ val viewModel: SchoolDirectoryListViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/launcher/AppLauncherScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/launcher/AppLauncherScreen.kt index 98aade679..37f4cec0c 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/launcher/AppLauncherScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/launcher/AppLauncherScreen.kt @@ -14,27 +14,42 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Book +import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Tab +import androidx.compose.material3.TabRow import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.paging.compose.collectAsLazyPagingItems import kotlinx.coroutines.flow.emptyFlow import org.jetbrains.compose.resources.painterResource @@ -47,13 +62,24 @@ import world.respect.datalayer.NoDataLoadedState import world.respect.datalayer.compatibleapps.model.RespectAppManifest import world.respect.datalayer.ext.dataOrNull import world.respect.shared.generated.resources.Res +import world.respect.shared.generated.resources.add +import world.respect.shared.generated.resources.add_from_link +import world.respect.shared.generated.resources.add_new +import world.respect.shared.generated.resources.add_playlist +import world.respect.shared.generated.resources.all +import world.respect.shared.generated.resources.apps import world.respect.shared.generated.resources.empty import world.respect.shared.generated.resources.empty_list import world.respect.shared.generated.resources.more_info +import world.respect.shared.generated.resources.my_playlists +import world.respect.shared.generated.resources.no_playlists_yet +import world.respect.shared.generated.resources.playlists import world.respect.shared.generated.resources.remove +import world.respect.shared.generated.resources.school_playlists import world.respect.shared.viewmodel.app.appstate.getTitle import world.respect.shared.viewmodel.apps.launcher.AppLauncherUiState import world.respect.shared.viewmodel.apps.launcher.AppLauncherViewModel +import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping @Composable fun AppLauncherScreen( @@ -65,15 +91,85 @@ fun AppLauncherScreen( uiState = uiState, onClickApp = { viewModel.onClickApp(it) }, onClickRemove = { viewModel.onClickRemove(it) }, + onClickMapping = viewModel::onClickMapping, + onClickMoreOptions = viewModel::onClickMoreOptions, + onTabSelected = viewModel::onTabSelected, + onClickMap = viewModel::onClickMap, + onClickAddLink = viewModel::onClickAddLink, ) } - @Composable fun AppLauncherScreen( uiState: AppLauncherUiState, onClickApp: (DataLoadState) -> Unit, onClickRemove: (DataLoadState) -> Unit, + onClickMapping: (CurriculumMapping) -> Unit, + onClickMoreOptions: (CurriculumMapping) -> Unit, + onTabSelected: (Int) -> Unit, + onClickMap: () -> Unit, + onClickAddLink: () -> Unit, +) { + var selectedFilterChipIndex by remember { mutableIntStateOf(0) } + + val mainTabs = listOf( + stringResource(Res.string.apps), + stringResource(Res.string.playlists) + ) + + val filterChips = listOf( + stringResource(Res.string.all), + stringResource(Res.string.school_playlists), + stringResource(Res.string.my_playlists) + ) + + Column( + modifier = Modifier.fillMaxSize() + ) { + TabRow( + selectedTabIndex = uiState.selectedTabIndex, + modifier = Modifier.fillMaxWidth(), + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface + ) { + mainTabs.forEachIndexed { index, title -> + Tab( + selected = uiState.selectedTabIndex == index, + onClick = { onTabSelected(index) }, + text = { + Text( + text = title, + style = MaterialTheme.typography.titleMedium + ) + } + ) + } + } + + when (uiState.selectedTabIndex) { + 0 -> AppsTabContent( + uiState = uiState, + onClickApp = onClickApp, + onClickRemove = onClickRemove, + ) + 1 -> PlaylistsTabContent( + uiState = uiState, + filterChips = filterChips, + selectedFilterChipIndex = selectedFilterChipIndex, + onFilterChipSelected = { selectedFilterChipIndex = it }, + onClickMapping = onClickMapping, + onClickAdd = onClickMap, + onClickAddLink = onClickAddLink, + ) + } + } +} + +@Composable +private fun AppsTabContent( + uiState: AppLauncherUiState, + onClickApp: (DataLoadState) -> Unit, + onClickRemove: (DataLoadState) -> Unit, ) { val pager = respectRememberPager(uiState.apps) val lazyPagingItems = pager.flow.collectAsLazyPagingItems() @@ -115,7 +211,6 @@ fun AppLauncherScreen( verticalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp) ) { - items( count = lazyPagingItems.itemCount, key = { index -> @@ -144,6 +239,217 @@ fun AppLauncherScreen( } } +@Composable +private fun PlaylistsTabContent( + uiState: AppLauncherUiState, + filterChips: List, + selectedFilterChipIndex: Int, + onFilterChipSelected: (Int) -> Unit, + onClickMapping: (CurriculumMapping) -> Unit, + onClickAdd: () -> Unit, + onClickAddLink: () -> Unit, +) { + var isFabMenuExpanded by remember { mutableStateOf(false) } + + Box(modifier = Modifier.fillMaxSize()) { + Column(modifier = Modifier.fillMaxSize()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + filterChips.forEachIndexed { index, label -> + FilterChip( + selected = selectedFilterChipIndex == index, + onClick = { onFilterChipSelected(index) }, + label = { Text(label) } + ) + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .weight(1f) + ) { + if (uiState.mappings.isEmpty()) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(Res.string.no_playlists_yet), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center + ) + } + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = androidx.compose.foundation.layout.PaddingValues( + top = 8.dp, + bottom = 88.dp + ) + ) { + items( + items = uiState.mappings, + key = { mapping -> mapping.uid } + ) { mapping -> + MappingListItem( + mapping = mapping, + onClickMapping = onClickMapping + ) + } + } + } + } + } + Column( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(16.dp), + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + if (isFabMenuExpanded) { + FloatingActionButton( + onClick = { + onClickAdd() + isFabMenuExpanded = false + }, + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + shape = RoundedCornerShape(16.dp), + elevation = FloatingActionButtonDefaults.elevation(defaultElevation = 6.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + Icons.Filled.Add, + contentDescription = stringResource(Res.string.add_new), + modifier = Modifier.size(20.dp) + ) + Text( + text = stringResource(Res.string.add_new), + fontSize = 14.sp, + fontWeight = FontWeight.Medium + ) + } + } + + FloatingActionButton( + onClick = { + onClickAddLink() + isFabMenuExpanded = false + }, + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + shape = RoundedCornerShape(16.dp), + elevation = FloatingActionButtonDefaults.elevation(defaultElevation = 6.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + Icons.Filled.Link, + contentDescription = stringResource(Res.string.add_from_link), + modifier = Modifier.size(20.dp) + ) + Text( + text = stringResource(Res.string.add_from_link), + fontSize = 14.sp, + fontWeight = FontWeight.Medium + ) + } + } + } + + FloatingActionButton( + onClick = { isFabMenuExpanded = !isFabMenuExpanded }, + shape = RoundedCornerShape(16.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + Icons.Filled.Add, + contentDescription = stringResource(Res.string.add), + modifier = Modifier.size(20.dp) + ) + Text( + text = stringResource(Res.string.add_playlist), + fontSize = 14.sp, + fontWeight = FontWeight.Medium + ) + } + } + } + } +} + +@Composable +private fun MappingListItem( + mapping: CurriculumMapping, + onClickMapping: (CurriculumMapping) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onClickMapping(mapping) } + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + Icons.Filled.Book, + contentDescription = null, + modifier = Modifier.size(40.dp), + tint = MaterialTheme.colorScheme.primary + ) + + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = mapping.title, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + Icons.Filled.Book, + contentDescription = null, + modifier = Modifier.size(12.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Text( + text = "${mapping.sections.size} section, ${mapping.sections.sumOf { it.items.size }} items", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } +} + @Composable fun AppGridItem( app: DataLoadState, @@ -233,4 +539,4 @@ fun AppGridItem( Text(text = "-") } } -} +} \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml index bb2bae4df..4ba7408bf 100644 --- a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml +++ b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml @@ -475,5 +475,6 @@ All sections, items Created by: Edit playlist + Home diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt index ffeb6a493..f3be704b8 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.koin.core.scope.Scope @@ -29,26 +30,35 @@ import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.domain.devmode.GetDevModeEnabledUseCase import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.app -import world.respect.shared.generated.resources.apps import world.respect.shared.generated.resources.empty_list_description_admin import world.respect.shared.generated.resources.empty_list_description_non_admin +import world.respect.shared.generated.resources.home +import world.respect.shared.generated.resources.error_unexpected_result_type import world.respect.shared.navigation.AppsDetail import world.respect.shared.navigation.LearningUnitList import world.respect.shared.navigation.NavCommand import world.respect.shared.navigation.Settings import world.respect.shared.navigation.RespectAppLauncher import world.respect.shared.navigation.RespectAppList +import world.respect.shared.navigation.CurriculumMappingEdit +import world.respect.shared.navigation.EnterLink +import world.respect.shared.navigation.NavResultReturner import world.respect.shared.resources.UiText import world.respect.shared.util.ext.asUiText import world.respect.shared.util.ext.isAdmin import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.FabUiState +import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel +import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping data class AppLauncherUiState( - val apps : IPagingSourceFactory = EmptyPagingSourceFactory(), + val apps: IPagingSourceFactory = EmptyPagingSourceFactory(), val respectAppForSchoolApp: (SchoolApp) -> Flow> = { emptyFlow() }, val canRemove: Boolean = false, - val emptyListDescription: UiText?=null, + val emptyListDescription: UiText? = null, + val mappings: List = emptyList(), + val selectedTabIndex: Int = 0, + val error: UiText? = null, ) class AppLauncherViewModel( @@ -56,6 +66,8 @@ class AppLauncherViewModel( private val appDataSource: RespectAppDataSource, private val accountManager: RespectAccountManager, private val getDevModeEnabledUseCase: GetDevModeEnabledUseCase, + private val json: Json, + private val resultReturner: NavResultReturner, ) : RespectViewModel(savedStateHandle), KoinScopeComponent { override val scope: Scope = accountManager.requireActiveAccountScope() @@ -65,6 +77,7 @@ class AppLauncherViewModel( val uiState = _uiState.asStateFlow() var errorMessage: String = "" + private var isAdmin: Boolean = false private val route: RespectAppLauncher = savedStateHandle.toRoute() @@ -80,19 +93,8 @@ class AppLauncherViewModel( init { _appUiState.update { it.copy( - title = Res.string.apps.asUiText(), + title = Res.string.home.asUiText(), onClickSettings = ::onClickSettings, - fabState = FabUiState( - icon = FabUiState.FabIcon.ADD, - text = Res.string.app.asUiText(), - onClick = { - _navCommandFlow.tryEmit( - NavCommand.Navigate( - RespectAppList - ) - ) - } - ), hideBottomNavigation = route.resultDest != null, showBackButton = route.resultDest != null, ) @@ -101,20 +103,21 @@ class AppLauncherViewModel( _uiState.update { prev -> prev.copy( respectAppForSchoolApp = this@AppLauncherViewModel::respectAppForSchoolApp, - apps = pagingSourceHolder + apps = pagingSourceHolder, + mappings = loadMappingsFromSavedState(savedStateHandle) ) - } viewModelScope.launch { accountManager.selectedAccountAndPersonFlow.collect { selected -> val isAdmin = selected?.person?.isAdmin() == true val devModeEnabled = getDevModeEnabledUseCase() + + this@AppLauncherViewModel.isAdmin = isAdmin + updateFabState(isAdmin, _uiState.value.selectedTabIndex) + _appUiState.update { it.copy( - fabState = it.fabState.copy( - visible = isAdmin - ), settingsIconVisible = isAdmin && devModeEnabled, ) } @@ -129,6 +132,86 @@ class AppLauncherViewModel( } } } + + viewModelScope.launch { + resultReturner.resultFlowForKey( + CurriculumMappingEditViewModel.KEY_SAVED_MAPPING + ).collect { result -> + val savedMapping = result.result as? CurriculumMapping + if (savedMapping == null) { + _uiState.update { + it.copy(error = Res.string.error_unexpected_result_type.asUiText()) + } + return@collect + } + addOrUpdateMapping(savedMapping) + } + } + } + + fun onTabSelected(index: Int) { + _uiState.update { it.copy(selectedTabIndex = index) } + updateFabState(isAdmin, index) + } + + private fun updateFabState(isAdmin: Boolean, tabIndex: Int) { + _appUiState.update { + it.copy( + fabState = when (tabIndex) { + 0 -> FabUiState( + visible = isAdmin, + icon = FabUiState.FabIcon.ADD, + text = Res.string.app.asUiText(), + onClick = { + _navCommandFlow.tryEmit( + NavCommand.Navigate(RespectAppList) + ) + } + ) + 1 -> FabUiState(visible = false) + else -> FabUiState(visible = false) + } + ) + } + } + + private fun loadMappingsFromSavedState(savedStateHandle: SavedStateHandle): List { + val mappingsJson = savedStateHandle.get(KEY_MAPPINGS_LIST) ?: return emptyList() + return try { + json.decodeFromString>(mappingsJson) + } catch (e: Exception) { + emptyList() + } + } + + private fun saveMappingsToSavedState(mappings: List) { + savedStateHandle[KEY_MAPPINGS_LIST] = json.encodeToString( + kotlinx.serialization.builtins.ListSerializer(CurriculumMapping.serializer()), + mappings + ) + } + + private fun addOrUpdateMapping(mapping: CurriculumMapping) { + val currentMappings = _uiState.value.mappings.toMutableList() + val existingIndex = currentMappings.indexOfFirst { it.uid == mapping.uid } + + if (existingIndex >= 0) { + currentMappings[existingIndex] = mapping + } else { + val newMapping = if (mapping.uid == 0L) { + mapping.copy(uid = System.currentTimeMillis()) + } else { + mapping + } + currentMappings.add(newMapping) + } + + updateMappings(currentMappings) + } + + private fun updateMappings(newMappings: List) { + _uiState.update { it.copy(mappings = newMappings) } + saveMappingsToSavedState(newMappings) } fun onClickApp(app: DataLoadState) { @@ -152,6 +235,7 @@ class AppLauncherViewModel( ) ) } + fun onClickSettings() { _navCommandFlow.tryEmit( NavCommand.Navigate(Settings) @@ -173,11 +257,44 @@ class AppLauncherViewModel( } } + fun onClickMapping(mapping: CurriculumMapping) { + _navCommandFlow.tryEmit( + NavCommand.Navigate( + CurriculumMappingEdit.create( + uid = mapping.uid, + mappingData = mapping + ) + ) + ) + } + + fun onClickMap() { + _navCommandFlow.tryEmit( + NavCommand.Navigate( + CurriculumMappingEdit.create(uid = 0L, mappingData = null) + ) + ) + } + fun onClickAddLink() { + _navCommandFlow.tryEmit( + NavCommand.Navigate( + EnterLink.create() + ) + ) + } + + fun onClickMoreOptions(mapping: CurriculumMapping) { + // TODO: Implement more options + } + fun respectAppForSchoolApp(schoolApp: SchoolApp): Flow> { return appDataSource.compatibleAppsDataSource.getAppAsFlow( schoolApp.appManifestUrl, DataLoadParams() ) } -} + companion object { + const val KEY_MAPPINGS_LIST = "mappings_list" + } +} \ No newline at end of file From 8f2621a05500b79fef8b8e0a07c24ecddc532cc8 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Mon, 15 Dec 2025 19:08:22 +0530 Subject: [PATCH 11/58] Remove the CurriculumMappingListScreen --- .../kotlin/world/respect/AppKoinModule.kt | 2 - .../world/respect/app/app/AppNavHost.kt | 5 +- .../view/apps/launcher/AppLauncherScreen.kt | 36 ++- .../list/CurriculumMappingListScreen.kt | 298 ------------------ .../respect/shared/navigation/AppRoutes.kt | 48 +-- .../apps/launcher/AppLauncherViewModel.kt | 6 + .../list/CurriculumMappingListViewModel.kt | 150 --------- 7 files changed, 57 insertions(+), 488 deletions(-) delete mode 100644 respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/CurriculumMappingListScreen.kt delete mode 100644 respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/CurriculumMappingListViewModel.kt diff --git a/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt b/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt index a188e743a..1d8f8d673 100644 --- a/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt +++ b/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt @@ -206,7 +206,6 @@ import world.respect.shared.viewmodel.report.list.ReportTemplateListViewModel import world.respect.sharedse.domain.account.authenticatepassword.AuthenticatePasswordUseCaseDbImpl import java.io.File import world.respect.shared.viewmodel.settings.SettingsViewModel -import world.respect.shared.viewmodel.curriculum.mapping.list.CurriculumMappingListViewModel import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel import world.respect.shared.viewmodel.schooldirectory.edit.SchoolDirectoryEditViewModel import world.respect.shared.viewmodel.schooldirectory.list.SchoolDirectoryListViewModel @@ -316,7 +315,6 @@ val appKoinModule = module { viewModelOf(::IndicatorListViewModel) viewModelOf(::IndicatorDetailViewModel) viewModelOf(::SettingsViewModel) - viewModelOf(::CurriculumMappingListViewModel) viewModelOf(::CurriculumMappingEditViewModel) viewModelOf(::SetUsernameAndPasswordViewModel) viewModelOf(::ChangePasswordViewModel) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt index 79877d1c1..a7e0f1f81 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt @@ -134,13 +134,12 @@ import world.respect.shared.viewmodel.report.list.ReportListViewModel import world.respect.shared.viewmodel.report.list.ReportTemplateListViewModel import world.respect.shared.viewmodel.manageuser.signup.CreateAccountViewModel import world.respect.app.view.settings.SettingsScreenForViewModel -import world.respect.app.view.curriculum.mapping.list.CurriculumMappingListScreenForViewModel import world.respect.app.view.curriculum.mapping.edit.CurriculumMappingEditScreenForViewModel import world.respect.shared.viewmodel.settings.SettingsViewModel -import world.respect.shared.viewmodel.curriculum.mapping.list.CurriculumMappingListViewModel + import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel import world.respect.shared.navigation.Settings -import world.respect.shared.navigation.CurriculumMappingList + import world.respect.shared.navigation.CurriculumMappingEdit import world.respect.shared.viewmodel.onboarding.OnboardingViewModel import world.respect.shared.viewmodel.schooldirectory.edit.SchoolDirectoryEditViewModel diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/launcher/AppLauncherScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/launcher/AppLauncherScreen.kt index 37f4cec0c..27b02a97c 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/launcher/AppLauncherScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/launcher/AppLauncherScreen.kt @@ -96,6 +96,7 @@ fun AppLauncherScreen( onTabSelected = viewModel::onTabSelected, onClickMap = viewModel::onClickMap, onClickAddLink = viewModel::onClickAddLink, + onRemoveMapping = viewModel::removeMapping, ) } @@ -109,6 +110,7 @@ fun AppLauncherScreen( onTabSelected: (Int) -> Unit, onClickMap: () -> Unit, onClickAddLink: () -> Unit, + onRemoveMapping: (CurriculumMapping) -> Unit, ) { var selectedFilterChipIndex by remember { mutableIntStateOf(0) } @@ -160,6 +162,7 @@ fun AppLauncherScreen( onClickMapping = onClickMapping, onClickAdd = onClickMap, onClickAddLink = onClickAddLink, + onRemoveMapping = onRemoveMapping, ) } } @@ -248,6 +251,7 @@ private fun PlaylistsTabContent( onClickMapping: (CurriculumMapping) -> Unit, onClickAdd: () -> Unit, onClickAddLink: () -> Unit, + onRemoveMapping: (CurriculumMapping) -> Unit, ) { var isFabMenuExpanded by remember { mutableStateOf(false) } @@ -298,7 +302,8 @@ private fun PlaylistsTabContent( ) { mapping -> MappingListItem( mapping = mapping, - onClickMapping = onClickMapping + onClickMapping = onClickMapping, + onRemoveMapping = onRemoveMapping, ) } } @@ -398,8 +403,11 @@ private fun PlaylistsTabContent( @Composable private fun MappingListItem( mapping: CurriculumMapping, - onClickMapping: (CurriculumMapping) -> Unit + onClickMapping: (CurriculumMapping) -> Unit, + onRemoveMapping: (CurriculumMapping) -> Unit, ) { + var menuExpanded by remember { mutableStateOf(false) } + Row( modifier = Modifier .fillMaxWidth() @@ -447,6 +455,30 @@ private fun MappingListItem( ) } } + + Box { + IconButton(onClick = { menuExpanded = true }) { + Icon( + imageVector = Icons.Filled.MoreVert, + contentDescription = "", + ) + } + + DropdownMenu( + expanded = menuExpanded, + onDismissRequest = { menuExpanded = false } + ) { + DropdownMenuItem( + text = { + Text(stringResource(Res.string.remove)) + }, + onClick = { + menuExpanded = false + onRemoveMapping(mapping) + } + ) + } + } } } diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/CurriculumMappingListScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/CurriculumMappingListScreen.kt deleted file mode 100644 index eefc9bdd2..000000000 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/CurriculumMappingListScreen.kt +++ /dev/null @@ -1,298 +0,0 @@ -package world.respect.app.view.curriculum.mapping.list - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Book -import androidx.compose.material.icons.filled.Link -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import org.jetbrains.compose.resources.stringResource -import world.respect.shared.generated.resources.Res -import world.respect.shared.generated.resources.add -import world.respect.shared.generated.resources.add_from_link -import world.respect.shared.generated.resources.add_new -import world.respect.shared.generated.resources.add_playlist -import world.respect.shared.generated.resources.all -import world.respect.shared.generated.resources.apps -import world.respect.shared.generated.resources.my_playlists -import world.respect.shared.generated.resources.no_playlists_yet -import world.respect.shared.generated.resources.playlists -import world.respect.shared.generated.resources.school_playlists -import world.respect.shared.viewmodel.curriculum.mapping.list.CurriculumMappingListUiState -import world.respect.shared.viewmodel.curriculum.mapping.list.CurriculumMappingListViewModel -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping - -@Composable -fun CurriculumMappingListScreen( - uiState: CurriculumMappingListUiState = CurriculumMappingListUiState(), - onClickMapping: (CurriculumMapping) -> Unit = {}, - onClickMoreOptions: (CurriculumMapping) -> Unit = {}, - onClickAdd: () -> Unit = {}, - onClickAddFromLink: () -> Unit = {}, -) { - var selectedMainTabIndex by remember { mutableIntStateOf(1) } - var selectedFilterChipIndex by remember { mutableIntStateOf(0) } - var isFabMenuExpanded by remember { mutableStateOf(false) } - - val mainTabs = listOf( - stringResource(Res.string.apps), - stringResource(Res.string.playlists) - ) - val filterChips = listOf( - stringResource(Res.string.all), - stringResource(Res.string.school_playlists), - stringResource(Res.string.my_playlists) - ) - - Scaffold( - floatingActionButton = { - if (selectedMainTabIndex == 1) { - Column( - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - if (isFabMenuExpanded) { - FloatingActionButton( - onClick = { - onClickAdd() - isFabMenuExpanded = false - }, - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface, - shape = RoundedCornerShape(16.dp) - ) { - Row( - modifier = Modifier.padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - Icons.Filled.Add, - contentDescription = stringResource(Res.string.add_new), - modifier = Modifier.size(20.dp) - ) - Text( - text = stringResource(Res.string.add_new), - fontSize = 14.sp, - fontWeight = FontWeight.Medium - ) - } - } - - FloatingActionButton( - onClick = { - onClickAddFromLink() - isFabMenuExpanded = false - }, - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface, - shape = RoundedCornerShape(16.dp) - ) { - Row( - modifier = Modifier.padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - Icons.Filled.Link, - contentDescription = stringResource(Res.string.add_from_link), - modifier = Modifier.size(20.dp) - ) - Text( - text = stringResource(Res.string.add_from_link), - fontSize = 14.sp, - fontWeight = FontWeight.Medium - ) - } - } - } - - FloatingActionButton( - onClick = { isFabMenuExpanded = !isFabMenuExpanded }, - shape = RoundedCornerShape(16.dp) - ) { - Row( - modifier = Modifier.padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - Icons.Filled.Add, - contentDescription = stringResource(Res.string.add), - modifier = Modifier.size(20.dp) - ) - Text( - text = stringResource(Res.string.add_playlist), - fontSize = 14.sp, - fontWeight = FontWeight.Medium - ) - } - } - } - } - } - ) { paddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - ) { - TabRow( - selectedTabIndex = selectedMainTabIndex, - modifier = Modifier.fillMaxWidth(), - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface - ) { - mainTabs.forEachIndexed { index, title -> - Tab( - selected = selectedMainTabIndex == index, - onClick = { selectedMainTabIndex = index }, - text = { - Text( - text = title, - style = MaterialTheme.typography.titleMedium - ) - } - ) - } - } - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - filterChips.forEachIndexed { index, label -> - FilterChip( - selected = selectedFilterChipIndex == index, - onClick = { selectedFilterChipIndex = index }, - label = { Text(label) } - ) - } - } - - Box( - modifier = Modifier - .fillMaxSize() - .weight(1f) - ) { - if (uiState.mappings.isEmpty()) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text( - text = stringResource(Res.string.no_playlists_yet), - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center - ) - } - } else { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(vertical = 8.dp) - ) { - items( - items = uiState.mappings, - key = { mapping -> mapping.uid } - ) { mapping -> - MappingListItem( - mapping = mapping, - onClickMapping = onClickMapping - ) - } - } - } - } - } - } -} - -@Composable -private fun MappingListItem( - mapping: CurriculumMapping, - onClickMapping: (CurriculumMapping) -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { onClickMapping(mapping) } - .padding(horizontal = 16.dp, vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - Icon( - Icons.Filled.Book, - contentDescription = null, - modifier = Modifier.size(40.dp), - tint = MaterialTheme.colorScheme.primary - ) - - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = mapping.title, - fontSize = 15.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - - Spacer(modifier = Modifier.height(4.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - Icon( - Icons.Filled.Book, - contentDescription = null, - modifier = Modifier.size(12.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Text( - text = "${mapping.sections.size} section, ${mapping.sections.sumOf { it.items.size }} items", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } -} - -@Composable -fun CurriculumMappingListScreenForViewModel( - viewModel: CurriculumMappingListViewModel -) { - val uiState by viewModel.uiState.collectAsState() - - CurriculumMappingListScreen( - uiState = uiState, - onClickMapping = viewModel::onClickMapping, - onClickMoreOptions = viewModel::onClickMoreOptions, - onClickAdd = viewModel::onClickMap, - onClickAddFromLink = viewModel::onClickAddFromLink - ) -} \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index 6f91441ab..74c2746fb 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -230,7 +230,21 @@ class IndictorEdit(val indicatorId: String?) : RespectAppRoute object RespectAppList : RespectAppRoute @Serializable -object EnterLink : RespectAppRoute +data class EnterLink( + private val resultDestStr: String? = null, +) : RespectAppRoute, RouteWithResultDest { + + @Transient + override val resultDest: ResultDest? = ResultDest.fromStringOrNull(resultDestStr) + + companion object { + fun create( + resultDest: ResultDest? = null, + ) = EnterLink( + resultDestStr = resultDest.encodeToJsonStringOrNull() + ) + } +} @Serializable data class GetStartedScreen( @@ -635,8 +649,6 @@ data class PersonEdit( @Serializable data object Settings : RespectAppRoute -@Serializable -data object CurriculumMappingList : RespectAppRoute @Serializable data class CurriculumMappingEdit( @@ -669,36 +681,6 @@ data class CurriculumMappingEdit( ) } } -@Serializable -data class CurriculumMappingDetail( - val uid: Long, - private val mappingDataJson: String? = null -) : RespectAppRoute { - - @Transient - val mappingData: CurriculumMapping? = mappingDataJson?.let { jsonString -> - try { - Json.decodeFromString(CurriculumMapping.serializer(), jsonString) - } catch (e: Exception) { - null - } - } - - companion object { - fun create( - uid: Long, - mappingData: CurriculumMapping - ) = CurriculumMappingDetail( - uid = uid, - mappingDataJson = try { - Json.encodeToString(CurriculumMapping.serializer(), mappingData) - } catch (e: Exception) { - null - } - ) - } -} - @Serializable data class SetUsernameAndPassword( val guid: String diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt index f3be704b8..f0b35ec42 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt @@ -275,6 +275,7 @@ class AppLauncherViewModel( ) ) } + fun onClickAddLink() { _navCommandFlow.tryEmit( NavCommand.Navigate( @@ -287,6 +288,11 @@ class AppLauncherViewModel( // TODO: Implement more options } + fun removeMapping(mapping: CurriculumMapping) { + val updated = _uiState.value.mappings.filter { it.uid != mapping.uid } + updateMappings(updated) + } + fun respectAppForSchoolApp(schoolApp: SchoolApp): Flow> { return appDataSource.compatibleAppsDataSource.getAppAsFlow( schoolApp.appManifestUrl, diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/CurriculumMappingListViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/CurriculumMappingListViewModel.kt deleted file mode 100644 index 56c3327b8..000000000 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/CurriculumMappingListViewModel.kt +++ /dev/null @@ -1,150 +0,0 @@ -package world.respect.shared.viewmodel.curriculum.mapping.list - -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.viewModelScope -import io.ktor.http.Url -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.serialization.json.Json -import world.respect.shared.generated.resources.Res -import world.respect.shared.generated.resources.error_unexpected_result_type -import world.respect.shared.generated.resources.mapping -import world.respect.shared.generated.resources.mappings -import world.respect.shared.navigation.CurriculumMappingDetail -import world.respect.shared.navigation.CurriculumMappingEdit -import world.respect.shared.navigation.NavCommand -import world.respect.shared.navigation.NavResultReturner -import world.respect.shared.resources.UiText -import world.respect.shared.util.ext.asUiText -import world.respect.shared.viewmodel.RespectViewModel -import world.respect.shared.viewmodel.app.appstate.FabUiState -import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping - -data class CurriculumMappingListUiState( - val mappings: List = emptyList(), - val error: UiText? = null, -) - -class CurriculumMappingListViewModel( - savedStateHandle: SavedStateHandle, - private val json: Json, - private val resultReturner: NavResultReturner, -) : RespectViewModel(savedStateHandle) { - - private val _uiState = MutableStateFlow( - CurriculumMappingListUiState( - mappings = loadMappingsFromSavedState(savedStateHandle) - ) - ) - val uiState = _uiState.asStateFlow() - - init { - _appUiState.update { prev -> - prev.copy( - title = Res.string.mappings.asUiText(), - userAccountIconVisible = true, - /* - fabState = FabUiState( - visible = true, - icon = FabUiState.FabIcon.ADD, - text = Res.string.mapping.asUiText(), - onClick = ::onClickMap, - ), - */ - hideBottomNavigation = false, - ) - } - viewModelScope.launch { - resultReturner.resultFlowForKey( - CurriculumMappingEditViewModel.KEY_SAVED_MAPPING - ).collect { result -> - val savedMapping = result.result as? CurriculumMapping - if (savedMapping == null) { - _uiState.update { - it.copy(error = Res.string.error_unexpected_result_type.asUiText()) - } - return@collect - } - addOrUpdateMapping(savedMapping) - } - } - } - - private fun loadMappingsFromSavedState(savedStateHandle: SavedStateHandle): List { - val mappingsJson = savedStateHandle.get(KEY_MAPPINGS_LIST) ?: return emptyList() - return try { - json.decodeFromString>(mappingsJson) - } catch (e: Exception) { - emptyList() - } - } - - private fun saveMappingsToSavedState(mappings: List) { - savedStateHandle[KEY_MAPPINGS_LIST] = json.encodeToString( - kotlinx.serialization.builtins.ListSerializer(CurriculumMapping.serializer()), - mappings - ) - } - - private fun addOrUpdateMapping(mapping: CurriculumMapping) { - val currentMappings = _uiState.value.mappings.toMutableList() - val existingIndex = currentMappings.indexOfFirst { it.uid == mapping.uid } - - if (existingIndex >= 0) { - currentMappings[existingIndex] = mapping - } else { - val newMapping = if (mapping.uid == 0L) { - mapping.copy(uid = System.currentTimeMillis()) - } else { - mapping - } - currentMappings.add(newMapping) - } - - updateMappings(currentMappings) - } - - fun onClickMapping(mapping: CurriculumMapping) { - _navCommandFlow.tryEmit( - NavCommand.Navigate( - CurriculumMappingDetail.create( - uid = mapping.uid, - mappingData = mapping - ) - ) - ) - } - - fun onClickMap() { - _navCommandFlow.tryEmit( - NavCommand.Navigate( - CurriculumMappingEdit.create(uid = 0L, mappingData = null) - ) - ) - } - fun onClickAddFromLink() { - // TODO: Implement add from link functionality - } - fun onClickMoreOptions(mapping: CurriculumMapping) { - // TODO - } - - private fun updateMappings(newMappings: List) { - _uiState.update { it.copy(mappings = newMappings) } - saveMappingsToSavedState(newMappings) - } - - fun removeMapping(mapping: CurriculumMapping) { - val updated = _uiState.value.mappings.filter { it.uid != mapping.uid } - updateMappings(updated) - } - - companion object { - const val KEY_MAPPINGS_LIST = "mappings_list" - } -} \ No newline at end of file From f154e7070e4d9544ae85e1ea5c4fa1df0beb1ab8 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Tue, 16 Dec 2025 10:22:21 +0530 Subject: [PATCH 12/58] Added onclick lesson to navigate to leaning unit detail screen --- .../edit/CurriculumMappingEditScreen.kt | 12 +++++++++-- .../viewmodel/apps/list/AppListViewModel.kt | 3 ++- .../edit/CurriculumMappingEditViewModel.kt | 20 ++++++++++++++++++- .../model/CurriculumMappingSectionLink.kt | 4 +++- .../viewmodel/settings/SettingsViewModel.kt | 4 ++-- 5 files changed, 36 insertions(+), 7 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt index d79a9e8d9..b584793bb 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt @@ -69,6 +69,7 @@ fun CurriculumMappingEditScreenForViewModel( onClickAddLesson = viewModel::onClickAddLesson, onClickRemoveLesson = viewModel::onClickRemoveLesson, onLessonMovedBetweenSections = viewModel::onLessonMovedBetweenSections, + onClickLesson = viewModel::onClickLesson, ) } @@ -85,6 +86,7 @@ fun CurriculumMappingEditScreen( onClickAddLesson: (Int) -> Unit = {}, onClickRemoveLesson: (Int, Int) -> Unit = { _, _ -> }, onLessonMovedBetweenSections: (Int, Int, Int, Int) -> Unit = { _, _, _, _ -> }, + onClickLesson: (CurriculumMappingSectionLink) -> Unit = {}, ) { val haptic = LocalHapticFeedback.current val lazyListState = rememberLazyListState() @@ -93,7 +95,7 @@ fun CurriculumMappingEditScreen( val reorderableLazyListState = rememberReorderableLazyListState( lazyListState = lazyListState, onMove = { from, to -> - val headerItemCount = 4 //TODO: This MUST be explained + val headerItemCount = 4 val fromIndex = from.index - headerItemCount val toIndex = to.index - headerItemCount @@ -290,6 +292,7 @@ fun CurriculumMappingEditScreen( sectionIndex = sectionIndex, linkIndex = linkIndex, onClickRemoveLesson = onClickRemoveLesson, + onClickLesson = onClickLesson, isDragging = isDragging, isParentSectionDragging = isParentSectionDragging, dragModifier = Modifier.draggableHandle( @@ -408,6 +411,7 @@ private fun LessonItem( sectionIndex: Int, linkIndex: Int, onClickRemoveLesson: (Int, Int) -> Unit, + onClickLesson: (CurriculumMappingSectionLink) -> Unit = {}, isDragging: Boolean, isParentSectionDragging: Boolean = false, dragModifier: Modifier = Modifier @@ -429,6 +433,10 @@ private fun LessonItem( } else { Modifier } + ) + .clickable( + enabled = !isDragging && !isParentSectionDragging, + onClick = { onClickLesson(link) } ), elevation = CardDefaults.cardElevation( defaultElevation = if (isDragging) 8.dp else 1.dp @@ -488,4 +496,4 @@ private fun LessonItem( } } } -} +} \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/list/AppListViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/list/AppListViewModel.kt index 9f7e540a5..2c0ae7350 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/list/AppListViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/list/AppListViewModel.kt @@ -21,6 +21,7 @@ import world.respect.shared.navigation.NavCommand import world.respect.shared.util.ext.asUiText + data class AppListUiState( val appList: List> = emptyList() ) @@ -63,7 +64,7 @@ class AppListViewModel( fun onClickAddLink() { _navCommandFlow.tryEmit( NavCommand.Navigate( - EnterLink + EnterLink.create() ) ) } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt index 6327d1d9a..12ce1a6ca 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt @@ -27,6 +27,7 @@ import world.respect.shared.generated.resources.edit_playlist import world.respect.shared.generated.resources.required_field import world.respect.shared.generated.resources.save import world.respect.shared.navigation.CurriculumMappingEdit +import world.respect.shared.navigation.LearningUnitDetail import world.respect.shared.navigation.NavCommand import world.respect.shared.navigation.NavResult import world.respect.shared.navigation.NavResultReturner @@ -118,7 +119,8 @@ class CurriculumMappingEditViewModel( it.copy( items = it.items + CurriculumMappingSectionLink( href = selectedLearningUnit.learningUnitManifestUrl.toString(), - title = selectedLearningUnit.selectedPublication.metadata.title.getTitle() + title = selectedLearningUnit.selectedPublication.metadata.title.getTitle(), + appManifestUrl = selectedLearningUnit.appManifestUrl ) ) } @@ -280,6 +282,22 @@ class CurriculumMappingEditViewModel( } } + fun onClickLesson(link: CurriculumMappingSectionLink) { + val publicationUrl = Url(link.href) + val appManifestUrl = link.appManifestUrl ?: return + + _navCommandFlow.tryEmit( + NavCommand.Navigate( + LearningUnitDetail.create( + learningUnitManifestUrl = publicationUrl, + appManifestUrl = appManifestUrl, + refererUrl = publicationUrl, + expectedIdentifier = null + ) + ) + ) + } + /** * Provide a flow that creates the SectionLinkUiState . */ diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMappingSectionLink.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMappingSectionLink.kt index 8003e7ae9..3ec577701 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMappingSectionLink.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMappingSectionLink.kt @@ -1,5 +1,6 @@ package world.respect.shared.viewmodel.curriculum.mapping.model +import io.ktor.http.Url import kotlinx.serialization.Serializable /** @@ -9,5 +10,6 @@ import kotlinx.serialization.Serializable data class CurriculumMappingSectionLink( val uid: Long = System.currentTimeMillis(), val href: String, - val title: String? = "" + val title: String? = "", + val appManifestUrl: Url? = null, ) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/settings/SettingsViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/settings/SettingsViewModel.kt index 23b8c3b98..6843b0968 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/settings/SettingsViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/settings/SettingsViewModel.kt @@ -8,8 +8,8 @@ import kotlinx.coroutines.flow.update import kotlinx.serialization.json.Json import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.settings -import world.respect.shared.navigation.CurriculumMappingList import world.respect.shared.navigation.NavCommand +import world.respect.shared.navigation.RespectAppLauncher import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel @@ -43,7 +43,7 @@ class SettingsViewModel( fun onNavigateToMapping() { _navCommandFlow.tryEmit( - NavCommand.Navigate(CurriculumMappingList) + NavCommand.Navigate(RespectAppLauncher()) ) } } \ No newline at end of file From 2446746ddfedab0a462910d9e0cbea0f8999e286 Mon Sep 17 00:00:00 2001 From: pooja Date: Tue, 16 Dec 2025 11:47:08 +0400 Subject: [PATCH 13/58] added home and playlist test --- .maestro/flows/000_000_hello_world.yaml | 2 +- ...001_001_invite_using_invite_code_test.yaml | 12 +-- .../flows/001_002_add_user_direct_test.yaml | 4 +- .../001_003_login_using_school_link_test.yaml | 2 +- .maestro/flows/002_browse_lessons_test.yaml | 81 +++++++++++++++++-- ...er_assigns_assignment_to_a_class_test.yaml | 2 +- .../subflows/school_admin_login_flow.yaml | 2 +- 7 files changed, 88 insertions(+), 17 deletions(-) diff --git a/.maestro/flows/000_000_hello_world.yaml b/.maestro/flows/000_000_hello_world.yaml index d4b1e93c8..4fbfddd6b 100644 --- a/.maestro/flows/000_000_hello_world.yaml +++ b/.maestro/flows/000_000_hello_world.yaml @@ -39,6 +39,6 @@ onFlowComplete: when: visible: "Save password for Respect?" file: "subflows/save_password_prompt_cancel.yaml" -- assertVisible: "Apps" +- assertVisible: "Home" - tapOn: id: "user_account_icon" \ No newline at end of file diff --git a/.maestro/flows/001_001_invite_using_invite_code_test.yaml b/.maestro/flows/001_001_invite_using_invite_code_test.yaml index 8168c2267..fe74aa89c 100644 --- a/.maestro/flows/001_001_invite_using_invite_code_test.yaml +++ b/.maestro/flows/001_001_invite_using_invite_code_test.yaml @@ -22,7 +22,7 @@ onFlowComplete: - runFlow: "subflows/school_admin_login_flow.yaml" - assertVisible: id: "app_title" - text: "Apps" + text: "Home" - tapOn: "Classes" - assertVisible: id: "app_title" @@ -149,7 +149,7 @@ onFlowComplete: id : "password" - inputText: "testt1" - tapOn: "Login" -- assertVisible: "Apps" +- assertVisible: "Home" - tapOn: "Classes" - assertVisible: id: "app_title" @@ -225,7 +225,7 @@ onFlowComplete: id : "password" - inputText: "testt1" - tapOn: "Login" -- assertVisible: "Apps" +- assertVisible: "Home" - tapOn: "Classes" - assertVisible: id: "app_title" @@ -258,7 +258,7 @@ onFlowComplete: id : "password" - inputText: "tests1" - tapOn: "Login" -- assertVisible: "Apps" +- assertVisible: "Home" - tapOn: "Classes" - assertVisible: id: "app_title" @@ -352,7 +352,7 @@ onFlowComplete: id : "password" - inputText: "testt1" - tapOn: "Login" -- assertVisible: "Apps" +- assertVisible: "Home" - tapOn: "People" - assertVisible: "Parent User" - assertVisible: "Child User" @@ -385,7 +385,7 @@ onFlowComplete: id : "password" - inputText: "testp1" - tapOn: "Login" -- assertVisible: "Apps" +- assertVisible: "Home" - tapOn: "Classes" - assertVisible: id: "app_title" diff --git a/.maestro/flows/001_002_add_user_direct_test.yaml b/.maestro/flows/001_002_add_user_direct_test.yaml index a8f34cb16..b66418927 100644 --- a/.maestro/flows/001_002_add_user_direct_test.yaml +++ b/.maestro/flows/001_002_add_user_direct_test.yaml @@ -133,7 +133,7 @@ onFlowComplete: when: visible: "Save password for Respect?" file: "subflows/save_password_prompt_cancel.yaml" -- assertVisible: "Apps" +- assertVisible: "Home" - tapOn: "People" - tapOn: "Test User" - tapOn: "Manage account" @@ -178,4 +178,4 @@ onFlowComplete: when: visible: "Save password for Respect?" file: "subflows/save_password_prompt_cancel.yaml" -- assertVisible: "Apps" \ No newline at end of file +- assertVisible: "Home" \ No newline at end of file diff --git a/.maestro/flows/001_003_login_using_school_link_test.yaml b/.maestro/flows/001_003_login_using_school_link_test.yaml index d532b485f..87164d7fe 100644 --- a/.maestro/flows/001_003_login_using_school_link_test.yaml +++ b/.maestro/flows/001_003_login_using_school_link_test.yaml @@ -48,5 +48,5 @@ onFlowComplete: when: visible: "Save password for Respect?" file: "subflows/save_password_prompt_cancel.yaml" -- assertVisible: "Apps" +- assertVisible: "Home" diff --git a/.maestro/flows/002_browse_lessons_test.yaml b/.maestro/flows/002_browse_lessons_test.yaml index 118cdb93c..2ca3a2249 100644 --- a/.maestro/flows/002_browse_lessons_test.yaml +++ b/.maestro/flows/002_browse_lessons_test.yaml @@ -17,9 +17,13 @@ onFlowComplete: file: "scripts/teardown.js" --- - runFlow: "subflows/school_admin_login_flow.yaml" - +- assertVisible: "Home" +- assertVisible: + id: "app_title" + text: "Home" +- tapOn: "Apps" - tapOn: - id: "floating_action_button" + id: "floating_action_button" # +Add App - tapOn: "Add from Link" - tapOn: "Link*" - inputText: "https://respect.world/respect-ds/case_valid/appmanifest.json" @@ -31,10 +35,8 @@ onFlowComplete: - assertVisible: "Add App" # verify App got added to Apps section - tapOn: "Add App" +- tapOn: "Home" - tapOn: "Apps" -- assertVisible: - id: "app_title" - text: "Apps" - assertVisible: "My app" - tapOn: "My app" - assertVisible: "Lessons" @@ -49,5 +51,74 @@ onFlowComplete: timeout: 1000 - assertVisible: "Hello World Lesson" - tapOn: "Close" +- tapOn: "Home" +- tapOn: "Playlists" +- assertVisible: "All" +- assertVisible: "School Playlists" +- assertVisible: "My Playlists" +- assertVisible: "No playlists yet" +- tapOn: + id: "floating_action_button" # + Playlist +- assertVisible: "Add new" +- assertVisible: "Add from a link" +- tapOn: + id: "Add new" +- assertVisible: + id: "app_title" + text: "Add playlist" +- tapOn: "Save" # To test mandatory fields +- assertVisible: "Required field" #Title field is mandatory +- tapOn: "Title*" +- inputText: "Playlist - Grade 1" +- tapOn: "Description" +- inputText: "Test list" +- tapOn: "Subject" +- tapOn: "English" +- tapOn: "Grade" +- tapOn: "Grade 1" +- tapOn: "Section" +- tapOn: "Section title" +- inputText: "Day 1" +- hideKeyboard +- tapOn: + id: "add_item" +- tapOn: "My app" +- tapOn: "Lessons" +- tapOn: "Grade 1" +- tapOn: "Lesson 001" +- assertVisible: + id: "app_title" + text: "Add playlist" +- assertVisible: "Lesson 001" +- tapOn: "Save" +- assertVisible: + id: "app_title" + text: "Playlist - Grade 1" +- assertVisible: "Day 1" +- assertVisible: "Lesson 001" +- tapOn: + id: "floating_action_button" # Edit button +- assertVisible: + id: "app_title" + text: "Edit playlist" +- tapOn: "Section" +- tapOn: "Section title" +- inputText: "Day 2" +- hideKeyboard +- tapOn: "Save" +- assertVisible: + id: "app_title" + text: "Playlist - Grade 1" +- assertVisible: "Day 1" +- assertVisible: "Lesson 001" +- assertVisible: "Day 2" +- assertVisible: + id: "share_btn" +- assertVisible: + id: "copy_btn" +- assertVisible: + id: "assign_btn" +- assertVisible: + id: "delete_btn" diff --git a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml index 6a7d5908b..51f467221 100644 --- a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml +++ b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml @@ -41,7 +41,7 @@ onFlowComplete: when: visible: "Save password for Respect?" file: "subflows/save_password_prompt_cancel.yaml" -- assertVisible: "Apps" +- assertVisible: "Home" - tapOn: "Classes" - assertVisible: id: "app_title" diff --git a/.maestro/flows/subflows/school_admin_login_flow.yaml b/.maestro/flows/subflows/school_admin_login_flow.yaml index bedde5007..37ccbabd4 100644 --- a/.maestro/flows/subflows/school_admin_login_flow.yaml +++ b/.maestro/flows/subflows/school_admin_login_flow.yaml @@ -23,4 +23,4 @@ appId: world.respect.app when: visible: "Save password for Respect?" file: "save_password_prompt_cancel.yaml" -- assertVisible: "Apps" + From bb08d4fac7daa09d673a2822bd346f27f1ebd29b Mon Sep 17 00:00:00 2001 From: pooja Date: Tue, 16 Dec 2025 12:56:12 +0400 Subject: [PATCH 14/58] added playlist --- .maestro/flows/002_browse_lessons_test.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.maestro/flows/002_browse_lessons_test.yaml b/.maestro/flows/002_browse_lessons_test.yaml index 2ca3a2249..42cf96d33 100644 --- a/.maestro/flows/002_browse_lessons_test.yaml +++ b/.maestro/flows/002_browse_lessons_test.yaml @@ -57,12 +57,11 @@ onFlowComplete: - assertVisible: "School Playlists" - assertVisible: "My Playlists" - assertVisible: "No playlists yet" -- tapOn: - id: "floating_action_button" # + Playlist +- tapOn: "Playlist" - assertVisible: "Add new" - assertVisible: "Add from a link" - tapOn: - id: "Add new" + text: "Add new" - assertVisible: id: "app_title" text: "Add playlist" From fbe93a3f805490cb75450c6a63849146732182fc Mon Sep 17 00:00:00 2001 From: pooja Date: Tue, 16 Dec 2025 13:07:32 +0400 Subject: [PATCH 15/58] test updates --- .../003_admin_user_assigns_assignment_to_a_class_test.yaml | 2 -- .maestro/flows/subflows/admin_add_app.yaml | 3 ++- .maestro/flows/subflows/admin_add_app_and_teacher.yaml | 3 ++- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml index 51f467221..5e22adb55 100644 --- a/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml +++ b/.maestro/flows/003_admin_user_assigns_assignment_to_a_class_test.yaml @@ -68,8 +68,6 @@ onFlowComplete: file: "scripts/setDate.js" - inputText: ${output.currentTime} - tapOn: "Lesson/assessment" -- assertVisible: - id: "app_title" - tapOn: "My app" - tapOn: "Grade 1" - tapOn: "Lesson 001" diff --git a/.maestro/flows/subflows/admin_add_app.yaml b/.maestro/flows/subflows/admin_add_app.yaml index e5a4be4be..d79e10ebb 100644 --- a/.maestro/flows/subflows/admin_add_app.yaml +++ b/.maestro/flows/subflows/admin_add_app.yaml @@ -3,7 +3,8 @@ appId: world.respect.app --- - assertVisible: id: "app_title" - text: "Apps" + text: "Home" +- tapOn: "Apps" - tapOn: id: "floating_action_button" - tapOn: "Add from Link" diff --git a/.maestro/flows/subflows/admin_add_app_and_teacher.yaml b/.maestro/flows/subflows/admin_add_app_and_teacher.yaml index ae4f0c126..0623afbbe 100644 --- a/.maestro/flows/subflows/admin_add_app_and_teacher.yaml +++ b/.maestro/flows/subflows/admin_add_app_and_teacher.yaml @@ -5,7 +5,8 @@ appId: world.respect.app - runFlow: "school_admin_login_flow.yaml" - assertVisible: id: "app_title" - text: "Apps" + text: "Home" +- tapOn: "Apps" - tapOn: id: "floating_action_button" - tapOn: "Add from Link" From 729f234adf5d251a43d630de348fa282ef6de06f Mon Sep 17 00:00:00 2001 From: pooja Date: Tue, 16 Dec 2025 13:22:12 +0400 Subject: [PATCH 16/58] test updates --- .maestro/flows/002_browse_lessons_test.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.maestro/flows/002_browse_lessons_test.yaml b/.maestro/flows/002_browse_lessons_test.yaml index 42cf96d33..fa544b1f8 100644 --- a/.maestro/flows/002_browse_lessons_test.yaml +++ b/.maestro/flows/002_browse_lessons_test.yaml @@ -119,5 +119,9 @@ onFlowComplete: id: "assign_btn" - assertVisible: id: "delete_btn" +- tapOn: "Lesson 001" +- assertVisible: "Lesson 001" +- assertVisible: "App name" +- assertVisible: "Open" From ee17ff3fc4681c5c55f7bfd675ce9d49ae75859a Mon Sep 17 00:00:00 2001 From: pooja Date: Tue, 16 Dec 2025 17:07:18 +0400 Subject: [PATCH 17/58] test updates --- .maestro/flows/subflows/admin_add_app_and_teacher.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.maestro/flows/subflows/admin_add_app_and_teacher.yaml b/.maestro/flows/subflows/admin_add_app_and_teacher.yaml index 0623afbbe..7a106a51a 100644 --- a/.maestro/flows/subflows/admin_add_app_and_teacher.yaml +++ b/.maestro/flows/subflows/admin_add_app_and_teacher.yaml @@ -17,7 +17,7 @@ appId: world.respect.app id: "app_title" text: "App detail" - tapOn: "Add App" -- tapOn: "Apps" +- tapOn: "Home" - assertVisible: "My app" # Admin add new class From 455063a4dd954469c75b86280ed8eeb86960316c Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Thu, 18 Dec 2025 08:58:28 +0530 Subject: [PATCH 18/58] Modified the learning unit detail screen --- .../view/apps/launcher/AppLauncherScreen.kt | 5 +- .../detail/LearningUnitDetailScreen.kt | 418 ++++++++++++++---- .../composeResources/values/strings.xml | 8 +- .../respect/shared/navigation/AppRoutes.kt | 46 +- .../apps/launcher/AppLauncherViewModel.kt | 6 +- .../edit/CurriculumMappingEditViewModel.kt | 10 +- .../detail/LearningUnitDetailViewModel.kt | 281 ++++++++++-- 7 files changed, 632 insertions(+), 142 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/launcher/AppLauncherScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/launcher/AppLauncherScreen.kt index 27b02a97c..1d401a9a9 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/launcher/AppLauncherScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/launcher/AppLauncherScreen.kt @@ -63,6 +63,7 @@ import world.respect.datalayer.compatibleapps.model.RespectAppManifest import world.respect.datalayer.ext.dataOrNull import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.add +import world.respect.shared.generated.resources.add_from_a_link import world.respect.shared.generated.resources.add_from_link import world.respect.shared.generated.resources.add_new import world.respect.shared.generated.resources.add_playlist @@ -363,11 +364,11 @@ private fun PlaylistsTabContent( ) { Icon( Icons.Filled.Link, - contentDescription = stringResource(Res.string.add_from_link), + contentDescription = stringResource(Res.string.add_from_a_link), modifier = Modifier.size(20.dp) ) Text( - text = stringResource(Res.string.add_from_link), + text = stringResource(Res.string.add_from_a_link), fontSize = 14.sp, fontWeight = FontWeight.Medium ) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt index ba44292a5..c730e664a 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt @@ -2,54 +2,42 @@ package world.respect.app.view.learningunit.detail import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Android -import androidx.compose.material.icons.filled.NearMe -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.ustadmobile.libcache.PublicationPinState import com.ustadmobile.libuicompose.theme.black import com.ustadmobile.libuicompose.theme.white +import kotlinx.coroutines.flow.Flow import org.jetbrains.compose.resources.stringResource -import world.respect.shared.generated.resources.Res -import world.respect.shared.generated.resources.app_name -import world.respect.shared.viewmodel.learningunit.detail.LearningUnitDetailViewModel -import androidx.compose.ui.graphics.vector.ImageVector -import world.respect.shared.generated.resources.assign -import world.respect.shared.generated.resources.download -import world.respect.shared.generated.resources.open -import world.respect.shared.viewmodel.app.appstate.getTitle -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.ui.layout.ContentScale -import com.ustadmobile.libcache.PublicationPinState import world.respect.app.app.RespectAsyncImage import world.respect.app.components.RespectOfflineItemStatusIcon import world.respect.app.components.RespectQuickActionButton -import world.respect.shared.generated.resources.cancel -import world.respect.shared.generated.resources.downloaded +import world.respect.app.components.defaultItemPadding +import world.respect.datalayer.DataLoadState +import world.respect.datalayer.DataLoadingState +import world.respect.datalayer.ext.dataOrNull +import world.respect.shared.generated.resources.* +import world.respect.shared.viewmodel.app.appstate.getTitle +import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingSectionUiState +import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMappingSectionLink import world.respect.shared.viewmodel.learningunit.detail.LearningUnitDetailUiState +import world.respect.shared.viewmodel.learningunit.detail.LearningUnitDetailViewModel @Composable fun LearningUnitDetailScreen( @@ -57,44 +45,52 @@ fun LearningUnitDetailScreen( ) { val uiState by viewModel.uiState.collectAsState() - LearningUnitDetailScreen( - uiState = uiState, - onClickOpen = viewModel::onClickOpen, - onClickDownload = viewModel::onClickDownload, - onClickAssign = viewModel::onClickAssign, - ) + // Choose which screen to show based on whether we have a mapping + if (uiState.mapping != null) { + PlaylistDetailScreen( + uiState = uiState, + onClickLesson = viewModel::onClickLesson, + onClickEdit = viewModel::onClickEdit, + onClickAssign = viewModel::onClickAssign, + onClickShare = viewModel::onClickShare, + onClickCopy = viewModel::onClickCopy, + onClickDelete = viewModel::onClickDelete, + ) + } else { + SingleLessonDetailScreen( + uiState = uiState, + onClickOpen = viewModel::onClickOpen, + onClickDownload = viewModel::onClickDownload, + onClickAssign = viewModel::onClickAssign, + ) + } } +// Original single lesson screen @Composable -fun LearningUnitDetailScreen( +private fun SingleLessonDetailScreen( uiState: LearningUnitDetailUiState, onClickOpen: () -> Unit, onClickDownload: () -> Unit, onClickAssign: () -> Unit, ) { - LazyColumn( modifier = Modifier .fillMaxSize() .padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - item { ListItem( leadingContent = { val iconUrl = uiState.lessonDetail?.images?.firstOrNull()?.href - iconUrl.also { icon -> - RespectAsyncImage( - uri = icon, - contentDescription = "", - contentScale = ContentScale.Crop, - modifier = Modifier - .size(120.dp) - - ) - } + RespectAsyncImage( + uri = iconUrl, + contentDescription = "", + contentScale = ContentScale.Crop, + modifier = Modifier.size(120.dp) + ) }, headlineContent = { Text( @@ -104,8 +100,7 @@ fun LearningUnitDetailScreen( }, supportingContent = { Column( - verticalArrangement = - Arrangement.spacedBy(4.dp) + verticalArrangement = Arrangement.spacedBy(4.dp) ) { Row( verticalAlignment = Alignment.CenterVertically @@ -133,10 +128,8 @@ fun LearningUnitDetailScreen( } Text( - text = uiState.lessonDetail?.metadata?.subtitle - ?.getTitle().orEmpty() + text = uiState.lessonDetail?.metadata?.subtitle?.getTitle().orEmpty() ) - } } ) @@ -144,9 +137,7 @@ fun LearningUnitDetailScreen( item { Button( - onClick = { - onClickOpen() - }, + onClick = onClickOpen, modifier = Modifier.fillMaxWidth() ) { Text(stringResource(Res.string.open)) @@ -184,27 +175,302 @@ fun LearningUnitDetailScreen( } } } +@Composable +private fun PlaylistDetailScreen( + uiState: LearningUnitDetailUiState, + onClickLesson: (CurriculumMappingSectionLink) -> Unit, + onClickEdit: () -> Unit, + onClickAssign: () -> Unit, + onClickShare: () -> Unit, + onClickCopy: () -> Unit, + onClickDelete: () -> Unit, +) { + var expandedSections by remember { mutableStateOf(setOf()) } + val mapping = uiState.mapping + + Box(modifier = Modifier.fillMaxSize()) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(bottom = 88.dp) + ) { + item("header") { + Card( + modifier = Modifier + .fillMaxWidth() + .defaultItemPadding(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface + ) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.Top + ) { + Box( + modifier = Modifier + .size(56.dp) + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant), + contentAlignment = Alignment.Center + ) { + Icon( + Icons.Default.Book, + contentDescription = null, + modifier = Modifier.size(32.dp) + ) + } + + if (!mapping?.description.isNullOrEmpty()) { + Text( + text = mapping?.description.orEmpty(), + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurface, + lineHeight = 18.sp, + modifier = Modifier.weight(1f) + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + ActionButton( + icon = Icons.Default.Share, + label = stringResource(Res.string.share), + onClick = onClickShare + ) + ActionButton( + icon = Icons.Default.ContentCopy, + label = stringResource(Res.string.copy), + onClick = onClickCopy + ) + ActionButton( + icon = Icons.Default.Task, + label = stringResource(Res.string.assign), + onClick = onClickAssign, + ) + ActionButton( + icon = Icons.Default.Delete, + label = stringResource(Res.string.delete), + onClick = onClickDelete + ) + } + } + } + } + + mapping?.sections?.forEachIndexed { sectionIndex, section -> + item(key = "section_${section.uid}") { + val isExpanded = expandedSections.contains(section.uid) + + Card( + modifier = Modifier + .fillMaxWidth() + .defaultItemPadding() + .clickable { + expandedSections = if (isExpanded) { + expandedSections - section.uid + } else { + expandedSections + section.uid + } + }, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = section.title, + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.weight(1f) + ) + + IconButton( + onClick = onClickAssign, + modifier = Modifier.size(40.dp) + ) { + Icon( + Icons.Default.Task, + contentDescription = stringResource(Res.string.assign), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + IconButton( + onClick = { + expandedSections = if (isExpanded) { + expandedSections - section.uid + } else { + expandedSections + section.uid + } + } + ) { + Icon( + if (isExpanded) Icons.Default.KeyboardArrowUp + else Icons.Default.KeyboardArrowDown, + contentDescription = null + ) + } + } + } + } + + if (expandedSections.contains(section.uid)) { + itemsIndexed( + items = section.items, + key = { linkIndex, _ -> "lesson_${section.uid}_${linkIndex}" } + ) { _, link -> + LessonListItem( + link = link, + sectionLinkUiState = uiState.sectionLinkUiState, + onClickLesson = onClickLesson + ) + } + } + } + } + + if (uiState.showEditButton) { + FloatingActionButton( + onClick = onClickEdit, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(16.dp), + shape = RoundedCornerShape(16.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + Icons.Filled.Edit, + contentDescription = stringResource(Res.string.edit), + modifier = Modifier.size(20.dp) + ) + Text( + text = stringResource(Res.string.edit), + fontSize = 14.sp, + fontWeight = FontWeight.Medium + ) + } + } + } + } +} @Composable -private fun IconLabel(icon: ImageVector, labelRes: String) { - Column( - horizontalAlignment = Alignment.CenterHorizontally +private fun LessonListItem( + link: CurriculumMappingSectionLink, + sectionLinkUiState: (CurriculumMappingSectionLink) -> Flow>, + onClickLesson: (CurriculumMappingSectionLink) -> Unit +) { + val stateFlow = remember(link.href) { + sectionLinkUiState(link) + } + val linkUiState by stateFlow.collectAsState(initial = DataLoadingState()) + val linkData = linkUiState.dataOrNull() + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onClickLesson(link) } + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary - ) + linkData?.icon?.let { iconUrl -> + Box( + modifier = Modifier + .size(48.dp) + .clip(RoundedCornerShape(4.dp)) + ) { + RespectAsyncImage( + uri = iconUrl.toString(), + contentDescription = "", + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + } + } ?: run { + Box( + modifier = Modifier + .size(48.dp) + .clip(RoundedCornerShape(4.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant), + contentAlignment = Alignment.Center + ) { + Icon( + Icons.Default.Android, + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } - Spacer( - modifier = Modifier - .height(4.dp) - ) + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = linkData?.title ?: link.title.orEmpty(), + fontSize = 15.sp, + fontWeight = FontWeight.Medium + ) + + if (linkData?.subtitle?.isNotEmpty() == true) { + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = linkData.subtitle, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } +} +@Composable +private fun ActionButton( + icon: ImageVector, + label: String, + onClick: () -> Unit, + enabled: Boolean = true +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + IconButton( + onClick = onClick, + modifier = Modifier.size(40.dp), + enabled = enabled + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } Text( - text = labelRes + text = label, + fontSize = 10.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + lineHeight = 12.sp ) - } -} +} \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml index 4ba7408bf..2b39ebf55 100644 --- a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml +++ b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml @@ -378,7 +378,7 @@ Edit mapping Textbooks Chapter - Lesson + Item Add book cover No sections have been added yet. Click + Section to add one. Map @@ -460,13 +460,13 @@ Edit enrollment - Section name + Section title Playlists School Playlists My Playlists No playlists yet Create your first playlist to get started - Create Playlist + Add playlist Error loading playlists Add new Add from a link @@ -476,5 +476,7 @@ Created by: Edit playlist Home + copy playlist + %1$d sections • %2$d items diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index 74c2746fb..c51210a2d 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -511,22 +511,33 @@ class CreateAccount( * @property expectedIdentifier (optional), where a refererUrl is provided, to use cached feed * metadata as above, the identifier of the publication within the feed. */ + @Serializable -class LearningUnitDetail( +class LearningUnitDetail ( private val learningUnitManifestUrlStr: String, private val appManifestUrlStr: String, private val refererUrlStr: String? = null, - val expectedIdentifier: String? = null + val expectedIdentifier: String? = null, + private val mappingDataJson: String? = null ) : RespectAppRoute { - @Transient - val learningUnitManifestUrl = Url(learningUnitManifestUrlStr) + val learningUnitManifestUrl: Url + get() = Url(learningUnitManifestUrlStr) - @Transient - val refererUrl = refererUrlStr?.let { Url(it) } + val appManifestUrl: Url + get() = Url(appManifestUrlStr) - @Transient - val appManifestUrl = Url(appManifestUrlStr) + val refererUrl: Url? + get() = refererUrlStr?.let { Url(it) } + + val mappingData: CurriculumMapping? + get() = mappingDataJson?.let { + try { + Json.decodeFromString(CurriculumMapping.serializer(), it) + } catch (e: Exception) { + null + } + } companion object { @@ -540,12 +551,29 @@ class LearningUnitDetail( appManifestUrlStr = appManifestUrl.toString(), refererUrlStr = refererUrl?.toString(), expectedIdentifier = expectedIdentifier, + mappingDataJson = null ) - } + fun createFromMapping(mapping: CurriculumMapping): LearningUnitDetail { + val mappingJson = try { + Json.encodeToString(CurriculumMapping.serializer(), mapping) + } catch (e: Exception) { + null + } + return LearningUnitDetail( + learningUnitManifestUrlStr = "", + appManifestUrlStr = "", + refererUrlStr = null, + expectedIdentifier = null, + mappingDataJson = mappingJson + ) + } + } } + + @Serializable class LearningUnitViewer( private val learningUnitIdStr: String, diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt index f0b35ec42..b0b2a1857 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt @@ -42,6 +42,7 @@ import world.respect.shared.navigation.RespectAppLauncher import world.respect.shared.navigation.RespectAppList import world.respect.shared.navigation.CurriculumMappingEdit import world.respect.shared.navigation.EnterLink +import world.respect.shared.navigation.LearningUnitDetail import world.respect.shared.navigation.NavResultReturner import world.respect.shared.resources.UiText import world.respect.shared.util.ext.asUiText @@ -260,10 +261,7 @@ class AppLauncherViewModel( fun onClickMapping(mapping: CurriculumMapping) { _navCommandFlow.tryEmit( NavCommand.Navigate( - CurriculumMappingEdit.create( - uid = mapping.uid, - mappingData = mapping - ) + LearningUnitDetail.createFromMapping(mapping) ) ) } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt index 12ce1a6ca..f30993104 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt @@ -22,6 +22,7 @@ import world.respect.libutil.ext.moveItem import world.respect.libutil.ext.updateAtIndex import world.respect.libutil.ext.resolve import world.respect.shared.generated.resources.Res +import world.respect.shared.generated.resources.create_playlist import world.respect.shared.generated.resources.edit_mapping import world.respect.shared.generated.resources.edit_playlist import world.respect.shared.generated.resources.required_field @@ -68,6 +69,9 @@ data class CurriculumMappingEditUiState( data class CurriculumMappingSectionUiState( val icon: Url? = null, + val title: String = "", + val subtitle: String = "", + val description: String = "", ) class CurriculumMappingEditViewModel( @@ -94,7 +98,11 @@ class CurriculumMappingEditViewModel( init { _appUiState.update { prev -> prev.copy( - title = Res.string.edit_playlist.asUiText(), + title = if (mappingData == null) { + Res.string.create_playlist.asUiText() + } else { + Res.string.edit_playlist.asUiText() + }, userAccountIconVisible = false, actionBarButtonState = ActionBarButtonUiState( visible = true, diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt index cb968c9f7..20c98eaf7 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt @@ -5,12 +5,13 @@ import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute import com.ustadmobile.libcache.PublicationPinState import com.ustadmobile.libcache.UstadCache +import io.ktor.http.Url +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import world.respect.shared.navigation.LearningUnitDetail -import world.respect.shared.viewmodel.RespectViewModel import world.respect.datalayer.DataLoadParams import world.respect.datalayer.DataLoadState import world.respect.datalayer.DataLoadingState @@ -18,15 +19,25 @@ import world.respect.datalayer.DataReadyState import world.respect.datalayer.RespectAppDataSource import world.respect.datalayer.compatibleapps.model.RespectAppManifest import world.respect.datalayer.ext.dataOrNull -import world.respect.lib.opds.model.OpdsPublication +import world.respect.datalayer.ext.map import world.respect.datalayer.respect.model.LEARNING_UNIT_MIME_TYPES +import world.respect.lib.opds.model.OpdsPublication +import world.respect.lib.opds.model.findIcons import world.respect.libutil.ext.resolve import world.respect.shared.domain.launchapp.LaunchAppUseCase import world.respect.shared.navigation.AssignmentEdit +import world.respect.shared.navigation.CurriculumMappingEdit +import world.respect.shared.navigation.LearningUnitDetail import world.respect.shared.navigation.NavCommand +import world.respect.shared.navigation.NavResultReturner import world.respect.shared.util.ext.asUiText import world.respect.shared.util.ext.resolve +import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.getTitle +import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel +import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingSectionUiState +import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping +import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMappingSectionLink import world.respect.shared.viewmodel.learningunit.LearningUnitSelection data class LearningUnitDetailUiState( @@ -35,9 +46,16 @@ data class LearningUnitDetailUiState( val pinState: PublicationPinState = PublicationPinState( PublicationPinState.Status.NOT_PINNED, 0, 0 ), + val mapping: CurriculumMapping? = null, + val sectionLinkUiState: (CurriculumMappingSectionLink) -> Flow> = { + kotlinx.coroutines.flow.emptyFlow() + }, ) { val buttonsEnabled: Boolean - get() = lessonDetail != null + get() = lessonDetail != null || mapping != null + + val showEditButton: Boolean + get() = mapping != null } class LearningUnitDetailViewModel( @@ -45,6 +63,7 @@ class LearningUnitDetailViewModel( private val appDataSource: RespectAppDataSource, private val launchAppUseCase: LaunchAppUseCase, private val ustadCache: UstadCache, + private val resultReturner: NavResultReturner, ) : RespectViewModel(savedStateHandle) { private val _uiState = MutableStateFlow(LearningUnitDetailUiState()) @@ -55,52 +74,87 @@ class LearningUnitDetailViewModel( init { viewModelScope.launch { - appDataSource.opdsDataSource.loadOpdsPublication( - url = route.learningUnitManifestUrl, - params = DataLoadParams(), - referrerUrl = route.learningUnitManifestUrl, - expectedPublicationId = route.expectedIdentifier + resultReturner.filteredResultFlowForKey( + CurriculumMappingEditViewModel.KEY_SAVED_MAPPING ).collect { result -> - when (result) { - is DataReadyState -> { - _uiState.update { - it.copy( - lessonDetail = result.data.resolve( - route.learningUnitManifestUrl + val savedMapping = result.result as? CurriculumMapping + if (savedMapping != null) { + _uiState.update { + it.copy( + mapping = savedMapping, + sectionLinkUiState = this@LearningUnitDetailViewModel::sectionLinkUiStateFor + ) + } + _appUiState.update { + it.copy( + title = savedMapping.title.asUiText() + ) + } + } + } + } + + val mappingData = route.mappingData + + if (mappingData != null) { + _uiState.update { + it.copy( + mapping = mappingData, + sectionLinkUiState = this@LearningUnitDetailViewModel::sectionLinkUiStateFor + ) + } + _appUiState.update { + it.copy( + title = mappingData.title.asUiText() + ) + } + } else { + viewModelScope.launch { + appDataSource.opdsDataSource.loadOpdsPublication( + url = route.learningUnitManifestUrl, + params = DataLoadParams(), + referrerUrl = route.learningUnitManifestUrl, + expectedPublicationId = route.expectedIdentifier + ).collect { result -> + when (result) { + is DataReadyState -> { + _uiState.update { + it.copy( + lessonDetail = result.data.resolve( + route.learningUnitManifestUrl + ) ) - ) - } + } - _appUiState.update { - it.copy( - title = result.data.metadata.title.getTitle().asUiText() - ) + _appUiState.update { + it.copy( + title = result.data.metadata.title.getTitle().asUiText() + ) + } + } + else -> { } - } - else -> { } } } - } - viewModelScope.launch { - appDataSource.compatibleAppsDataSource.getAppAsFlow( - manifestUrl = route.appManifestUrl, - loadParams = DataLoadParams() - ).collect { app -> - _uiState.update { it.copy(app = app) } + viewModelScope.launch { + appDataSource.compatibleAppsDataSource.getAppAsFlow( + manifestUrl = route.appManifestUrl, + loadParams = DataLoadParams() + ).collect { app -> + _uiState.update { it.copy(app = app) } + } } - } - viewModelScope.launch { - ustadCache.publicationPinState(route.learningUnitManifestUrl).collect { pinState -> - _uiState.update { it.copy(pinState = pinState) } + viewModelScope.launch { + ustadCache.publicationPinState(route.learningUnitManifestUrl).collect { pinState -> + _uiState.update { it.copy(pinState = pinState) } + } } } - } - fun onClickOpen() { val respectApp = _uiState.value.app.dataOrNull() ?: return val launchLink = _uiState.value.lessonDetail?.links?.firstOrNull { link -> @@ -133,27 +187,160 @@ class LearningUnitDetailViewModel( //Do nothing } } - - }catch(t: Throwable) { + } catch(t: Throwable) { t.printStackTrace() } } } fun onClickAssign() { - val publicationVal = uiState.value.lessonDetail ?: return + val mapping = uiState.value.mapping + + if (mapping != null) { + val firstLesson = mapping.sections.firstOrNull()?.items?.firstOrNull() + + if (firstLesson != null && firstLesson.appManifestUrl != null) { + viewModelScope.launch { + appDataSource.opdsDataSource.loadOpdsPublication( + url = Url(firstLesson.href), + params = DataLoadParams(), + referrerUrl = null, + expectedPublicationId = null, + ).collect { publicationState -> + val publication = (publicationState as? DataReadyState)?.data + if (publication != null) { + _navCommandFlow.tryEmit( + NavCommand.Navigate( + destination = AssignmentEdit.create( + uid = null, + learningUnitSelected = LearningUnitSelection( + learningUnitManifestUrl = Url(firstLesson.href), + selectedPublication = publication, + appManifestUrl = firstLesson.appManifestUrl!! + ) + ) + ) + ) + } + } + } + } else { + _navCommandFlow.tryEmit( + NavCommand.Navigate( + destination = AssignmentEdit.create( + uid = null, + learningUnitSelected = null + ) + ) + ) + } + } else { + val publicationVal = uiState.value.lessonDetail ?: return + _navCommandFlow.tryEmit( + NavCommand.Navigate( + destination = AssignmentEdit.create( + uid = null, + learningUnitSelected = LearningUnitSelection( + learningUnitManifestUrl = route.learningUnitManifestUrl, + selectedPublication = publicationVal, + appManifestUrl = route.appManifestUrl + ) + ) + ) + ) + } + } + + fun onClickLesson(link: CurriculumMappingSectionLink) { + viewModelScope.launch { + val appManifestUrl = link.appManifestUrl + if (appManifestUrl == null) { + return@launch + } + appDataSource.compatibleAppsDataSource.getAppAsFlow( + manifestUrl = appManifestUrl, + loadParams = DataLoadParams() + ).collect { appState -> + val app = appState.dataOrNull() + if (app != null) { + appDataSource.opdsDataSource.loadOpdsPublication( + url = Url(link.href), + params = DataLoadParams(), + referrerUrl = null, + expectedPublicationId = null, + ).collect { publicationState -> + val publication = (publicationState as? DataReadyState)?.data + if (publication != null) { + val launchLink = publication.links.firstOrNull { pubLink -> + pubLink.rel?.any { + it.startsWith("http://opds-spec.org/acquisition") + } == true && LEARNING_UNIT_MIME_TYPES.any { + pubLink.type?.startsWith(it) == true + } + } + + if (launchLink != null) { + val launchUrl = Url(link.href).resolve(launchLink.href) + launchAppUseCase( + app = app, + learningUnitId = launchUrl, + navigateFn = { + _navCommandFlow.tryEmit(it) + } + ) + } + } + } + } + } + } + } + + fun onClickEdit() { + val mapping = _uiState.value.mapping ?: return _navCommandFlow.tryEmit( NavCommand.Navigate( - destination = AssignmentEdit.create( - uid = null, - learningUnitSelected = LearningUnitSelection( - learningUnitManifestUrl = route.learningUnitManifestUrl, - selectedPublication = publicationVal, - appManifestUrl = route.appManifestUrl - ) + CurriculumMappingEdit.create( + uid = mapping.uid, + mappingData = mapping ) ) ) } -} + + fun onClickShare() { + // TODO: Implement share functionality + } + + fun onClickCopy() { + // TODO: Implement copy functionality + } + + fun onClickDelete() { + // TODO: Implement delete functionality + } + + fun sectionLinkUiStateFor( + link: CurriculumMappingSectionLink + ): Flow> { + val publicationUrl = Url(link.href) + return appDataSource.opdsDataSource.loadOpdsPublication( + url = publicationUrl, + params = DataLoadParams(), + referrerUrl = null, + expectedPublicationId = null, + ).map { opdsLoadState -> + opdsLoadState.map { publication -> + CurriculumMappingSectionUiState( + icon = publication.findIcons().firstOrNull()?.let { + publicationUrl.resolve(it.href) + }, + title = publication.metadata.title.getTitle(), + subtitle = publication.metadata.subtitle?.getTitle().orEmpty(), + description = publication.metadata.description.orEmpty() + ) + } + } + } +} \ No newline at end of file From 715976a48eab622a4fd697dcdd4e590cc38bad77 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Thu, 18 Dec 2025 14:25:28 +0530 Subject: [PATCH 19/58] Modify the Test --- .maestro/flows/002_browse_lessons_test.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.maestro/flows/002_browse_lessons_test.yaml b/.maestro/flows/002_browse_lessons_test.yaml index fa544b1f8..533aefb38 100644 --- a/.maestro/flows/002_browse_lessons_test.yaml +++ b/.maestro/flows/002_browse_lessons_test.yaml @@ -71,10 +71,10 @@ onFlowComplete: - inputText: "Playlist - Grade 1" - tapOn: "Description" - inputText: "Test list" -- tapOn: "Subject" -- tapOn: "English" -- tapOn: "Grade" -- tapOn: "Grade 1" +#- tapOn: "Subject" # Need to Implement +#- tapOn: "English" +#- tapOn: "Grade" +#- tapOn: "Grade 1" - tapOn: "Section" - tapOn: "Section title" - inputText: "Day 1" From 8e2fe375eecea63109e8ff621989518d9fdb90bf Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Fri, 19 Dec 2025 08:57:56 +0530 Subject: [PATCH 20/58] add onclicklesson function --- .../detail/LearningUnitDetailScreen.kt | 4 -- .../detail/LearningUnitDetailViewModel.kt | 57 +++++-------------- 2 files changed, 14 insertions(+), 47 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt index c730e664a..c2a88c713 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt @@ -44,8 +44,6 @@ fun LearningUnitDetailScreen( viewModel: LearningUnitDetailViewModel ) { val uiState by viewModel.uiState.collectAsState() - - // Choose which screen to show based on whether we have a mapping if (uiState.mapping != null) { PlaylistDetailScreen( uiState = uiState, @@ -65,8 +63,6 @@ fun LearningUnitDetailScreen( ) } } - -// Original single lesson screen @Composable private fun SingleLessonDetailScreen( uiState: LearningUnitDetailUiState, diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt index 20c98eaf7..3455bf3a1 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt @@ -9,6 +9,7 @@ import io.ktor.http.Url import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -48,7 +49,7 @@ data class LearningUnitDetailUiState( ), val mapping: CurriculumMapping? = null, val sectionLinkUiState: (CurriculumMappingSectionLink) -> Flow> = { - kotlinx.coroutines.flow.emptyFlow() + emptyFlow() }, ) { val buttonsEnabled: Boolean @@ -252,49 +253,19 @@ class LearningUnitDetailViewModel( } fun onClickLesson(link: CurriculumMappingSectionLink) { - viewModelScope.launch { - val appManifestUrl = link.appManifestUrl - if (appManifestUrl == null) { - return@launch - } - - appDataSource.compatibleAppsDataSource.getAppAsFlow( - manifestUrl = appManifestUrl, - loadParams = DataLoadParams() - ).collect { appState -> - val app = appState.dataOrNull() - if (app != null) { - appDataSource.opdsDataSource.loadOpdsPublication( - url = Url(link.href), - params = DataLoadParams(), - referrerUrl = null, - expectedPublicationId = null, - ).collect { publicationState -> - val publication = (publicationState as? DataReadyState)?.data - if (publication != null) { - val launchLink = publication.links.firstOrNull { pubLink -> - pubLink.rel?.any { - it.startsWith("http://opds-spec.org/acquisition") - } == true && LEARNING_UNIT_MIME_TYPES.any { - pubLink.type?.startsWith(it) == true - } - } + val publicationUrl = Url(link.href) + val appManifestUrl = link.appManifestUrl ?: return - if (launchLink != null) { - val launchUrl = Url(link.href).resolve(launchLink.href) - launchAppUseCase( - app = app, - learningUnitId = launchUrl, - navigateFn = { - _navCommandFlow.tryEmit(it) - } - ) - } - } - } - } - } - } + _navCommandFlow.tryEmit( + NavCommand.Navigate( + LearningUnitDetail.create( + learningUnitManifestUrl = publicationUrl, + appManifestUrl = appManifestUrl, + refererUrl = publicationUrl, + expectedIdentifier = null + ) + ) + ) } fun onClickEdit() { From e2b5b9142be76b03bd54e74fb93239e2b100ec1c Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Mon, 22 Dec 2025 11:15:37 +0530 Subject: [PATCH 21/58] fix the screen --- .../curriculum/mapping/edit/CurriculumMappingEditScreen.kt | 2 +- .../app/view/learningunit/detail/LearningUnitDetailScreen.kt | 3 ++- .../learningunit/detail/LearningUnitDetailViewModel.kt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt index b584793bb..27419c56c 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt @@ -95,7 +95,7 @@ fun CurriculumMappingEditScreen( val reorderableLazyListState = rememberReorderableLazyListState( lazyListState = lazyListState, onMove = { from, to -> - val headerItemCount = 4 + val headerItemCount = 4 //TODO: This MUST be explained val fromIndex = from.index - headerItemCount val toIndex = to.index - headerItemCount diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt index c2a88c713..35323f7db 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt @@ -124,7 +124,8 @@ private fun SingleLessonDetailScreen( } Text( - text = uiState.lessonDetail?.metadata?.subtitle?.getTitle().orEmpty() + text = uiState.lessonDetail?.metadata?.subtitle + ?.getTitle().orEmpty() ) } } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt index 3455bf3a1..9417a8c28 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt @@ -217,7 +217,7 @@ class LearningUnitDetailViewModel( learningUnitSelected = LearningUnitSelection( learningUnitManifestUrl = Url(firstLesson.href), selectedPublication = publication, - appManifestUrl = firstLesson.appManifestUrl!! + appManifestUrl = firstLesson.appManifestUrl ) ) ) From d3ad0510d3a818dd8443ad88e4a38734d64e21bb Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Mon, 22 Dec 2025 11:52:53 +0530 Subject: [PATCH 22/58] add the testtag for additem. --- .../curriculum/mapping/edit/CurriculumMappingEditScreen.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt index 27419c56c..2683406f2 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt @@ -388,7 +388,8 @@ private fun SectionItem( ) { OutlinedButton( onClick = { onClickAddLesson(sectionIndex) }, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth() + .testTag("add_item"), enabled = !isDragging ) { Icon( From 00cd2c038e52acfee5aa81fbc0a60a918f42c61f Mon Sep 17 00:00:00 2001 From: pooja Date: Mon, 22 Dec 2025 11:14:53 +0400 Subject: [PATCH 23/58] update --- .maestro/flows/002_browse_lessons_test.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.maestro/flows/002_browse_lessons_test.yaml b/.maestro/flows/002_browse_lessons_test.yaml index 533aefb38..3e21eec62 100644 --- a/.maestro/flows/002_browse_lessons_test.yaml +++ b/.maestro/flows/002_browse_lessons_test.yaml @@ -39,8 +39,6 @@ onFlowComplete: - tapOn: "Apps" - assertVisible: "My app" - tapOn: "My app" -- assertVisible: "Lessons" -- tapOn: "Lessons" - tapOn: "Grade 1" - tapOn: "Lesson 001" - assertVisible: "Lesson 001" From 4191723c0db71013e97518129433e0321f6583ed Mon Sep 17 00:00:00 2001 From: pooja Date: Mon, 22 Dec 2025 11:59:21 +0400 Subject: [PATCH 24/58] update --- .maestro/flows/002_browse_lessons_test.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.maestro/flows/002_browse_lessons_test.yaml b/.maestro/flows/002_browse_lessons_test.yaml index 3e21eec62..da77f8d50 100644 --- a/.maestro/flows/002_browse_lessons_test.yaml +++ b/.maestro/flows/002_browse_lessons_test.yaml @@ -80,7 +80,6 @@ onFlowComplete: - tapOn: id: "add_item" - tapOn: "My app" -- tapOn: "Lessons" - tapOn: "Grade 1" - tapOn: "Lesson 001" - assertVisible: From a2987eece1faa4f235bfe211a7880a1a51f82636 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Tue, 23 Dec 2025 10:12:14 +0530 Subject: [PATCH 25/58] change the navigation from playlist tab to learnint unit detail screen --- .../viewmodel/apps/launcher/AppLauncherViewModel.kt | 2 +- .../mapping/edit/CurriculumMappingEditViewModel.kt | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt index b0b2a1857..5bd2662c5 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt @@ -135,7 +135,7 @@ class AppLauncherViewModel( } viewModelScope.launch { - resultReturner.resultFlowForKey( + resultReturner.filteredResultFlowForKey( CurriculumMappingEditViewModel.KEY_SAVED_MAPPING ).collect { result -> val savedMapping = result.result as? CurriculumMapping diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt index f30993104..2c39850ea 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt @@ -341,7 +341,13 @@ class CurriculumMappingEditViewModel( result = mapping ) ) - _navCommandFlow.tryEmit(NavCommand.PopUp()) + _navCommandFlow.tryEmit( + NavCommand.Navigate( + destination = LearningUnitDetail.createFromMapping(mapping), + popUpTo = route, + popUpToInclusive = true + ) + ) } fun onClearError() { From 0b1a604db703afaba63dcc07d63847fabdd602ef Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Tue, 23 Dec 2025 13:36:21 +0530 Subject: [PATCH 26/58] Add the testtag for the expand/collapse --- .../app/view/learningunit/detail/LearningUnitDetailScreen.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt index 35323f7db..73554b4b8 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -314,7 +315,9 @@ private fun PlaylistDetailScreen( } else { expandedSections + section.uid } - } + }, + modifier = Modifier + .testTag("expand_collapse_icon_${section.uid}") ) { Icon( if (isExpanded) Icons.Default.KeyboardArrowUp From 0518afe1cb4b9d88918fe53171b0684e6035357a Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Tue, 23 Dec 2025 15:59:30 +0530 Subject: [PATCH 27/58] Add the testtag for the expand/collapse --- .../app/view/learningunit/detail/LearningUnitDetailScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt index 73554b4b8..23f12316a 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt @@ -317,7 +317,7 @@ private fun PlaylistDetailScreen( } }, modifier = Modifier - .testTag("expand_collapse_icon_${section.uid}") + .testTag("expand_collapse_icon_") ) { Icon( if (isExpanded) Icons.Default.KeyboardArrowUp From dda37748080c0551451255529cf223efffe89710 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Tue, 23 Dec 2025 16:44:54 +0530 Subject: [PATCH 28/58] Add the testtag for the action buttons --- .../detail/LearningUnitDetailScreen.kt | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt index 23f12316a..6484025ed 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt @@ -243,22 +243,27 @@ private fun PlaylistDetailScreen( ActionButton( icon = Icons.Default.Share, label = stringResource(Res.string.share), - onClick = onClickShare + onClick = onClickShare, + modifier = Modifier + .testTag("share_btn") ) ActionButton( icon = Icons.Default.ContentCopy, label = stringResource(Res.string.copy), - onClick = onClickCopy + onClick = onClickCopy, + modifier = Modifier.testTag("copy_btn") ) ActionButton( icon = Icons.Default.Task, label = stringResource(Res.string.assign), onClick = onClickAssign, + modifier = Modifier.testTag("assign_btn") ) ActionButton( icon = Icons.Default.Delete, label = stringResource(Res.string.delete), - onClick = onClickDelete + onClick = onClickDelete, + modifier = Modifier.testTag("delete_btn") ) } } @@ -449,7 +454,8 @@ private fun ActionButton( icon: ImageVector, label: String, onClick: () -> Unit, - enabled: Boolean = true + enabled: Boolean = true, + modifier: Modifier = Modifier ) { Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -457,7 +463,7 @@ private fun ActionButton( ) { IconButton( onClick = onClick, - modifier = Modifier.size(40.dp), + modifier = modifier.size(40.dp), enabled = enabled ) { Icon( From f4542b80aa7354ecf5036dee470f3492c1ebf7c8 Mon Sep 17 00:00:00 2001 From: pooja Date: Tue, 23 Dec 2025 16:53:07 +0400 Subject: [PATCH 29/58] update --- .maestro/flows/002_browse_lessons_test.yaml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.maestro/flows/002_browse_lessons_test.yaml b/.maestro/flows/002_browse_lessons_test.yaml index da77f8d50..d637de16b 100644 --- a/.maestro/flows/002_browse_lessons_test.yaml +++ b/.maestro/flows/002_browse_lessons_test.yaml @@ -91,21 +91,26 @@ onFlowComplete: id: "app_title" text: "Playlist - Grade 1" - assertVisible: "Day 1" +- tapOn: + id: "expand_collapse_icon_" - assertVisible: "Lesson 001" - tapOn: - id: "floating_action_button" # Edit button + text: "Edit" + index: 1 # Edit button - assertVisible: id: "app_title" text: "Edit playlist" - tapOn: "Section" -- tapOn: "Section title" +- tapOn: + text: "Section title" + index: 1 - inputText: "Day 2" - hideKeyboard - tapOn: "Save" - assertVisible: id: "app_title" text: "Playlist - Grade 1" -- assertVisible: "Day 1" +- tapOn: "Day 1" - assertVisible: "Lesson 001" - assertVisible: "Day 2" - assertVisible: From a9caa37797a23083e0dbbc1a2aaab20eb4e055de Mon Sep 17 00:00:00 2001 From: pooja Date: Tue, 23 Dec 2025 17:19:45 +0400 Subject: [PATCH 30/58] update --- .maestro/flows/002_browse_lessons_test.yaml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.maestro/flows/002_browse_lessons_test.yaml b/.maestro/flows/002_browse_lessons_test.yaml index d637de16b..b3958267e 100644 --- a/.maestro/flows/002_browse_lessons_test.yaml +++ b/.maestro/flows/002_browse_lessons_test.yaml @@ -125,5 +125,15 @@ onFlowComplete: - assertVisible: "Lesson 001" - assertVisible: "App name" - assertVisible: "Open" - - +- tapOn: "Home" +- tapOn: "Playlists" +- tapOn: "Playlist - Grade 1" +- assertVisible: + id: "app_title" + text: "Playlist - Grade 1" +- tapOn: + id: "assign_btn" +- assertVisible: + id: "app_title" + text: "Add assignment" +- assertVisible: "Lesson 001" \ No newline at end of file From e1acea4f046535e7a2be67b21a869b230882c008 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Wed, 24 Dec 2025 04:05:07 +0530 Subject: [PATCH 31/58] modify the code --- .../kotlin/world/respect/AppKoinModule.kt | 2 + .../world/respect/app/app/AppNavHost.kt | 1 - .../view/apps/launcher/AppLauncherScreen.kt | 333 +----------------- .../mapping/list/PlaylistListScreen.kt | 288 +++++++++++++++ .../detail/LearningUnitDetailScreen.kt | 4 +- .../apps/launcher/AppLauncherViewModel.kt | 91 +---- .../mapping/list/PlaylistListViewModel.kt | 111 ++++++ .../learningunit/LearningUnitSelection.kt | 10 + .../detail/LearningUnitDetailViewModel.kt | 44 ++- 9 files changed, 468 insertions(+), 416 deletions(-) create mode 100644 respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/PlaylistListScreen.kt create mode 100644 respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/PlaylistListViewModel.kt diff --git a/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt b/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt index 1d8f8d673..f9e9aea62 100644 --- a/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt +++ b/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt @@ -209,6 +209,7 @@ import world.respect.shared.viewmodel.settings.SettingsViewModel import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel import world.respect.shared.viewmodel.schooldirectory.edit.SchoolDirectoryEditViewModel import world.respect.shared.viewmodel.schooldirectory.list.SchoolDirectoryListViewModel +import world.respect.shared.viewmodel.curriculum.mapping.list.PlaylistListViewModel const val SHARED_PREF_SETTINGS_NAME = "respect_settings3_" @@ -326,6 +327,7 @@ val appKoinModule = module { viewModelOf(::AssignmentDetailViewModel) viewModelOf(::EnrollmentListViewModel) viewModelOf(::EnrollmentEditViewModel) + viewModelOf(::PlaylistListViewModel) single { diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt index a7e0f1f81..30a64a34b 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt @@ -538,7 +538,6 @@ fun AppNavHost( ) } - composable { val viewModel: CurriculumMappingEditViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/launcher/AppLauncherScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/launcher/AppLauncherScreen.kt index 1d401a9a9..07698ed5a 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/launcher/AppLauncherScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/launcher/AppLauncherScreen.kt @@ -3,53 +3,18 @@ package world.respect.app.view.apps.launcher import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Book -import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.FilterChip -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.FloatingActionButtonDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Tab -import androidx.compose.material3.TabRow -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.paging.compose.collectAsLazyPagingItems import kotlinx.coroutines.flow.emptyFlow import org.jetbrains.compose.resources.painterResource @@ -57,30 +22,17 @@ import org.jetbrains.compose.resources.stringResource import world.respect.app.app.RespectAsyncImage import world.respect.app.components.respectRememberPager import world.respect.app.components.uiTextStringResource +import world.respect.app.view.curriculum.mapping.list.PlaylistListScreen import world.respect.datalayer.DataLoadState import world.respect.datalayer.NoDataLoadedState import world.respect.datalayer.compatibleapps.model.RespectAppManifest import world.respect.datalayer.ext.dataOrNull -import world.respect.shared.generated.resources.Res -import world.respect.shared.generated.resources.add -import world.respect.shared.generated.resources.add_from_a_link -import world.respect.shared.generated.resources.add_from_link -import world.respect.shared.generated.resources.add_new -import world.respect.shared.generated.resources.add_playlist -import world.respect.shared.generated.resources.all -import world.respect.shared.generated.resources.apps -import world.respect.shared.generated.resources.empty -import world.respect.shared.generated.resources.empty_list -import world.respect.shared.generated.resources.more_info -import world.respect.shared.generated.resources.my_playlists -import world.respect.shared.generated.resources.no_playlists_yet -import world.respect.shared.generated.resources.playlists -import world.respect.shared.generated.resources.remove -import world.respect.shared.generated.resources.school_playlists +import world.respect.shared.generated.resources.* import world.respect.shared.viewmodel.app.appstate.getTitle import world.respect.shared.viewmodel.apps.launcher.AppLauncherUiState import world.respect.shared.viewmodel.apps.launcher.AppLauncherViewModel -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping +import world.respect.shared.viewmodel.curriculum.mapping.list.PlaylistListViewModel + @Composable fun AppLauncherScreen( @@ -92,12 +44,8 @@ fun AppLauncherScreen( uiState = uiState, onClickApp = { viewModel.onClickApp(it) }, onClickRemove = { viewModel.onClickRemove(it) }, - onClickMapping = viewModel::onClickMapping, - onClickMoreOptions = viewModel::onClickMoreOptions, onTabSelected = viewModel::onTabSelected, - onClickMap = viewModel::onClickMap, - onClickAddLink = viewModel::onClickAddLink, - onRemoveMapping = viewModel::removeMapping, + playlistListViewModel = viewModel.playlistListViewModel, ) } @@ -106,26 +54,14 @@ fun AppLauncherScreen( uiState: AppLauncherUiState, onClickApp: (DataLoadState) -> Unit, onClickRemove: (DataLoadState) -> Unit, - onClickMapping: (CurriculumMapping) -> Unit, - onClickMoreOptions: (CurriculumMapping) -> Unit, onTabSelected: (Int) -> Unit, - onClickMap: () -> Unit, - onClickAddLink: () -> Unit, - onRemoveMapping: (CurriculumMapping) -> Unit, + playlistListViewModel: PlaylistListViewModel, ) { - var selectedFilterChipIndex by remember { mutableIntStateOf(0) } - val mainTabs = listOf( stringResource(Res.string.apps), stringResource(Res.string.playlists) ) - val filterChips = listOf( - stringResource(Res.string.all), - stringResource(Res.string.school_playlists), - stringResource(Res.string.my_playlists) - ) - Column( modifier = Modifier.fillMaxSize() ) { @@ -155,15 +91,8 @@ fun AppLauncherScreen( onClickApp = onClickApp, onClickRemove = onClickRemove, ) - 1 -> PlaylistsTabContent( - uiState = uiState, - filterChips = filterChips, - selectedFilterChipIndex = selectedFilterChipIndex, - onFilterChipSelected = { selectedFilterChipIndex = it }, - onClickMapping = onClickMapping, - onClickAdd = onClickMap, - onClickAddLink = onClickAddLink, - onRemoveMapping = onRemoveMapping, + 1 -> PlaylistListScreen( + viewModel = playlistListViewModel ) } } @@ -243,246 +172,6 @@ private fun AppsTabContent( } } -@Composable -private fun PlaylistsTabContent( - uiState: AppLauncherUiState, - filterChips: List, - selectedFilterChipIndex: Int, - onFilterChipSelected: (Int) -> Unit, - onClickMapping: (CurriculumMapping) -> Unit, - onClickAdd: () -> Unit, - onClickAddLink: () -> Unit, - onRemoveMapping: (CurriculumMapping) -> Unit, -) { - var isFabMenuExpanded by remember { mutableStateOf(false) } - - Box(modifier = Modifier.fillMaxSize()) { - Column(modifier = Modifier.fillMaxSize()) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - filterChips.forEachIndexed { index, label -> - FilterChip( - selected = selectedFilterChipIndex == index, - onClick = { onFilterChipSelected(index) }, - label = { Text(label) } - ) - } - } - - Box( - modifier = Modifier - .fillMaxSize() - .weight(1f) - ) { - if (uiState.mappings.isEmpty()) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text( - text = stringResource(Res.string.no_playlists_yet), - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center - ) - } - } else { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = androidx.compose.foundation.layout.PaddingValues( - top = 8.dp, - bottom = 88.dp - ) - ) { - items( - items = uiState.mappings, - key = { mapping -> mapping.uid } - ) { mapping -> - MappingListItem( - mapping = mapping, - onClickMapping = onClickMapping, - onRemoveMapping = onRemoveMapping, - ) - } - } - } - } - } - Column( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(16.dp), - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - if (isFabMenuExpanded) { - FloatingActionButton( - onClick = { - onClickAdd() - isFabMenuExpanded = false - }, - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface, - shape = RoundedCornerShape(16.dp), - elevation = FloatingActionButtonDefaults.elevation(defaultElevation = 6.dp) - ) { - Row( - modifier = Modifier.padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - Icons.Filled.Add, - contentDescription = stringResource(Res.string.add_new), - modifier = Modifier.size(20.dp) - ) - Text( - text = stringResource(Res.string.add_new), - fontSize = 14.sp, - fontWeight = FontWeight.Medium - ) - } - } - - FloatingActionButton( - onClick = { - onClickAddLink() - isFabMenuExpanded = false - }, - containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface, - shape = RoundedCornerShape(16.dp), - elevation = FloatingActionButtonDefaults.elevation(defaultElevation = 6.dp) - ) { - Row( - modifier = Modifier.padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - Icons.Filled.Link, - contentDescription = stringResource(Res.string.add_from_a_link), - modifier = Modifier.size(20.dp) - ) - Text( - text = stringResource(Res.string.add_from_a_link), - fontSize = 14.sp, - fontWeight = FontWeight.Medium - ) - } - } - } - - FloatingActionButton( - onClick = { isFabMenuExpanded = !isFabMenuExpanded }, - shape = RoundedCornerShape(16.dp) - ) { - Row( - modifier = Modifier.padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - Icons.Filled.Add, - contentDescription = stringResource(Res.string.add), - modifier = Modifier.size(20.dp) - ) - Text( - text = stringResource(Res.string.add_playlist), - fontSize = 14.sp, - fontWeight = FontWeight.Medium - ) - } - } - } - } -} - -@Composable -private fun MappingListItem( - mapping: CurriculumMapping, - onClickMapping: (CurriculumMapping) -> Unit, - onRemoveMapping: (CurriculumMapping) -> Unit, -) { - var menuExpanded by remember { mutableStateOf(false) } - - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { onClickMapping(mapping) } - .padding(horizontal = 16.dp, vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - Icon( - Icons.Filled.Book, - contentDescription = null, - modifier = Modifier.size(40.dp), - tint = MaterialTheme.colorScheme.primary - ) - - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = mapping.title, - fontSize = 15.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - - Spacer(modifier = Modifier.height(4.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - Icon( - Icons.Filled.Book, - contentDescription = null, - modifier = Modifier.size(12.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Text( - text = "${mapping.sections.size} section, ${mapping.sections.sumOf { it.items.size }} items", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - Box { - IconButton(onClick = { menuExpanded = true }) { - Icon( - imageVector = Icons.Filled.MoreVert, - contentDescription = "", - ) - } - - DropdownMenu( - expanded = menuExpanded, - onDismissRequest = { menuExpanded = false } - ) { - DropdownMenuItem( - text = { - Text(stringResource(Res.string.remove)) - }, - onClick = { - menuExpanded = false - onRemoveMapping(mapping) - } - ) - } - } - } -} - @Composable fun AppGridItem( app: DataLoadState, diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/PlaylistListScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/PlaylistListScreen.kt new file mode 100644 index 000000000..828b9b0f0 --- /dev/null +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/PlaylistListScreen.kt @@ -0,0 +1,288 @@ +package world.respect.app.view.curriculum.mapping.list + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Book +import androidx.compose.material.icons.filled.Link +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.jetbrains.compose.resources.stringResource +import world.respect.shared.generated.resources.* +import world.respect.shared.viewmodel.curriculum.mapping.list.PlaylistListUiState +import world.respect.shared.viewmodel.curriculum.mapping.list.PlaylistListViewModel + +import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping + +@Composable +fun PlaylistListScreen( + viewModel: PlaylistListViewModel +) { + val uiState by viewModel.uiState.collectAsState() + + PlaylistListScreenContent( + uiState = uiState, + onFilterSelected = viewModel::onFilterSelected, + onClickMapping = viewModel::onClickMapping, + onClickAddNew = viewModel::onClickAddNew, + onClickAddLink = viewModel::onClickAddLink, + onRemoveMapping = viewModel::removeMapping, + ) +} + +@Composable +fun PlaylistListScreenContent( + uiState: PlaylistListUiState, + onFilterSelected: (Int) -> Unit, + onClickMapping: (CurriculumMapping) -> Unit, + onClickAddNew: () -> Unit, + onClickAddLink: () -> Unit, + onRemoveMapping: (CurriculumMapping) -> Unit, +) { + var isFabMenuExpanded by remember { mutableStateOf(false) } + + val filterChips = listOf( + stringResource(Res.string.all), + stringResource(Res.string.school_playlists), + stringResource(Res.string.my_playlists) + ) + + Box(modifier = Modifier.fillMaxSize()) { + Column(modifier = Modifier.fillMaxSize()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + filterChips.forEachIndexed { index, label -> + FilterChip( + selected = uiState.selectedFilterIndex == index, + onClick = { onFilterSelected(index) }, + label = { Text(label) } + ) + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .weight(1f) + ) { + if (uiState.mappings.isEmpty()) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(Res.string.no_playlists_yet), + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center + ) + } + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = 8.dp, + bottom = 88.dp + ) + ) { + items( + items = uiState.mappings, + key = { mapping -> mapping.uid } + ) { mapping -> + MappingListItem( + mapping = mapping, + onClickMapping = onClickMapping, + onRemoveMapping = onRemoveMapping, + ) + } + } + } + } + } + + Column( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(16.dp), + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + if (isFabMenuExpanded) { + FloatingActionButton( + onClick = { + onClickAddNew() + isFabMenuExpanded = false + }, + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + shape = RoundedCornerShape(16.dp), + elevation = FloatingActionButtonDefaults.elevation(defaultElevation = 6.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + Icons.Filled.Add, + contentDescription = stringResource(Res.string.add_new), + modifier = Modifier.size(20.dp) + ) + Text( + text = stringResource(Res.string.add_new), + fontSize = 14.sp, + fontWeight = FontWeight.Medium + ) + } + } + + FloatingActionButton( + onClick = { + onClickAddLink() + isFabMenuExpanded = false + }, + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface, + shape = RoundedCornerShape(16.dp), + elevation = FloatingActionButtonDefaults.elevation(defaultElevation = 6.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + Icons.Filled.Link, + contentDescription = stringResource(Res.string.add_from_a_link), + modifier = Modifier.size(20.dp) + ) + Text( + text = stringResource(Res.string.add_from_a_link), + fontSize = 14.sp, + fontWeight = FontWeight.Medium + ) + } + } + } + + FloatingActionButton( + onClick = { isFabMenuExpanded = !isFabMenuExpanded }, + shape = RoundedCornerShape(16.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + Icons.Filled.Add, + contentDescription = stringResource(Res.string.add), + modifier = Modifier.size(20.dp) + ) + Text( + text = stringResource(Res.string.add_playlist), + fontSize = 14.sp, + fontWeight = FontWeight.Medium + ) + } + } + } + } +} + +@Composable +private fun MappingListItem( + mapping: CurriculumMapping, + onClickMapping: (CurriculumMapping) -> Unit, + onRemoveMapping: (CurriculumMapping) -> Unit, +) { + var menuExpanded by remember { mutableStateOf(false) } + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onClickMapping(mapping) } + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + Icons.Filled.Book, + contentDescription = null, + modifier = Modifier.size(40.dp), + tint = MaterialTheme.colorScheme.primary + ) + + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = mapping.title, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + Icons.Filled.Book, + contentDescription = null, + modifier = Modifier.size(12.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Text( + text = "${mapping.sections.size} section, ${mapping.sections.sumOf { it.items.size }} items", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + Box { + IconButton(onClick = { menuExpanded = true }) { + Icon( + imageVector = Icons.Filled.MoreVert, + contentDescription = "", + ) + } + + DropdownMenu( + expanded = menuExpanded, + onDismissRequest = { menuExpanded = false } + ) { + DropdownMenuItem( + text = { + Text(stringResource(Res.string.remove)) + }, + onClick = { + menuExpanded = false + onRemoveMapping(mapping) + } + ) + } + } + } +} \ No newline at end of file diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt index 6484025ed..2797a0972 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt @@ -64,6 +64,7 @@ fun LearningUnitDetailScreen( ) } } + @Composable private fun SingleLessonDetailScreen( uiState: LearningUnitDetailUiState, @@ -173,6 +174,7 @@ private fun SingleLessonDetailScreen( } } } + @Composable private fun PlaylistDetailScreen( uiState: LearningUnitDetailUiState, @@ -322,7 +324,7 @@ private fun PlaylistDetailScreen( } }, modifier = Modifier - .testTag("expand_collapse_icon_") + .testTag("expand_collapse_icon_${section.uid}") ) { Icon( if (isExpanded) Icons.Default.KeyboardArrowUp diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt index 5bd2662c5..496d546c3 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt @@ -33,23 +33,19 @@ import world.respect.shared.generated.resources.app import world.respect.shared.generated.resources.empty_list_description_admin import world.respect.shared.generated.resources.empty_list_description_non_admin import world.respect.shared.generated.resources.home -import world.respect.shared.generated.resources.error_unexpected_result_type import world.respect.shared.navigation.AppsDetail import world.respect.shared.navigation.LearningUnitList import world.respect.shared.navigation.NavCommand import world.respect.shared.navigation.Settings import world.respect.shared.navigation.RespectAppLauncher import world.respect.shared.navigation.RespectAppList -import world.respect.shared.navigation.CurriculumMappingEdit -import world.respect.shared.navigation.EnterLink -import world.respect.shared.navigation.LearningUnitDetail import world.respect.shared.navigation.NavResultReturner import world.respect.shared.resources.UiText import world.respect.shared.util.ext.asUiText import world.respect.shared.util.ext.isAdmin import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.FabUiState -import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel +import world.respect.shared.viewmodel.curriculum.mapping.list.PlaylistListViewModel import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping data class AppLauncherUiState( @@ -57,9 +53,7 @@ data class AppLauncherUiState( val respectAppForSchoolApp: (SchoolApp) -> Flow> = { emptyFlow() }, val canRemove: Boolean = false, val emptyListDescription: UiText? = null, - val mappings: List = emptyList(), val selectedTabIndex: Int = 0, - val error: UiText? = null, ) class AppLauncherViewModel( @@ -68,7 +62,8 @@ class AppLauncherViewModel( private val accountManager: RespectAccountManager, private val getDevModeEnabledUseCase: GetDevModeEnabledUseCase, private val json: Json, - private val resultReturner: NavResultReturner, + resultReturner: NavResultReturner, + val playlistListViewModel: PlaylistListViewModel, ) : RespectViewModel(savedStateHandle), KoinScopeComponent { override val scope: Scope = accountManager.requireActiveAccountScope() @@ -77,7 +72,6 @@ class AppLauncherViewModel( val uiState = _uiState.asStateFlow() - var errorMessage: String = "" private var isAdmin: Boolean = false private val route: RespectAppLauncher = savedStateHandle.toRoute() @@ -105,9 +99,15 @@ class AppLauncherViewModel( prev.copy( respectAppForSchoolApp = this@AppLauncherViewModel::respectAppForSchoolApp, apps = pagingSourceHolder, - mappings = loadMappingsFromSavedState(savedStateHandle) ) } + val savedMappings = loadMappingsFromSavedState(savedStateHandle) + playlistListViewModel.setMappings(savedMappings) + viewModelScope.launch { + playlistListViewModel.navCommandFlow.collect { navCommand -> + _navCommandFlow.tryEmit(navCommand) + } + } viewModelScope.launch { accountManager.selectedAccountAndPersonFlow.collect { selected -> @@ -133,19 +133,9 @@ class AppLauncherViewModel( } } } - viewModelScope.launch { - resultReturner.filteredResultFlowForKey( - CurriculumMappingEditViewModel.KEY_SAVED_MAPPING - ).collect { result -> - val savedMapping = result.result as? CurriculumMapping - if (savedMapping == null) { - _uiState.update { - it.copy(error = Res.string.error_unexpected_result_type.asUiText()) - } - return@collect - } - addOrUpdateMapping(savedMapping) + playlistListViewModel.uiState.collect { playlistUiState -> + saveMappingsToSavedState(playlistUiState.mappings) } } } @@ -191,30 +181,6 @@ class AppLauncherViewModel( mappings ) } - - private fun addOrUpdateMapping(mapping: CurriculumMapping) { - val currentMappings = _uiState.value.mappings.toMutableList() - val existingIndex = currentMappings.indexOfFirst { it.uid == mapping.uid } - - if (existingIndex >= 0) { - currentMappings[existingIndex] = mapping - } else { - val newMapping = if (mapping.uid == 0L) { - mapping.copy(uid = System.currentTimeMillis()) - } else { - mapping - } - currentMappings.add(newMapping) - } - - updateMappings(currentMappings) - } - - private fun updateMappings(newMappings: List) { - _uiState.update { it.copy(mappings = newMappings) } - saveMappingsToSavedState(newMappings) - } - fun onClickApp(app: DataLoadState) { val url = app.metaInfo.url ?: return val appData = app.dataOrNull() ?: return @@ -258,39 +224,6 @@ class AppLauncherViewModel( } } - fun onClickMapping(mapping: CurriculumMapping) { - _navCommandFlow.tryEmit( - NavCommand.Navigate( - LearningUnitDetail.createFromMapping(mapping) - ) - ) - } - - fun onClickMap() { - _navCommandFlow.tryEmit( - NavCommand.Navigate( - CurriculumMappingEdit.create(uid = 0L, mappingData = null) - ) - ) - } - - fun onClickAddLink() { - _navCommandFlow.tryEmit( - NavCommand.Navigate( - EnterLink.create() - ) - ) - } - - fun onClickMoreOptions(mapping: CurriculumMapping) { - // TODO: Implement more options - } - - fun removeMapping(mapping: CurriculumMapping) { - val updated = _uiState.value.mappings.filter { it.uid != mapping.uid } - updateMappings(updated) - } - fun respectAppForSchoolApp(schoolApp: SchoolApp): Flow> { return appDataSource.compatibleAppsDataSource.getAppAsFlow( schoolApp.appManifestUrl, diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/PlaylistListViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/PlaylistListViewModel.kt new file mode 100644 index 000000000..7a19adcb5 --- /dev/null +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/PlaylistListViewModel.kt @@ -0,0 +1,111 @@ +package world.respect.shared.viewmodel.curriculum.mapping.list + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json +import world.respect.shared.navigation.CurriculumMappingEdit +import world.respect.shared.navigation.EnterLink +import world.respect.shared.navigation.LearningUnitDetail +import world.respect.shared.navigation.NavCommand +import world.respect.shared.navigation.NavResultReturner +import world.respect.shared.util.ext.asUiText +import world.respect.shared.viewmodel.RespectViewModel +import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel +import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping +import world.respect.shared.generated.resources.Res +import world.respect.shared.generated.resources.error_unexpected_result_type +import world.respect.shared.resources.UiText + +data class PlaylistListUiState( + val mappings: List = emptyList(), + val selectedFilterIndex: Int = 0, + val error: UiText? = null, +) + +class PlaylistListViewModel( + savedStateHandle: SavedStateHandle, + private val json: Json, + private val resultReturner: NavResultReturner, +) : RespectViewModel(savedStateHandle) { + + private val _uiState = MutableStateFlow(PlaylistListUiState()) + + val uiState = _uiState.asStateFlow() + + init { + viewModelScope.launch { + resultReturner.filteredResultFlowForKey( + CurriculumMappingEditViewModel.KEY_SAVED_MAPPING + ).collect { result -> + val savedMapping = result.result as? CurriculumMapping + if (savedMapping == null) { + _uiState.update { + it.copy(error = Res.string.error_unexpected_result_type.asUiText()) + } + return@collect + } + addOrUpdateMapping(savedMapping) + } + } + } + + fun setMappings(mappings: List) { + _uiState.update { it.copy(mappings = mappings) } + } + + fun onFilterSelected(index: Int) { + _uiState.update { it.copy(selectedFilterIndex = index) } + } + + fun onClickMapping(mapping: CurriculumMapping) { + _navCommandFlow.tryEmit( + NavCommand.Navigate( + LearningUnitDetail.createFromMapping(mapping) + ) + ) + } + + fun onClickAddNew() { + _navCommandFlow.tryEmit( + NavCommand.Navigate( + CurriculumMappingEdit.create(uid = 0L, mappingData = null) + ) + ) + } + + fun onClickAddLink() { + _navCommandFlow.tryEmit( + NavCommand.Navigate( + EnterLink.create() + ) + ) + } + + fun removeMapping(mapping: CurriculumMapping): List { + val updated = _uiState.value.mappings.filter { it.uid != mapping.uid } + _uiState.update { it.copy(mappings = updated) } + return updated + } + + private fun addOrUpdateMapping(mapping: CurriculumMapping) { + val currentMappings = _uiState.value.mappings.toMutableList() + val existingIndex = currentMappings.indexOfFirst { it.uid == mapping.uid } + + if (existingIndex >= 0) { + currentMappings[existingIndex] = mapping + } else { + val newMapping = if (mapping.uid == 0L) { + mapping.copy(uid = System.currentTimeMillis()) + } else { + mapping + } + currentMappings.add(newMapping) + } + + _uiState.update { it.copy(mappings = currentMappings) } + } +} \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/LearningUnitSelection.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/LearningUnitSelection.kt index 26b7a035e..44f7d8f65 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/LearningUnitSelection.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/LearningUnitSelection.kt @@ -13,4 +13,14 @@ data class LearningUnitSelection( val learningUnitManifestUrl: Url, val selectedPublication: OpdsPublication, val appManifestUrl: Url, + val additionalLessons: List? = null, +) +/** + * Represents additional lessons when multiple lessons are selected from a playlist/section + */ +@Serializable +data class AdditionalLesson( + val learningUnitManifestUrl: String, + val title: String, + val appManifestUrl: String, ) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt index 9417a8c28..cdb2395f0 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt @@ -39,6 +39,7 @@ import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingE import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingSectionUiState import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMappingSectionLink +import world.respect.shared.viewmodel.learningunit.AdditionalLesson import world.respect.shared.viewmodel.learningunit.LearningUnitSelection data class LearningUnitDetailUiState( @@ -198,9 +199,23 @@ class LearningUnitDetailViewModel( val mapping = uiState.value.mapping if (mapping != null) { - val firstLesson = mapping.sections.firstOrNull()?.items?.firstOrNull() + val allLessons = mapping.sections.flatMap { it.items } - if (firstLesson != null && firstLesson.appManifestUrl != null) { + if (allLessons.isEmpty()) { + _navCommandFlow.tryEmit( + NavCommand.Navigate( + destination = AssignmentEdit.create( + uid = null, + learningUnitSelected = null + ) + ) + ) + return + } + + val firstLesson = allLessons.first() + + if (firstLesson.appManifestUrl != null) { viewModelScope.launch { appDataSource.opdsDataSource.loadOpdsPublication( url = Url(firstLesson.href), @@ -210,6 +225,16 @@ class LearningUnitDetailViewModel( ).collect { publicationState -> val publication = (publicationState as? DataReadyState)?.data if (publication != null) { + val additionalLessons = allLessons.drop(1).mapNotNull { lesson -> + lesson.appManifestUrl?.let { + AdditionalLesson( + learningUnitManifestUrl = lesson.href, + title = lesson.title ?: "", + appManifestUrl = it.toString() + ) + } + } + _navCommandFlow.tryEmit( NavCommand.Navigate( destination = AssignmentEdit.create( @@ -217,7 +242,8 @@ class LearningUnitDetailViewModel( learningUnitSelected = LearningUnitSelection( learningUnitManifestUrl = Url(firstLesson.href), selectedPublication = publication, - appManifestUrl = firstLesson.appManifestUrl + appManifestUrl = firstLesson.appManifestUrl, + additionalLessons = additionalLessons.takeIf { it.isNotEmpty() } ) ) ) @@ -225,15 +251,6 @@ class LearningUnitDetailViewModel( } } } - } else { - _navCommandFlow.tryEmit( - NavCommand.Navigate( - destination = AssignmentEdit.create( - uid = null, - learningUnitSelected = null - ) - ) - ) } } else { val publicationVal = uiState.value.lessonDetail ?: return @@ -244,7 +261,8 @@ class LearningUnitDetailViewModel( learningUnitSelected = LearningUnitSelection( learningUnitManifestUrl = route.learningUnitManifestUrl, selectedPublication = publicationVal, - appManifestUrl = route.appManifestUrl + appManifestUrl = route.appManifestUrl, + additionalLessons = null ) ) ) From 5769152b2fb63f827d7925581195edf761e058c0 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Wed, 24 Dec 2025 10:12:28 +0530 Subject: [PATCH 32/58] modify the code --- ...ingEditScreen.kt => PlaylistEditScreen.kt} | 1 - .../detail/LearningUnitDetailScreen.kt | 79 +++++++- .../composeResources/values/strings.xml | 3 +- ...tViewModel.kt => PlaylistEditViewModel.kt} | 3 +- .../learningunit/LearningUnitSelection.kt | 11 +- .../detail/LearningUnitDetailViewModel.kt | 175 +++++++++++++----- 6 files changed, 209 insertions(+), 63 deletions(-) rename respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/{CurriculumMappingEditScreen.kt => PlaylistEditScreen.kt} (99%) rename respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/{CurriculumMappingEditViewModel.kt => PlaylistEditViewModel.kt} (99%) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/PlaylistEditScreen.kt similarity index 99% rename from respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt rename to respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/PlaylistEditScreen.kt index 2683406f2..4651f29ec 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/CurriculumMappingEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/PlaylistEditScreen.kt @@ -3,7 +3,6 @@ package world.respect.app.view.curriculum.mapping.edit import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt index 2797a0972..32bcb1598 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog import com.ustadmobile.libcache.PublicationPinState import com.ustadmobile.libuicompose.theme.black import com.ustadmobile.libuicompose.theme.white @@ -45,12 +46,24 @@ fun LearningUnitDetailScreen( viewModel: LearningUnitDetailViewModel ) { val uiState by viewModel.uiState.collectAsState() + + if (uiState.showCopyDialog) { + CopyPlaylistDialog( + currentName = uiState.mapping?.title.orEmpty(), + copyDialogName = uiState.copyDialogName, + onNameChanged = viewModel::onCopyDialogNameChanged, + onDismiss = viewModel::onCopyDialogDismiss, + onConfirm = viewModel::onCopyDialogConfirm + ) + } + if (uiState.mapping != null) { PlaylistDetailScreen( uiState = uiState, onClickLesson = viewModel::onClickLesson, onClickEdit = viewModel::onClickEdit, onClickAssign = viewModel::onClickAssign, + onClickAssignSection = viewModel::onClickAssignSection, onClickShare = viewModel::onClickShare, onClickCopy = viewModel::onClickCopy, onClickDelete = viewModel::onClickDelete, @@ -65,6 +78,67 @@ fun LearningUnitDetailScreen( } } +@Composable +private fun CopyPlaylistDialog( + currentName: String, + copyDialogName: String, + onNameChanged: (String) -> Unit, + onDismiss: () -> Unit, + onConfirm: () -> Unit +) { + Dialog(onDismissRequest = onDismiss) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + shape = RoundedCornerShape(16.dp) + ) { + Column( + modifier = Modifier.padding(24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = stringResource(Res.string.make_a_copy), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold + ) + + OutlinedTextField( + value = copyDialogName, + onValueChange = onNameChanged, + label = { Text(stringResource(Res.string.name)) }, + placeholder = { Text("Copy of $currentName") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + TextButton( + onClick = onDismiss + ) { + Text( + text = stringResource(Res.string.cancel), + color = MaterialTheme.colorScheme.primary + ) + } + TextButton( + onClick = onConfirm + ) { + Text( + text = stringResource(Res.string.copy_playlist), + color = MaterialTheme.colorScheme.primary + ) + } + } + } + } + } +} + @Composable private fun SingleLessonDetailScreen( uiState: LearningUnitDetailUiState, @@ -181,6 +255,7 @@ private fun PlaylistDetailScreen( onClickLesson: (CurriculumMappingSectionLink) -> Unit, onClickEdit: () -> Unit, onClickAssign: () -> Unit, + onClickAssignSection: (Long) -> Unit, onClickShare: () -> Unit, onClickCopy: () -> Unit, onClickDelete: () -> Unit, @@ -305,7 +380,7 @@ private fun PlaylistDetailScreen( ) IconButton( - onClick = onClickAssign, + onClick = { onClickAssignSection(section.uid) }, modifier = Modifier.size(40.dp) ) { Icon( @@ -324,7 +399,7 @@ private fun PlaylistDetailScreen( } }, modifier = Modifier - .testTag("expand_collapse_icon_${section.uid}") + .testTag("expand_collapse_icon_") ) { Icon( if (isExpanded) Icons.Default.KeyboardArrowUp diff --git a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml index 2b39ebf55..d5d0d5c78 100644 --- a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml +++ b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml @@ -471,12 +471,13 @@ Add new Add from a link Playlist - Copy playlist + Copy All sections, items Created by: Edit playlist Home copy playlist + Make a copy %1$d sections • %2$d items diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/PlaylistEditViewModel.kt similarity index 99% rename from respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt rename to respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/PlaylistEditViewModel.kt index 2c39850ea..6c9add3af 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/CurriculumMappingEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/PlaylistEditViewModel.kt @@ -23,7 +23,6 @@ import world.respect.libutil.ext.updateAtIndex import world.respect.libutil.ext.resolve import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.create_playlist -import world.respect.shared.generated.resources.edit_mapping import world.respect.shared.generated.resources.edit_playlist import world.respect.shared.generated.resources.required_field import world.respect.shared.generated.resources.save @@ -345,7 +344,7 @@ class CurriculumMappingEditViewModel( NavCommand.Navigate( destination = LearningUnitDetail.createFromMapping(mapping), popUpTo = route, - popUpToInclusive = true + popUpToInclusive = true ) ) } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/LearningUnitSelection.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/LearningUnitSelection.kt index 44f7d8f65..b29da5550 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/LearningUnitSelection.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/LearningUnitSelection.kt @@ -13,14 +13,5 @@ data class LearningUnitSelection( val learningUnitManifestUrl: Url, val selectedPublication: OpdsPublication, val appManifestUrl: Url, - val additionalLessons: List? = null, -) -/** - * Represents additional lessons when multiple lessons are selected from a playlist/section - */ -@Serializable -data class AdditionalLesson( - val learningUnitManifestUrl: String, - val title: String, - val appManifestUrl: String, ) + diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt index cdb2395f0..f74d8e56a 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt @@ -6,10 +6,12 @@ import androidx.navigation.toRoute import com.ustadmobile.libcache.PublicationPinState import com.ustadmobile.libcache.UstadCache import io.ktor.http.Url +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -30,6 +32,7 @@ import world.respect.shared.navigation.AssignmentEdit import world.respect.shared.navigation.CurriculumMappingEdit import world.respect.shared.navigation.LearningUnitDetail import world.respect.shared.navigation.NavCommand +import world.respect.shared.navigation.NavResult import world.respect.shared.navigation.NavResultReturner import world.respect.shared.util.ext.asUiText import world.respect.shared.util.ext.resolve @@ -39,7 +42,6 @@ import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingE import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingSectionUiState import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMappingSectionLink -import world.respect.shared.viewmodel.learningunit.AdditionalLesson import world.respect.shared.viewmodel.learningunit.LearningUnitSelection data class LearningUnitDetailUiState( @@ -52,6 +54,8 @@ data class LearningUnitDetailUiState( val sectionLinkUiState: (CurriculumMappingSectionLink) -> Flow> = { emptyFlow() }, + val showCopyDialog: Boolean = false, + val copyDialogName: String = "", ) { val buttonsEnabled: Boolean get() = lessonDetail != null || mapping != null @@ -194,14 +198,64 @@ class LearningUnitDetailViewModel( } } } + private suspend fun loadLessonPublications( + lessons: List + ): List { + return lessons.mapNotNull { lesson -> + try { + val publicationState = appDataSource.opdsDataSource.loadOpdsPublication( + url = Url(lesson.href), + params = DataLoadParams(), + referrerUrl = null, + expectedPublicationId = null, + ).first { it is DataReadyState } + val publication = (publicationState as? DataReadyState)?.data + + if (publication != null && lesson.appManifestUrl != null) { + LearningUnitSelection( + learningUnitManifestUrl = Url(lesson.href), + selectedPublication = publication, + appManifestUrl = lesson.appManifestUrl + ) + } else null + } catch (e: Exception) { + e.printStackTrace() + null + } + } + } fun onClickAssign() { val mapping = uiState.value.mapping if (mapping != null) { - val allLessons = mapping.sections.flatMap { it.items } + val allLessons = mapping.sections.flatMap { section -> + section.items.filter { it.appManifestUrl != null } + } - if (allLessons.isEmpty()) { + if (allLessons.isNotEmpty()) { + viewModelScope.launch { + val learningUnitSelections = loadLessonPublications(allLessons) + + if (learningUnitSelections.isNotEmpty()) { + resultReturner.sendResult( + NavResult( + key = KEY_BULK_LEARNING_UNITS, + result = learningUnitSelections + ) + ) + delay(50) + _navCommandFlow.tryEmit( + NavCommand.Navigate( + destination = AssignmentEdit.create( + uid = null, + learningUnitSelected = learningUnitSelections.first() + ) + ) + ) + } + } + } else { _navCommandFlow.tryEmit( NavCommand.Navigate( destination = AssignmentEdit.create( @@ -210,47 +264,6 @@ class LearningUnitDetailViewModel( ) ) ) - return - } - - val firstLesson = allLessons.first() - - if (firstLesson.appManifestUrl != null) { - viewModelScope.launch { - appDataSource.opdsDataSource.loadOpdsPublication( - url = Url(firstLesson.href), - params = DataLoadParams(), - referrerUrl = null, - expectedPublicationId = null, - ).collect { publicationState -> - val publication = (publicationState as? DataReadyState)?.data - if (publication != null) { - val additionalLessons = allLessons.drop(1).mapNotNull { lesson -> - lesson.appManifestUrl?.let { - AdditionalLesson( - learningUnitManifestUrl = lesson.href, - title = lesson.title ?: "", - appManifestUrl = it.toString() - ) - } - } - - _navCommandFlow.tryEmit( - NavCommand.Navigate( - destination = AssignmentEdit.create( - uid = null, - learningUnitSelected = LearningUnitSelection( - learningUnitManifestUrl = Url(firstLesson.href), - selectedPublication = publication, - appManifestUrl = firstLesson.appManifestUrl, - additionalLessons = additionalLessons.takeIf { it.isNotEmpty() } - ) - ) - ) - ) - } - } - } } } else { val publicationVal = uiState.value.lessonDetail ?: return @@ -261,14 +274,42 @@ class LearningUnitDetailViewModel( learningUnitSelected = LearningUnitSelection( learningUnitManifestUrl = route.learningUnitManifestUrl, selectedPublication = publicationVal, - appManifestUrl = route.appManifestUrl, - additionalLessons = null + appManifestUrl = route.appManifestUrl ) ) ) ) } } + fun onClickAssignSection(sectionUid: Long) { + val mapping = _uiState.value.mapping ?: return + val section = mapping.sections.find { it.uid == sectionUid } ?: return + val sectionLessons = section.items.filter { it.appManifestUrl != null } + + if (sectionLessons.isNotEmpty()) { + viewModelScope.launch { + val learningUnitSelections = loadLessonPublications(sectionLessons) + + if (learningUnitSelections.isNotEmpty()) { + resultReturner.sendResult( + NavResult( + key = KEY_BULK_LEARNING_UNITS, + result = learningUnitSelections + ) + ) + delay(50) + _navCommandFlow.tryEmit( + NavCommand.Navigate( + destination = AssignmentEdit.create( + uid = null, + learningUnitSelected = learningUnitSelections.first() + ) + ) + ) + } + } + } + } fun onClickLesson(link: CurriculumMappingSectionLink) { val publicationUrl = Url(link.href) @@ -303,7 +344,43 @@ class LearningUnitDetailViewModel( } fun onClickCopy() { - // TODO: Implement copy functionality + _uiState.update { it.copy(showCopyDialog = true) } + } + + fun onCopyDialogDismiss() { + _uiState.update { it.copy(showCopyDialog = false, copyDialogName = "") } + } + + fun onCopyDialogNameChanged(name: String) { + _uiState.update { it.copy(copyDialogName = name) } + } + + fun onCopyDialogConfirm() { + val mapping = _uiState.value.mapping ?: return + val newName = _uiState.value.copyDialogName.trim() + + if (newName.isEmpty()) { + return + } + + val copiedMapping = mapping.copy( + uid = System.currentTimeMillis(), + title = newName + ) + + resultReturner.sendResult( + NavResult( + key = CurriculumMappingEditViewModel.KEY_SAVED_MAPPING, + result = copiedMapping + ) + ) + + _uiState.update { + it.copy( + showCopyDialog = false, + copyDialogName = "" + ) + } } fun onClickDelete() { @@ -332,4 +409,8 @@ class LearningUnitDetailViewModel( } } } + + companion object { + const val KEY_BULK_LEARNING_UNITS = "bulk_learning_units" + } } \ No newline at end of file From dc14cbd5e1d773e49247b06b769971e6d523576d Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Wed, 24 Dec 2025 12:48:34 +0530 Subject: [PATCH 33/58] Modify the assign button functionality --- .../respect/shared/navigation/AppRoutes.kt | 45 +++++++++++------- .../apps/launcher/AppLauncherViewModel.kt | 12 ----- .../edit/AssignmentEditViewModel.kt | 47 +++++++++---------- .../detail/LearningUnitDetailViewModel.kt | 27 ++--------- 4 files changed, 54 insertions(+), 77 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index c51210a2d..3a932c1cc 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.SavedStateHandle import io.ktor.http.Url import kotlinx.serialization.Serializable import kotlinx.serialization.Transient +import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.json.Json import world.respect.shared.domain.account.invite.RespectRedeemInviteRequest import world.respect.datalayer.school.model.EnrollmentRoleEnum @@ -92,34 +93,46 @@ object AssignmentList : RespectAppRoute data class AssignmentDetail( val uid: String, ) : RespectAppRoute - @Serializable data class AssignmentEdit( - val guid: String?, - private val learningUnitStr: String? = null, -): RespectAppRoute { + val guid: String? = null, + private val learningUnitsJson: String? = null, +) : RespectAppRoute { @Transient - val learningUnitSelected: LearningUnitSelection? = learningUnitStr?.let { - Json.decodeFromString(LearningUnitSelection.serializer(), it) + val learningUnitSelectedList: List? = learningUnitsJson?.let { jsonStr -> + try { + Json.decodeFromString>(jsonStr) + } catch (e: Exception) { + null + } } companion object { - fun create( uid: String?, - learningUnitSelected: LearningUnitSelection? = null, - ) = AssignmentEdit( - guid = uid, - learningUnitStr = learningUnitSelected?.let { - Json.encodeToString(LearningUnitSelection.serializer(), it) - }, - ) + learningUnitSelected: LearningUnitSelection? = null + ): AssignmentEdit { + val learningUnits = learningUnitSelected?.let { listOf(it) } + return AssignmentEdit( + guid = uid, + learningUnitsJson = learningUnits?.let { + Json.encodeToString(it) + } + ) + } + fun createWithMultipleLessons( + uid: String?, + learningUnits: List + ): AssignmentEdit { + return AssignmentEdit( + guid = uid, + learningUnitsJson = Json.encodeToString(learningUnits) + ) + } } - } - @Serializable object ClazzList : RespectAppRoute diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt index 496d546c3..dcf084f6f 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt @@ -133,11 +133,6 @@ class AppLauncherViewModel( } } } - viewModelScope.launch { - playlistListViewModel.uiState.collect { playlistUiState -> - saveMappingsToSavedState(playlistUiState.mappings) - } - } } fun onTabSelected(index: Int) { @@ -174,13 +169,6 @@ class AppLauncherViewModel( emptyList() } } - - private fun saveMappingsToSavedState(mappings: List) { - savedStateHandle[KEY_MAPPINGS_LIST] = json.encodeToString( - kotlinx.serialization.builtins.ListSerializer(CurriculumMapping.serializer()), - mappings - ) - } fun onClickApp(app: DataLoadState) { val url = app.metaInfo.url ?: return val appData = app.dataOrNull() ?: return diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt index 08ba47d74..b326eaf08 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt @@ -115,6 +115,24 @@ class AssignmentEditViewModel( hideBottomNavigation = true, ) } + viewModelScope.launch { + resultReturner.filteredResultFlowForKey(KEY_LEARNING_UNIT).collect { result -> + val learningUnit = result.result as? LearningUnitSelection ?: return@collect + val assignmentResourceRef = learningUnit.toRef() + + _uiState.update { prev -> + val prevAssignment = prev.assignment.dataOrNull() ?: return@update prev + + prev.copy( + assignment = DataReadyState( + data = prevAssignment.copy( + learningUnits = prevAssignment.learningUnits + assignmentResourceRef + ) + ) + ) + } + } + } launchWithLoadingIndicator { val classes = schoolDataSource.classDataSource.list( @@ -151,6 +169,8 @@ class AssignmentEditViewModel( } ) }else { + val initialLearningUnits = route.learningUnitSelectedList?.map { it.toRef() } ?: emptyList() + _uiState.update { prev -> prev.copy( assignment = DataReadyState( @@ -158,33 +178,12 @@ class AssignmentEditViewModel( uid = uid, title = "", description = "", - learningUnits = route.learningUnitSelected?.let { - listOf(it.toRef()) - } ?: emptyList() + learningUnits = initialLearningUnits ) ) ) } } - - viewModelScope.launch { - resultReturner.filteredResultFlowForKey(KEY_LEARNING_UNIT).collect { result -> - val learningUnit = result.result as? LearningUnitSelection ?: return@collect - val assignmentResourceRef = learningUnit.toRef() - - _uiState.update { prev -> - val prevAssignment = prev.assignment.dataOrNull() ?: return@update prev - - prev.copy( - assignment = DataReadyState( - data = prevAssignment.copy( - learningUnits = prevAssignment.learningUnits + assignmentResourceRef - ) - ) - ) - } - } - } } } @@ -232,7 +231,6 @@ class AssignmentEditViewModel( } } - fun onClickAddLearningUnit() { _navCommandFlow.tryEmit( NavCommand.Navigate( @@ -303,9 +301,6 @@ class AssignmentEditViewModel( } companion object { - const val KEY_LEARNING_UNIT = "result_learning_unit" - } - } \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt index f74d8e56a..0d8f0366a 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt @@ -6,7 +6,6 @@ import androidx.navigation.toRoute import com.ustadmobile.libcache.PublicationPinState import com.ustadmobile.libcache.UstadCache import io.ktor.http.Url -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -238,18 +237,11 @@ class LearningUnitDetailViewModel( val learningUnitSelections = loadLessonPublications(allLessons) if (learningUnitSelections.isNotEmpty()) { - resultReturner.sendResult( - NavResult( - key = KEY_BULK_LEARNING_UNITS, - result = learningUnitSelections - ) - ) - delay(50) _navCommandFlow.tryEmit( NavCommand.Navigate( - destination = AssignmentEdit.create( + destination = AssignmentEdit.createWithMultipleLessons( uid = null, - learningUnitSelected = learningUnitSelections.first() + learningUnits = learningUnitSelections ) ) ) @@ -291,18 +283,11 @@ class LearningUnitDetailViewModel( val learningUnitSelections = loadLessonPublications(sectionLessons) if (learningUnitSelections.isNotEmpty()) { - resultReturner.sendResult( - NavResult( - key = KEY_BULK_LEARNING_UNITS, - result = learningUnitSelections - ) - ) - delay(50) _navCommandFlow.tryEmit( NavCommand.Navigate( - destination = AssignmentEdit.create( + destination = AssignmentEdit.createWithMultipleLessons( uid = null, - learningUnitSelected = learningUnitSelections.first() + learningUnits = learningUnitSelections ) ) ) @@ -409,8 +394,4 @@ class LearningUnitDetailViewModel( } } } - - companion object { - const val KEY_BULK_LEARNING_UNITS = "bulk_learning_units" - } } \ No newline at end of file From 0b61a446c809578182190ad2770da00657d06d40 Mon Sep 17 00:00:00 2001 From: pooja Date: Wed, 24 Dec 2025 11:51:09 +0400 Subject: [PATCH 34/58] updated test flow --- .maestro/flows/002_browse_lessons_test.yaml | 57 +++++++++++++++++++- .maestro/flows/subflows/admin_add_class.yaml | 3 -- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/.maestro/flows/002_browse_lessons_test.yaml b/.maestro/flows/002_browse_lessons_test.yaml index b3958267e..a4e5ebfd6 100644 --- a/.maestro/flows/002_browse_lessons_test.yaml +++ b/.maestro/flows/002_browse_lessons_test.yaml @@ -49,6 +49,7 @@ onFlowComplete: timeout: 1000 - assertVisible: "Hello World Lesson" - tapOn: "Close" +- runFlow: "subflows/admin_add_class.yaml" - tapOn: "Home" - tapOn: "Playlists" - assertVisible: "All" @@ -127,6 +128,15 @@ onFlowComplete: - assertVisible: "Open" - tapOn: "Home" - tapOn: "Playlists" +- assertVisible: "Playlist - Grade 1" +- assertVisible: "2 sections, 1 items" +- assertVisible: "Created by: Admin" +- tapOn: "My Playlist" +- assertVisible: "Playlist - Grade 1" +- tapOn: "School Playlist" +- assertVisible: "No playlists yet" +- tapOn: "All" +- assertVisible: "Playlist - Grade 1" - tapOn: "Playlist - Grade 1" - assertVisible: id: "app_title" @@ -136,4 +146,49 @@ onFlowComplete: - assertVisible: id: "app_title" text: "Add assignment" -- assertVisible: "Lesson 001" \ No newline at end of file +- tapOn: "Name*" +- inputText: "Homework 1" +- tapOn: "Class" +- tapOn: "New Class" +- tapOn: "Date" +- runScript: + file: "scripts/setDate.js" +- inputText: ${output.futureDate} +- tapOn: "Time" +- runScript: + file: "scripts/setDate.js" +- inputText: ${output.currentTime} +- assertVisible: "Lesson 001" +- tapOn: "Save" +- assertVisible: + id: "app_title" + text: "Homework 1" +- assertVisible: "Lesson 001" +- tapOn: "Home" +- tapOn: "Playlists" +- tapOn: "Playlist - Grade 1" +- assertVisible: + id: "share_btn" +- assertVisible: + id: "app_title" + text: "Share" +- assertVisible: ${output.futureDate}playlist/uid +- tapOn: "Who can view" +- tapOn: "Anyone with the link" +- tapOn: "Who can edit" +- tapOn: "Teacher/admin in my school" +- assertVisible: "Share link" +- assertVisible: "Copy link" +- assertVisible: "Send link via SMS" +- assertVisible: "Send link via email" +- back +- tapOn: + id: "copy_btn" +- assertVisible: "Make a copy" +- assertVisible: "copy the Playlist - Grade 1" +- tapOn: "Copy" +- assertVisible: + id: "app_title" + text: "copy the Playlist - Grade 1" +- assertVisible: "Day 1" +- assertVisible: "Day 2" diff --git a/.maestro/flows/subflows/admin_add_class.yaml b/.maestro/flows/subflows/admin_add_class.yaml index cd161cb5a..4d8608343 100644 --- a/.maestro/flows/subflows/admin_add_class.yaml +++ b/.maestro/flows/subflows/admin_add_class.yaml @@ -1,9 +1,6 @@ appId: world.respect.app --- -- assertVisible: - id: "app_title" - text: "Apps" # Admin add new class - tapOn: "Classes" - assertVisible: From 62b066175c4043e59c9abd805ab3b354681f52c0 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Mon, 29 Dec 2025 10:21:07 +0530 Subject: [PATCH 35/58] fix playlistList --- .../apps/launcher/AppLauncherViewModel.kt | 9 ++++---- .../mapping/edit/PlaylistEditViewModel.kt | 18 +++++++++------- .../mapping/list/PlaylistListViewModel.kt | 2 -- .../detail/LearningUnitDetailViewModel.kt | 21 ------------------- 4 files changed, 16 insertions(+), 34 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt index dcf084f6f..93811aa76 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.json.Json import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject @@ -45,6 +46,7 @@ import world.respect.shared.util.ext.asUiText import world.respect.shared.util.ext.isAdmin import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.FabUiState +import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel import world.respect.shared.viewmodel.curriculum.mapping.list.PlaylistListViewModel import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping @@ -84,7 +86,6 @@ class AppLauncherViewModel( params = SchoolAppDataSource.GetListParams() ) } - init { _appUiState.update { it.copy( @@ -101,14 +102,15 @@ class AppLauncherViewModel( apps = pagingSourceHolder, ) } - val savedMappings = loadMappingsFromSavedState(savedStateHandle) - playlistListViewModel.setMappings(savedMappings) viewModelScope.launch { playlistListViewModel.navCommandFlow.collect { navCommand -> _navCommandFlow.tryEmit(navCommand) } } + val savedMappings = loadMappingsFromSavedState(savedStateHandle) + playlistListViewModel.setMappings(savedMappings) + viewModelScope.launch { accountManager.selectedAccountAndPersonFlow.collect { selected -> val isAdmin = selected?.person?.isAdmin() == true @@ -134,7 +136,6 @@ class AppLauncherViewModel( } } } - fun onTabSelected(index: Int) { _uiState.update { it.copy(selectedTabIndex = index) } updateFabState(isAdmin, index) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/PlaylistEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/PlaylistEditViewModel.kt index 6c9add3af..608984141 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/PlaylistEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/PlaylistEditViewModel.kt @@ -305,9 +305,6 @@ class CurriculumMappingEditViewModel( ) } - /** - * Provide a flow that creates the SectionLinkUiState . - */ fun sectionLinkUiStateFor( link: CurriculumMappingSectionLink ): Flow> { @@ -334,17 +331,24 @@ class CurriculumMappingEditViewModel( _uiState.update { it.copy(titleError = Res.string.required_field.asUiText()) } return } + + val finalMapping = if (mapping.uid == 0L) { + mapping.copy(uid = System.currentTimeMillis()) + } else { + mapping + } + resultReturner.sendResult( NavResult( key = KEY_SAVED_MAPPING, - result = mapping + result = finalMapping ) ) _navCommandFlow.tryEmit( NavCommand.Navigate( - destination = LearningUnitDetail.createFromMapping(mapping), - popUpTo = route, - popUpToInclusive = true + destination = LearningUnitDetail.createFromMapping(finalMapping), + popUpTo = RespectAppLauncher(), + popUpToInclusive = false ) ) } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/PlaylistListViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/PlaylistListViewModel.kt index 7a19adcb5..01636cc7d 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/PlaylistListViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/PlaylistListViewModel.kt @@ -6,7 +6,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.serialization.json.Json import world.respect.shared.navigation.CurriculumMappingEdit import world.respect.shared.navigation.EnterLink import world.respect.shared.navigation.LearningUnitDetail @@ -28,7 +27,6 @@ data class PlaylistListUiState( class PlaylistListViewModel( savedStateHandle: SavedStateHandle, - private val json: Json, private val resultReturner: NavResultReturner, ) : RespectViewModel(savedStateHandle) { diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt index 0d8f0366a..c84981dd8 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt @@ -78,27 +78,6 @@ class LearningUnitDetailViewModel( private val route: LearningUnitDetail = savedStateHandle.toRoute() init { - viewModelScope.launch { - resultReturner.filteredResultFlowForKey( - CurriculumMappingEditViewModel.KEY_SAVED_MAPPING - ).collect { result -> - val savedMapping = result.result as? CurriculumMapping - if (savedMapping != null) { - _uiState.update { - it.copy( - mapping = savedMapping, - sectionLinkUiState = this@LearningUnitDetailViewModel::sectionLinkUiStateFor - ) - } - _appUiState.update { - it.copy( - title = savedMapping.title.asUiText() - ) - } - } - } - } - val mappingData = route.mappingData if (mappingData != null) { From 4ef55bcf67049fdab00f7c9c12e8af9630d46a18 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Tue, 30 Dec 2025 10:03:41 +0530 Subject: [PATCH 36/58] add the multiplelesson on assignment --- .../assignment/edit/AssignmentEditScreen.kt | 42 +++++- .../composeResources/values/strings.xml | 3 +- .../respect/shared/navigation/AppRoutes.kt | 43 +++++- .../apps/launcher/AppLauncherViewModel.kt | 6 + .../detail/AssignmentDetailViewModel.kt | 8 +- .../edit/AssignmentEditViewModel.kt | 138 +++++++++++++++++- .../list/AssignmentListViewModel.kt | 6 +- .../mapping/list/PlaylistListViewModel.kt | 4 + .../detail/LearningUnitDetailViewModel.kt | 15 +- 9 files changed, 244 insertions(+), 21 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt index 961263e9a..3efddc4dc 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt @@ -2,13 +2,21 @@ package world.respect.app.view.assignment.edit import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.LibraryAdd +import androidx.compose.material3.AlertDialog import androidx.compose.material3.ListItem import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api @@ -20,14 +28,17 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import kotlinx.coroutines.Dispatchers @@ -43,16 +54,21 @@ import world.respect.datalayer.ext.dataOrNull import world.respect.datalayer.school.model.Assignment import world.respect.datalayer.school.model.AssignmentLearningUnitRef import world.respect.datalayer.school.model.Clazz +import world.respect.lib.opds.model.OpdsGroup import world.respect.lib.opds.model.findIcons import world.respect.libutil.ext.resolve import world.respect.shared.generated.resources.Res +import world.respect.shared.generated.resources.add_from_playlist import world.respect.shared.generated.resources.assignment_tasks +import world.respect.shared.generated.resources.cancel import world.respect.shared.generated.resources.clazz import world.respect.shared.generated.resources.delete import world.respect.shared.generated.resources.description import world.respect.shared.generated.resources.lesson_assessment import world.respect.shared.generated.resources.name +import world.respect.shared.generated.resources.no_playlists_yet import world.respect.shared.generated.resources.required +import world.respect.shared.generated.resources.select_curriculum_unit import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.app.appstate.getTitle import world.respect.shared.viewmodel.assignment.edit.AssignmentEditUiState @@ -71,6 +87,7 @@ fun AssignmentEditScreen( onClickAddLearningUnit = viewModel::onClickAddLearningUnit, onAssigneeClassSelected = viewModel::onAssigneeClassSelected, onClickRemoveLearningUnit = viewModel::onClickRemoveLearningUnit, + onClickAddFromCurriculum = viewModel::onClickAddFromCurriculum, ) } @@ -83,6 +100,7 @@ fun AssignmentEditScreen( onAssigneeClassSelected: (Clazz) -> Unit, onClickAddLearningUnit: () -> Unit, onClickRemoveLearningUnit: (AssignmentLearningUnitRef) -> Unit, + onClickAddFromCurriculum: () -> Unit, ) { val assignment = uiState.assignment.dataOrNull() val filteredOptions = if(uiState.assigneeText.isNotBlank()) { @@ -115,7 +133,6 @@ fun AssignmentEditScreen( var expanded by remember { mutableStateOf(false) } - //As per https://developer.android.com/reference/kotlin/androidx/compose/material3/package-summary#ExposedDropdownMenuBox(kotlin.Boolean,kotlin.Function1,androidx.compose.ui.Modifier,kotlin.Function1) ExposedDropdownMenuBox( expanded = expanded, onExpandedChange = { expanded = it } @@ -199,7 +216,6 @@ fun AssignmentEditScreen( text = stringResource(Res.string.assignment_tasks), style = MaterialTheme.typography.titleMedium ) - ListItem( modifier = Modifier.fillMaxWidth().clickable { onClickAddLearningUnit() @@ -216,6 +232,25 @@ fun AssignmentEditScreen( } ) + if (uiState.showPlaylistButton) { + ListItem( + modifier = Modifier.fillMaxWidth().clickable { + onClickAddFromCurriculum() + }, + leadingContent = { + Icon( + imageVector = Icons.Default.LibraryAdd, + modifier = Modifier.size(40.dp).padding(8.dp), + contentDescription = "", + ) + }, + headlineContent = { + Text( + text = stringResource(Res.string.add_from_playlist), + ) + } + ) + } assignment?.learningUnits?.forEach { learningUnit -> val learningUnitInfoFlow = remember( uiState.learningUnitInfoFlow, learningUnit.learningUnitManifestUrl @@ -256,8 +291,5 @@ fun AssignmentEditScreen( }, ) } - - } - } diff --git a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml index d5d0d5c78..fb90325af 100644 --- a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml +++ b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml @@ -479,5 +479,6 @@ copy playlist Make a copy %1$d sections • %2$d items - + Add From Playlist + Select Playlist Unit diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index 3a932c1cc..8a389ead1 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -93,10 +93,13 @@ object AssignmentList : RespectAppRoute data class AssignmentDetail( val uid: String, ) : RespectAppRoute + + @Serializable data class AssignmentEdit( val guid: String? = null, private val learningUnitsJson: String? = null, + private val availablePlaylistsJson: String? = null, ) : RespectAppRoute { @Transient @@ -108,27 +111,59 @@ data class AssignmentEdit( } } + @Transient + val availablePlaylists: List? = availablePlaylistsJson?.let { jsonStr -> + try { + Json.decodeFromString( + ListSerializer(CurriculumMapping.serializer()), + jsonStr + ) + } catch (e: Exception) { + null + } + } + companion object { fun create( uid: String?, - learningUnitSelected: LearningUnitSelection? = null + learningUnitSelected: LearningUnitSelection? = null, + availablePlaylists: List? = null, ): AssignmentEdit { val learningUnits = learningUnitSelected?.let { listOf(it) } return AssignmentEdit( guid = uid, learningUnitsJson = learningUnits?.let { - Json.encodeToString(it) + Json.encodeToString( + ListSerializer(LearningUnitSelection.serializer()), + it + ) + }, + availablePlaylistsJson = availablePlaylists?.let { + Json.encodeToString( + ListSerializer(CurriculumMapping.serializer()), + it + ) } ) } fun createWithMultipleLessons( uid: String?, - learningUnits: List + learningUnits: List, + availablePlaylists: List? = null, ): AssignmentEdit { return AssignmentEdit( guid = uid, - learningUnitsJson = Json.encodeToString(learningUnits) + learningUnitsJson = Json.encodeToString( + ListSerializer(LearningUnitSelection.serializer()), + learningUnits + ), + availablePlaylistsJson = availablePlaylists?.let { + Json.encodeToString( + ListSerializer(CurriculumMapping.serializer()), + it + ) + } ) } } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt index 93811aa76..3fb24cd6e 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt @@ -110,6 +110,11 @@ class AppLauncherViewModel( val savedMappings = loadMappingsFromSavedState(savedStateHandle) playlistListViewModel.setMappings(savedMappings) + viewModelScope.launch { + playlistListViewModel.uiState.collect { state -> + cachedPlaylists = state.mappings + } + } viewModelScope.launch { accountManager.selectedAccountAndPersonFlow.collect { selected -> @@ -222,5 +227,6 @@ class AppLauncherViewModel( companion object { const val KEY_MAPPINGS_LIST = "mappings_list" + var cachedPlaylists: List = emptyList() } } \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/detail/AssignmentDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/detail/AssignmentDetailViewModel.kt index 0a36c957b..0f4d669be 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/detail/AssignmentDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/detail/AssignmentDetailViewModel.kt @@ -43,6 +43,7 @@ import world.respect.shared.util.ext.asUiText import world.respect.shared.util.ext.isAdminOrTeacher import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.FabUiState +import world.respect.shared.viewmodel.apps.launcher.AppLauncherViewModel data class AssignmentDetailUiState( val assignment: DataLoadState = DataLoadingState(), @@ -136,7 +137,12 @@ class AssignmentDetailViewModel( fun onClickEdit() { _navCommandFlow.tryEmit( - NavCommand.Navigate(AssignmentEdit.create(uid = route.uid)) + NavCommand.Navigate( + AssignmentEdit.create( + uid = route.uid, + availablePlaylists = AppLauncherViewModel.cachedPlaylists + ) + ) ) } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt index b326eaf08..61ec42832 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch +import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.json.Json import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject @@ -28,7 +29,12 @@ import world.respect.datalayer.school.model.Assignment import world.respect.datalayer.school.model.AssignmentAssigneeRef import world.respect.datalayer.school.model.AssignmentLearningUnitRef import world.respect.datalayer.school.model.Clazz +import world.respect.lib.opds.model.LangMap +import world.respect.lib.opds.model.OpdsFeedMetadata +import world.respect.lib.opds.model.OpdsGroup import world.respect.lib.opds.model.OpdsPublication +import world.respect.lib.opds.model.ReadiumLink +import world.respect.lib.opds.model.ReadiumMetadata import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.domain.school.SchoolPrimaryKeyGenerator import world.respect.shared.generated.resources.Res @@ -47,6 +53,7 @@ import world.respect.shared.util.LaunchDebouncer import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.ActionBarButtonUiState +import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping import world.respect.shared.viewmodel.learningunit.LearningUnitSelection import kotlin.time.Clock @@ -57,6 +64,7 @@ data class AssignmentEditUiState( val classOptions: List = emptyList(), val classError: UiText? = null, val learningUnitInfoFlow: (Url) -> Flow> = { flowOf(DataLoadingState()) }, + val showPlaylistButton: Boolean = true, ) { val fieldsEnabled: Boolean get() = assignment.isReadyAndSettled() @@ -115,6 +123,7 @@ class AssignmentEditViewModel( hideBottomNavigation = true, ) } + viewModelScope.launch { resultReturner.filteredResultFlowForKey(KEY_LEARNING_UNIT).collect { result -> val learningUnit = result.result as? LearningUnitSelection ?: return@collect @@ -133,6 +142,35 @@ class AssignmentEditViewModel( } } } + viewModelScope.launch { + resultReturner.filteredResultFlowForKey(KEY_PLAYLIST_SELECTION).collect { result -> + val mapping = result.result as? CurriculumMapping ?: return@collect + val group = convertMappingToOpdsGroup(mapping) + + viewModelScope.launch { + val assignment = _uiState.value.assignment.dataOrNull() ?: return@launch + val newLearningUnits = loadLessonsFromOpdsGroup(group) + + val existingUrls = assignment.learningUnits.map { + it.learningUnitManifestUrl + }.toSet() + + val uniqueNewUnits = newLearningUnits.filter { + it.learningUnitManifestUrl !in existingUrls + } + + _uiState.update { prev -> + prev.copy( + assignment = DataReadyState( + assignment.copy( + learningUnits = assignment.learningUnits + uniqueNewUnits + ) + ) + ) + } + } + } + } launchWithLoadingIndicator { val classes = schoolDataSource.classDataSource.list( @@ -168,6 +206,22 @@ class AssignmentEditViewModel( } } ) + + viewModelScope.launch { + schoolDataSource.assignmentDataSource.findByGuidAsFlow( + route.guid + ).collect { assignmentState -> + if (assignmentState is DataReadyState) { + val currentLearningUnits = _uiState.value.assignment.dataOrNull()?.learningUnits + val newLearningUnits = assignmentState.data.learningUnits + if (currentLearningUnits != newLearningUnits) { + _uiState.update { prev -> + prev.copy(assignment = assignmentState) + } + } + } + } + } }else { val initialLearningUnits = route.learningUnitSelectedList?.map { it.toRef() } ?: emptyList() @@ -187,6 +241,80 @@ class AssignmentEditViewModel( } } + /** + * Convert CurriculumMapping to OpdsGroup for processing multiple lessons + */ + private fun convertMappingToOpdsGroup(mapping: CurriculumMapping): OpdsGroup { + return OpdsGroup( + metadata = OpdsFeedMetadata( + title = mapping.title + ), + publications = mapping.sections.flatMap { section -> + section.items.map { link -> + OpdsPublication( + metadata = ReadiumMetadata( + title = mapOf("en" to (link.title ?: "Untitled")) as LangMap, + ), + links = listOfNotNull( + ReadiumLink( + href = link.href, + rel = listOf("http://opds-spec.org/acquisition"), + ), + link.appManifestUrl?.let { + ReadiumLink( + href = it.toString(), + rel = listOf("http://opds-spec.org/compatible-app"), + ) + } + ) + ) + } + } + ) + } + + fun onClickAddFromCurriculum() { + _navCommandFlow.tryEmit( + NavCommand.Navigate( + RespectAppLauncher.create( + resultDest = RouteResultDest( + resultPopUpTo = route, + resultKey = KEY_PLAYLIST_SELECTION, + ) + ) + ) + ) + } + + private suspend fun loadLessonsFromOpdsGroup(group: OpdsGroup): List { + val publications = group.publications ?: emptyList() + + return publications.mapNotNull { publication -> + try { + val acquisitionLink = publication.links.firstOrNull { link -> + link.rel?.any { it.startsWith("http://opds-spec.org/acquisition") } == true + } ?: return@mapNotNull null + + val publicationUrl = Url(acquisitionLink.href) + + val appManifestLink = publication.links.firstOrNull { link -> + link.rel?.contains("http://opds-spec.org/compatible-app") == true + } + + val appManifestUrl = appManifestLink?.let { Url(it.href) } + ?: return@mapNotNull null + + AssignmentLearningUnitRef( + learningUnitManifestUrl = publicationUrl, + appManifestUrl = appManifestUrl + ) + } catch (e: Exception) { + e.printStackTrace() + null + } + } + } + fun learningUnitInfoFlowFor(url: Url): Flow> { return respectAppDataSource.opdsDataSource.loadOpdsPublication( url = url, params = DataLoadParams(), null, null @@ -221,7 +349,10 @@ class AssignmentEditViewModel( } debouncer.launch(DEFAULT_SAVED_STATE_KEY) { - savedStateHandle[DEFAULT_SAVED_STATE_KEY] = json.encodeToString(assignment) + savedStateHandle[DEFAULT_SAVED_STATE_KEY] = json.encodeToString( + Assignment.serializer(), + assignment + ) } } @@ -244,9 +375,7 @@ class AssignmentEditViewModel( ) } - fun onClickRemoveLearningUnit( - ref: AssignmentLearningUnitRef - ) { + fun onClickRemoveLearningUnit(ref: AssignmentLearningUnitRef) { val assignment = uiState.value.assignment.dataOrNull() ?: return _uiState.update { prev -> @@ -302,5 +431,6 @@ class AssignmentEditViewModel( companion object { const val KEY_LEARNING_UNIT = "result_learning_unit" + const val KEY_PLAYLIST_SELECTION = "result_playlist_selection" } } \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/list/AssignmentListViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/list/AssignmentListViewModel.kt index ea6a6db74..4a6740518 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/list/AssignmentListViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/list/AssignmentListViewModel.kt @@ -34,6 +34,7 @@ import world.respect.shared.util.ext.asUiText import world.respect.shared.util.ext.isAdminOrTeacher import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.FabUiState +import world.respect.shared.viewmodel.apps.launcher.AppLauncherViewModel data class AssignmentListUiState( val assignments: IPagingSourceFactory = EmptyPagingSourceFactory(), @@ -107,7 +108,10 @@ class AssignmentListViewModel( fun onClickAdd() { _navCommandFlow.tryEmit( NavCommand.Navigate( - AssignmentEdit.create(uid = null) + AssignmentEdit.create( + uid = null, + availablePlaylists = AppLauncherViewModel.cachedPlaylists + ) ) ) } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/PlaylistListViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/PlaylistListViewModel.kt index 01636cc7d..aa78824dc 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/PlaylistListViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/PlaylistListViewModel.kt @@ -13,6 +13,7 @@ import world.respect.shared.navigation.NavCommand import world.respect.shared.navigation.NavResultReturner import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel +import world.respect.shared.viewmodel.apps.launcher.AppLauncherViewModel import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping import world.respect.shared.generated.resources.Res @@ -53,6 +54,7 @@ class PlaylistListViewModel( fun setMappings(mappings: List) { _uiState.update { it.copy(mappings = mappings) } + AppLauncherViewModel.cachedPlaylists = mappings } fun onFilterSelected(index: Int) { @@ -86,6 +88,7 @@ class PlaylistListViewModel( fun removeMapping(mapping: CurriculumMapping): List { val updated = _uiState.value.mappings.filter { it.uid != mapping.uid } _uiState.update { it.copy(mappings = updated) } + AppLauncherViewModel.cachedPlaylists = updated return updated } @@ -105,5 +108,6 @@ class PlaylistListViewModel( } _uiState.update { it.copy(mappings = currentMappings) } + AppLauncherViewModel.cachedPlaylists = currentMappings } } \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt index c84981dd8..bacfa01a5 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt @@ -37,6 +37,7 @@ import world.respect.shared.util.ext.asUiText import world.respect.shared.util.ext.resolve import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.getTitle +import world.respect.shared.viewmodel.apps.launcher.AppLauncherViewModel import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingSectionUiState import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping @@ -220,7 +221,8 @@ class LearningUnitDetailViewModel( NavCommand.Navigate( destination = AssignmentEdit.createWithMultipleLessons( uid = null, - learningUnits = learningUnitSelections + learningUnits = learningUnitSelections, + availablePlaylists = AppLauncherViewModel.cachedPlaylists ) ) ) @@ -231,7 +233,8 @@ class LearningUnitDetailViewModel( NavCommand.Navigate( destination = AssignmentEdit.create( uid = null, - learningUnitSelected = null + learningUnitSelected = null, + availablePlaylists = AppLauncherViewModel.cachedPlaylists ) ) ) @@ -245,8 +248,9 @@ class LearningUnitDetailViewModel( learningUnitSelected = LearningUnitSelection( learningUnitManifestUrl = route.learningUnitManifestUrl, selectedPublication = publicationVal, - appManifestUrl = route.appManifestUrl - ) + appManifestUrl = route.appManifestUrl, + ), + availablePlaylists = AppLauncherViewModel.cachedPlaylists ) ) ) @@ -266,7 +270,8 @@ class LearningUnitDetailViewModel( NavCommand.Navigate( destination = AssignmentEdit.createWithMultipleLessons( uid = null, - learningUnits = learningUnitSelections + learningUnits = learningUnitSelections, + availablePlaylists = AppLauncherViewModel.cachedPlaylists ) ) ) From 7c073595a73a065a1df21a6d5249a2c955105448 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Tue, 30 Dec 2025 14:47:58 +0530 Subject: [PATCH 37/58] Navigation change to playlist tab onclick add from playlist in assignmentedit --- .../shared/viewmodel/apps/launcher/AppLauncherViewModel.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt index 3fb24cd6e..2fa15ed79 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt @@ -46,6 +46,7 @@ import world.respect.shared.util.ext.asUiText import world.respect.shared.util.ext.isAdmin import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.FabUiState +import world.respect.shared.viewmodel.assignment.edit.AssignmentEditViewModel import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel import world.respect.shared.viewmodel.curriculum.mapping.list.PlaylistListViewModel import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping @@ -95,11 +96,12 @@ class AppLauncherViewModel( showBackButton = route.resultDest != null, ) } - + val showPlaylistTab = route.resultDest?.resultKey == AssignmentEditViewModel.KEY_PLAYLIST_SELECTION _uiState.update { prev -> prev.copy( respectAppForSchoolApp = this@AppLauncherViewModel::respectAppForSchoolApp, apps = pagingSourceHolder, + selectedTabIndex = if (showPlaylistTab) 1 else 0, ) } viewModelScope.launch { From b6f83022508a9fde0a9983881478d18287d97874 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Wed, 31 Dec 2025 10:37:27 +0530 Subject: [PATCH 38/58] Add filtermappings --- .../mapping/list/PlaylistListScreen.kt | 4 +- .../mapping/edit/PlaylistEditViewModel.kt | 50 ++++++++++------- .../mapping/list/PlaylistListViewModel.kt | 54 +++++++++++++------ .../mapping/model/CurriculumMapping.kt | 6 ++- 4 files changed, 76 insertions(+), 38 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/PlaylistListScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/PlaylistListScreen.kt index 828b9b0f0..67cba9112 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/PlaylistListScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/PlaylistListScreen.kt @@ -81,7 +81,7 @@ fun PlaylistListScreenContent( .fillMaxSize() .weight(1f) ) { - if (uiState.mappings.isEmpty()) { + if (uiState.filteredMappings.isEmpty()) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center @@ -101,7 +101,7 @@ fun PlaylistListScreenContent( ) ) { items( - items = uiState.mappings, + items = uiState.filteredMappings, key = { mapping -> mapping.uid } ) { mapping -> MappingListItem( diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/PlaylistEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/PlaylistEditViewModel.kt index 608984141..86fc76067 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/PlaylistEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/PlaylistEditViewModel.kt @@ -8,11 +8,14 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch import kotlinx.serialization.json.Json +import org.koin.core.component.KoinScopeComponent +import org.koin.core.scope.Scope import world.respect.datalayer.DataLoadParams import world.respect.datalayer.DataLoadState import world.respect.datalayer.RespectAppDataSource @@ -21,6 +24,7 @@ import world.respect.lib.opds.model.findIcons import world.respect.libutil.ext.moveItem import world.respect.libutil.ext.updateAtIndex import world.respect.libutil.ext.resolve +import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.create_playlist import world.respect.shared.generated.resources.edit_playlist @@ -78,8 +82,10 @@ class CurriculumMappingEditViewModel( private val resultReturner: NavResultReturner, private val json: Json, private val respectAppDataSource: RespectAppDataSource, -) : RespectViewModel(savedStateHandle) { + private val accountManager: RespectAccountManager, +) : RespectViewModel(savedStateHandle), KoinScopeComponent { + override val scope: Scope = accountManager.requireActiveAccountScope() private val route: CurriculumMappingEdit = savedStateHandle.toRoute() private val mappingUid = route.textbookUid @@ -324,7 +330,6 @@ class CurriculumMappingEditViewModel( } } } - fun onClickSave() { val mapping = _uiState.value.mapping ?: return if (mapping.title.isBlank()) { @@ -332,27 +337,34 @@ class CurriculumMappingEditViewModel( return } - val finalMapping = if (mapping.uid == 0L) { - mapping.copy(uid = System.currentTimeMillis()) - } else { - mapping - } + viewModelScope.launch { + val currentSession = accountManager.selectedAccountAndPersonFlow.first() + + val finalMapping = if (mapping.uid == 0L) { + mapping.copy( + uid = System.currentTimeMillis(), + createdBy = currentSession?.person?.guid, + schoolUrl= currentSession?.session?.account?.school?.self + ) + } else { + mapping + } - resultReturner.sendResult( - NavResult( - key = KEY_SAVED_MAPPING, - result = finalMapping + resultReturner.sendResult( + NavResult( + key = KEY_SAVED_MAPPING, + result = finalMapping + ) ) - ) - _navCommandFlow.tryEmit( - NavCommand.Navigate( - destination = LearningUnitDetail.createFromMapping(finalMapping), - popUpTo = RespectAppLauncher(), - popUpToInclusive = false + _navCommandFlow.tryEmit( + NavCommand.Navigate( + destination = LearningUnitDetail.createFromMapping(finalMapping), + popUpTo = RespectAppLauncher(), + popUpToInclusive = false + ) ) - ) + } } - fun onClearError() { _uiState.update { it.copy(titleError = null) } } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/PlaylistListViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/PlaylistListViewModel.kt index aa78824dc..331bec1df 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/PlaylistListViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/PlaylistListViewModel.kt @@ -2,10 +2,14 @@ package world.respect.shared.viewmodel.curriculum.mapping.list import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import io.ktor.http.Url import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.koin.core.component.KoinScopeComponent +import org.koin.core.scope.Scope +import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.navigation.CurriculumMappingEdit import world.respect.shared.navigation.EnterLink import world.respect.shared.navigation.LearningUnitDetail @@ -13,7 +17,6 @@ import world.respect.shared.navigation.NavCommand import world.respect.shared.navigation.NavResultReturner import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel -import world.respect.shared.viewmodel.apps.launcher.AppLauncherViewModel import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping import world.respect.shared.generated.resources.Res @@ -24,18 +27,48 @@ data class PlaylistListUiState( val mappings: List = emptyList(), val selectedFilterIndex: Int = 0, val error: UiText? = null, -) + val currentUserGuid: String? = null, + val currentSchoolUrl: Url? = null, +) { + val filteredMappings: List + get() = when (selectedFilterIndex) { + 0 -> mappings + 1 -> mappings.filter { mapping -> + mapping.isSchoolWide && + mapping.schoolUrl == currentSchoolUrl && + mapping.createdBy != currentUserGuid + } + 2 -> mappings.filter { mapping -> + mapping.createdBy == currentUserGuid + } + else -> mappings + } +} class PlaylistListViewModel( savedStateHandle: SavedStateHandle, private val resultReturner: NavResultReturner, -) : RespectViewModel(savedStateHandle) { + private val accountManager: RespectAccountManager, +) : RespectViewModel(savedStateHandle), KoinScopeComponent { + + override val scope: Scope = accountManager.requireActiveAccountScope() private val _uiState = MutableStateFlow(PlaylistListUiState()) val uiState = _uiState.asStateFlow() init { + viewModelScope.launch { + accountManager.selectedAccountAndPersonFlow.collect { sessionAndPerson -> + _uiState.update { prev -> + prev.copy( + currentUserGuid = sessionAndPerson?.person?.guid, + currentSchoolUrl = sessionAndPerson?.session?.account?.school?.self + ) + } + } + } + viewModelScope.launch { resultReturner.filteredResultFlowForKey( CurriculumMappingEditViewModel.KEY_SAVED_MAPPING @@ -54,13 +87,11 @@ class PlaylistListViewModel( fun setMappings(mappings: List) { _uiState.update { it.copy(mappings = mappings) } - AppLauncherViewModel.cachedPlaylists = mappings } fun onFilterSelected(index: Int) { _uiState.update { it.copy(selectedFilterIndex = index) } } - fun onClickMapping(mapping: CurriculumMapping) { _navCommandFlow.tryEmit( NavCommand.Navigate( @@ -68,7 +99,6 @@ class PlaylistListViewModel( ) ) } - fun onClickAddNew() { _navCommandFlow.tryEmit( NavCommand.Navigate( @@ -85,11 +115,9 @@ class PlaylistListViewModel( ) } - fun removeMapping(mapping: CurriculumMapping): List { + fun removeMapping(mapping: CurriculumMapping) { val updated = _uiState.value.mappings.filter { it.uid != mapping.uid } _uiState.update { it.copy(mappings = updated) } - AppLauncherViewModel.cachedPlaylists = updated - return updated } private fun addOrUpdateMapping(mapping: CurriculumMapping) { @@ -99,15 +127,9 @@ class PlaylistListViewModel( if (existingIndex >= 0) { currentMappings[existingIndex] = mapping } else { - val newMapping = if (mapping.uid == 0L) { - mapping.copy(uid = System.currentTimeMillis()) - } else { - mapping - } - currentMappings.add(newMapping) + currentMappings.add(mapping) } _uiState.update { it.copy(mappings = currentMappings) } - AppLauncherViewModel.cachedPlaylists = currentMappings } } \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMapping.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMapping.kt index 34253f1bc..75235cf11 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMapping.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMapping.kt @@ -1,5 +1,6 @@ package world.respect.shared.viewmodel.curriculum.mapping.model +import io.ktor.http.Url import kotlinx.serialization.Serializable @Serializable @@ -7,5 +8,8 @@ data class CurriculumMapping( val uid: Long = System.currentTimeMillis(), val title: String = "", val description: String = "", - val sections: List = emptyList() + val sections: List = emptyList(), + val createdBy: String? = null, + val isSchoolWide: Boolean = false, + val schoolUrl: Url? = null, ) From a96d8751413c9173b98cd3d68914dbe21580cbe1 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Wed, 31 Dec 2025 13:15:18 +0530 Subject: [PATCH 39/58] Refactor the asignmentedit viewmodel --- .../assignment/edit/AssignmentEditScreen.kt | 17 +-- .../assignment/AssignmentOpdsAdapters.kt | 39 +++++++ .../edit/AssignmentEditViewModel.kt | 103 +++--------------- .../mapping/CurriculumMappingAdapter.kt | 34 +++++- 4 files changed, 93 insertions(+), 100 deletions(-) create mode 100644 respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/AssignmentOpdsAdapters.kt diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt index 3efddc4dc..6c29e1e19 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt @@ -2,21 +2,14 @@ package world.respect.app.view.assignment.edit import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.LibraryAdd -import androidx.compose.material3.AlertDialog import androidx.compose.material3.ListItem import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api @@ -28,17 +21,14 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import kotlinx.coroutines.Dispatchers @@ -68,7 +58,6 @@ import world.respect.shared.generated.resources.lesson_assessment import world.respect.shared.generated.resources.name import world.respect.shared.generated.resources.no_playlists_yet import world.respect.shared.generated.resources.required -import world.respect.shared.generated.resources.select_curriculum_unit import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.app.appstate.getTitle import world.respect.shared.viewmodel.assignment.edit.AssignmentEditUiState @@ -87,7 +76,7 @@ fun AssignmentEditScreen( onClickAddLearningUnit = viewModel::onClickAddLearningUnit, onAssigneeClassSelected = viewModel::onAssigneeClassSelected, onClickRemoveLearningUnit = viewModel::onClickRemoveLearningUnit, - onClickAddFromCurriculum = viewModel::onClickAddFromCurriculum, + onClickAddFromPlaylists = viewModel::onClickAddFromPlaylists, ) } @@ -100,7 +89,7 @@ fun AssignmentEditScreen( onAssigneeClassSelected: (Clazz) -> Unit, onClickAddLearningUnit: () -> Unit, onClickRemoveLearningUnit: (AssignmentLearningUnitRef) -> Unit, - onClickAddFromCurriculum: () -> Unit, + onClickAddFromPlaylists: () -> Unit, ) { val assignment = uiState.assignment.dataOrNull() val filteredOptions = if(uiState.assigneeText.isNotBlank()) { @@ -235,7 +224,7 @@ fun AssignmentEditScreen( if (uiState.showPlaylistButton) { ListItem( modifier = Modifier.fillMaxWidth().clickable { - onClickAddFromCurriculum() + onClickAddFromPlaylists() }, leadingContent = { Icon( diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/AssignmentOpdsAdapters.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/AssignmentOpdsAdapters.kt new file mode 100644 index 000000000..50457064c --- /dev/null +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/AssignmentOpdsAdapters.kt @@ -0,0 +1,39 @@ +package world.respect.shared.viewmodel.assignment + +import io.ktor.http.Url +import world.respect.datalayer.school.model.AssignmentLearningUnitRef +import world.respect.lib.opds.model.OpdsGroup + +/** + * Convert OpdsGroup publications to AssignmentLearningUnitRef list. + * Extracts learning unit manifest URLs and their corresponding app manifest URLs + * from OPDS publications. + */ +fun OpdsGroup.toAssignmentLearningUnitRefs(): List { + val publications = this.publications ?: emptyList() + + return publications.mapNotNull { publication -> + try { + val acquisitionLink = publication.links.firstOrNull { link -> + link.rel?.any { it.startsWith("http://opds-spec.org/acquisition") } == true + } ?: return@mapNotNull null + + val publicationUrl = Url(acquisitionLink.href) + + val appManifestLink = publication.links.firstOrNull { link -> + link.rel?.contains("http://opds-spec.org/compatible-app") == true + } + + val appManifestUrl = appManifestLink?.let { Url(it.href) } + ?: return@mapNotNull null + + AssignmentLearningUnitRef( + learningUnitManifestUrl = publicationUrl, + appManifestUrl = appManifestUrl + ) + } catch (e: Exception) { + e.printStackTrace() + null + } + } +} \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt index 61ec42832..c43439e7e 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt @@ -11,7 +11,6 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch -import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.json.Json import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject @@ -29,12 +28,7 @@ import world.respect.datalayer.school.model.Assignment import world.respect.datalayer.school.model.AssignmentAssigneeRef import world.respect.datalayer.school.model.AssignmentLearningUnitRef import world.respect.datalayer.school.model.Clazz -import world.respect.lib.opds.model.LangMap -import world.respect.lib.opds.model.OpdsFeedMetadata -import world.respect.lib.opds.model.OpdsGroup import world.respect.lib.opds.model.OpdsPublication -import world.respect.lib.opds.model.ReadiumLink -import world.respect.lib.opds.model.ReadiumMetadata import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.domain.school.SchoolPrimaryKeyGenerator import world.respect.shared.generated.resources.Res @@ -53,7 +47,9 @@ import world.respect.shared.util.LaunchDebouncer import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.ActionBarButtonUiState +import world.respect.shared.viewmodel.assignment.toAssignmentLearningUnitRefs import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping +import world.respect.shared.viewmodel.curriculum.mapping.toOpdsGroup import world.respect.shared.viewmodel.learningunit.LearningUnitSelection import kotlin.time.Clock @@ -145,29 +141,27 @@ class AssignmentEditViewModel( viewModelScope.launch { resultReturner.filteredResultFlowForKey(KEY_PLAYLIST_SELECTION).collect { result -> val mapping = result.result as? CurriculumMapping ?: return@collect - val group = convertMappingToOpdsGroup(mapping) + val group = mapping.toOpdsGroup() + val newLearningUnits = group.toAssignmentLearningUnitRefs() - viewModelScope.launch { - val assignment = _uiState.value.assignment.dataOrNull() ?: return@launch - val newLearningUnits = loadLessonsFromOpdsGroup(group) + val assignment = _uiState.value.assignment.dataOrNull() ?: return@collect - val existingUrls = assignment.learningUnits.map { - it.learningUnitManifestUrl - }.toSet() + val existingUrls = assignment.learningUnits.map { + it.learningUnitManifestUrl + }.toSet() - val uniqueNewUnits = newLearningUnits.filter { - it.learningUnitManifestUrl !in existingUrls - } + val uniqueNewUnits = newLearningUnits.filter { + it.learningUnitManifestUrl !in existingUrls + } - _uiState.update { prev -> - prev.copy( - assignment = DataReadyState( - assignment.copy( - learningUnits = assignment.learningUnits + uniqueNewUnits - ) + _uiState.update { prev -> + prev.copy( + assignment = DataReadyState( + assignment.copy( + learningUnits = assignment.learningUnits + uniqueNewUnits ) ) - } + ) } } } @@ -241,39 +235,7 @@ class AssignmentEditViewModel( } } - /** - * Convert CurriculumMapping to OpdsGroup for processing multiple lessons - */ - private fun convertMappingToOpdsGroup(mapping: CurriculumMapping): OpdsGroup { - return OpdsGroup( - metadata = OpdsFeedMetadata( - title = mapping.title - ), - publications = mapping.sections.flatMap { section -> - section.items.map { link -> - OpdsPublication( - metadata = ReadiumMetadata( - title = mapOf("en" to (link.title ?: "Untitled")) as LangMap, - ), - links = listOfNotNull( - ReadiumLink( - href = link.href, - rel = listOf("http://opds-spec.org/acquisition"), - ), - link.appManifestUrl?.let { - ReadiumLink( - href = it.toString(), - rel = listOf("http://opds-spec.org/compatible-app"), - ) - } - ) - ) - } - } - ) - } - - fun onClickAddFromCurriculum() { + fun onClickAddFromPlaylists() { _navCommandFlow.tryEmit( NavCommand.Navigate( RespectAppLauncher.create( @@ -286,35 +248,6 @@ class AssignmentEditViewModel( ) } - private suspend fun loadLessonsFromOpdsGroup(group: OpdsGroup): List { - val publications = group.publications ?: emptyList() - - return publications.mapNotNull { publication -> - try { - val acquisitionLink = publication.links.firstOrNull { link -> - link.rel?.any { it.startsWith("http://opds-spec.org/acquisition") } == true - } ?: return@mapNotNull null - - val publicationUrl = Url(acquisitionLink.href) - - val appManifestLink = publication.links.firstOrNull { link -> - link.rel?.contains("http://opds-spec.org/compatible-app") == true - } - - val appManifestUrl = appManifestLink?.let { Url(it.href) } - ?: return@mapNotNull null - - AssignmentLearningUnitRef( - learningUnitManifestUrl = publicationUrl, - appManifestUrl = appManifestUrl - ) - } catch (e: Exception) { - e.printStackTrace() - null - } - } - } - fun learningUnitInfoFlowFor(url: Url): Flow> { return respectAppDataSource.opdsDataSource.loadOpdsPublication( url = url, params = DataLoadParams(), null, null diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/CurriculumMappingAdapter.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/CurriculumMappingAdapter.kt index d7f5bf874..c45b7747b 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/CurriculumMappingAdapter.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/CurriculumMappingAdapter.kt @@ -6,10 +6,13 @@ package world.respect.shared.viewmodel.curriculum.mapping //e.g. have CurriculumMapping.toOpds (convert from CurriculumMapping data class to Opds) // and OpdsFeed.toCurriculumMapping (convert from OpdsFeed to CurriculumMapping) +import world.respect.lib.opds.model.LangMap import world.respect.lib.opds.model.OpdsFeed import world.respect.lib.opds.model.OpdsFeedMetadata import world.respect.lib.opds.model.OpdsGroup +import world.respect.lib.opds.model.OpdsPublication import world.respect.lib.opds.model.ReadiumLink +import world.respect.lib.opds.model.ReadiumMetadata import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMappingSection import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMappingSectionLink @@ -44,6 +47,35 @@ fun CurriculumMapping.toOpds(selfLink: String): OpdsFeed { ) } +fun CurriculumMapping.toOpdsGroup(): OpdsGroup { + return OpdsGroup( + metadata = OpdsFeedMetadata( + title = this.title + ), + publications = this.sections.flatMap { section -> + section.items.map { link -> + OpdsPublication( + metadata = ReadiumMetadata( + title = mapOf("en" to (link.title ?: "")) as LangMap, + ), + links = listOfNotNull( + ReadiumLink( + href = link.href, + rel = listOf("http://opds-spec.org/acquisition"), + ), + link.appManifestUrl?.let { + ReadiumLink( + href = it.toString(), + rel = listOf("http://opds-spec.org/compatible-app"), + ) + } + ) + ) + } + } + ) +} + fun OpdsFeed.toCurriculumMapping(): CurriculumMapping { return CurriculumMapping( uid = System.currentTimeMillis(), @@ -63,4 +95,4 @@ fun OpdsFeed.toCurriculumMapping(): CurriculumMapping { ) } ?: emptyList() ) -} +} \ No newline at end of file From 1486927dc7844394403ff64b87c62fdb8e19a42d Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Wed, 31 Dec 2025 14:46:51 +0530 Subject: [PATCH 40/58] Add "Add task to assignment" --- .../app/view/assignment/edit/AssignmentEditScreen.kt | 3 --- .../commonMain/composeResources/values/strings.xml | 1 + .../viewmodel/apps/launcher/AppLauncherViewModel.kt | 12 +++++++++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt index 6c29e1e19..324767be5 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt @@ -44,19 +44,16 @@ import world.respect.datalayer.ext.dataOrNull import world.respect.datalayer.school.model.Assignment import world.respect.datalayer.school.model.AssignmentLearningUnitRef import world.respect.datalayer.school.model.Clazz -import world.respect.lib.opds.model.OpdsGroup import world.respect.lib.opds.model.findIcons import world.respect.libutil.ext.resolve import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.add_from_playlist import world.respect.shared.generated.resources.assignment_tasks -import world.respect.shared.generated.resources.cancel import world.respect.shared.generated.resources.clazz import world.respect.shared.generated.resources.delete import world.respect.shared.generated.resources.description import world.respect.shared.generated.resources.lesson_assessment import world.respect.shared.generated.resources.name -import world.respect.shared.generated.resources.no_playlists_yet import world.respect.shared.generated.resources.required import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.app.appstate.getTitle diff --git a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml index fb90325af..f3cb5ce50 100644 --- a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml +++ b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml @@ -481,4 +481,5 @@ %1$d sections • %2$d items Add From Playlist Select Playlist Unit + Add task to assignment diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt index 2fa15ed79..ace97ffc6 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt @@ -30,6 +30,7 @@ import world.respect.libutil.ext.resolve import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.domain.devmode.GetDevModeEnabledUseCase import world.respect.shared.generated.resources.Res +import world.respect.shared.generated.resources.add_task_to_assignment import world.respect.shared.generated.resources.app import world.respect.shared.generated.resources.empty_list_description_admin import world.respect.shared.generated.resources.empty_list_description_non_admin @@ -90,7 +91,16 @@ class AppLauncherViewModel( init { _appUiState.update { it.copy( - title = Res.string.home.asUiText(), + title = if (route.resultDest != null) { + when (route.resultDest.resultKey) { + AssignmentEditViewModel.KEY_PLAYLIST_SELECTION -> + Res.string.add_task_to_assignment.asUiText() + else -> + Res.string.home.asUiText() + } + } else { + Res.string.home.asUiText() + }, onClickSettings = ::onClickSettings, hideBottomNavigation = route.resultDest != null, showBackButton = route.resultDest != null, From 080c066d6ea7391e09e035e3e26e2fab426d6f28 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Wed, 31 Dec 2025 15:13:37 +0530 Subject: [PATCH 41/58] change the package name to playlists --- .../androidMain/kotlin/world/respect/AppKoinModule.kt | 4 ++-- .../kotlin/world/respect/app/app/AppNavHost.kt | 2 +- .../app/view/apps/launcher/AppLauncherScreen.kt | 2 +- .../view/curriculum/mapping/edit/PlaylistEditScreen.kt | 10 +++++----- .../view/curriculum/mapping/list/PlaylistListScreen.kt | 6 +++--- .../learningunit/detail/LearningUnitDetailScreen.kt | 4 ++-- .../world/respect/shared/navigation/AppRoutes.kt | 3 +-- .../viewmodel/apps/launcher/AppLauncherViewModel.kt | 6 ++---- .../assignment/edit/AssignmentEditViewModel.kt | 4 ++-- .../learningunit/detail/LearningUnitDetailViewModel.kt | 8 ++++---- .../mapping/CurriculumMappingAdapter.kt | 8 ++++---- .../mapping/edit/PlaylistEditViewModel.kt | 8 ++++---- .../mapping/list/PlaylistListViewModel.kt | 6 +++--- .../mapping/model/CurriculumMapping.kt | 2 +- .../mapping/model/CurriculumMappingSection.kt | 2 +- .../mapping/model/CurriculumMappingSectionLink.kt | 2 +- 16 files changed, 37 insertions(+), 40 deletions(-) rename respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/{curriculum => playlists}/mapping/CurriculumMappingAdapter.kt (91%) rename respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/{curriculum => playlists}/mapping/edit/PlaylistEditViewModel.kt (97%) rename respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/{curriculum => playlists}/mapping/list/PlaylistListViewModel.kt (95%) rename respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/{curriculum => playlists}/mapping/model/CurriculumMapping.kt (86%) rename respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/{curriculum => playlists}/mapping/model/CurriculumMappingSection.kt (78%) rename respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/{curriculum => playlists}/mapping/model/CurriculumMappingSectionLink.kt (85%) diff --git a/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt b/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt index f9e9aea62..65ed9bd71 100644 --- a/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt +++ b/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt @@ -206,10 +206,10 @@ import world.respect.shared.viewmodel.report.list.ReportTemplateListViewModel import world.respect.sharedse.domain.account.authenticatepassword.AuthenticatePasswordUseCaseDbImpl import java.io.File import world.respect.shared.viewmodel.settings.SettingsViewModel -import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel +import world.respect.shared.viewmodel.playlists.mapping.edit.CurriculumMappingEditViewModel import world.respect.shared.viewmodel.schooldirectory.edit.SchoolDirectoryEditViewModel import world.respect.shared.viewmodel.schooldirectory.list.SchoolDirectoryListViewModel -import world.respect.shared.viewmodel.curriculum.mapping.list.PlaylistListViewModel +import world.respect.shared.viewmodel.playlists.mapping.list.PlaylistListViewModel const val SHARED_PREF_SETTINGS_NAME = "respect_settings3_" diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt index 30a64a34b..9a0432891 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt @@ -137,7 +137,7 @@ import world.respect.app.view.settings.SettingsScreenForViewModel import world.respect.app.view.curriculum.mapping.edit.CurriculumMappingEditScreenForViewModel import world.respect.shared.viewmodel.settings.SettingsViewModel -import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel +import world.respect.shared.viewmodel.playlists.mapping.edit.CurriculumMappingEditViewModel import world.respect.shared.navigation.Settings import world.respect.shared.navigation.CurriculumMappingEdit diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/launcher/AppLauncherScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/launcher/AppLauncherScreen.kt index 07698ed5a..7f52b3306 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/launcher/AppLauncherScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/launcher/AppLauncherScreen.kt @@ -31,7 +31,7 @@ import world.respect.shared.generated.resources.* import world.respect.shared.viewmodel.app.appstate.getTitle import world.respect.shared.viewmodel.apps.launcher.AppLauncherUiState import world.respect.shared.viewmodel.apps.launcher.AppLauncherViewModel -import world.respect.shared.viewmodel.curriculum.mapping.list.PlaylistListViewModel +import world.respect.shared.viewmodel.playlists.mapping.list.PlaylistListViewModel @Composable diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/PlaylistEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/PlaylistEditScreen.kt index 4651f29ec..399a93200 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/PlaylistEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/PlaylistEditScreen.kt @@ -42,11 +42,11 @@ import world.respect.shared.generated.resources.sections import world.respect.shared.generated.resources.title import world.respect.shared.generated.resources.section_name import world.respect.shared.util.ext.asUiText -import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditUiState -import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel -import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingSectionUiState -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMappingSection -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMappingSectionLink +import world.respect.shared.viewmodel.playlists.mapping.edit.CurriculumMappingEditUiState +import world.respect.shared.viewmodel.playlists.mapping.edit.CurriculumMappingEditViewModel +import world.respect.shared.viewmodel.playlists.mapping.edit.CurriculumMappingSectionUiState +import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMappingSection +import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMappingSectionLink import androidx.compose.ui.draw.alpha diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/PlaylistListScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/PlaylistListScreen.kt index 67cba9112..7bc782f6f 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/PlaylistListScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/PlaylistListScreen.kt @@ -21,10 +21,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.jetbrains.compose.resources.stringResource import world.respect.shared.generated.resources.* -import world.respect.shared.viewmodel.curriculum.mapping.list.PlaylistListUiState -import world.respect.shared.viewmodel.curriculum.mapping.list.PlaylistListViewModel +import world.respect.shared.viewmodel.playlists.mapping.list.PlaylistListUiState +import world.respect.shared.viewmodel.playlists.mapping.list.PlaylistListViewModel -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping +import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMapping @Composable fun PlaylistListScreen( diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt index 32bcb1598..988e6c2fa 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt @@ -36,8 +36,8 @@ import world.respect.datalayer.DataLoadingState import world.respect.datalayer.ext.dataOrNull import world.respect.shared.generated.resources.* import world.respect.shared.viewmodel.app.appstate.getTitle -import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingSectionUiState -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMappingSectionLink +import world.respect.shared.viewmodel.playlists.mapping.edit.CurriculumMappingSectionUiState +import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMappingSectionLink import world.respect.shared.viewmodel.learningunit.detail.LearningUnitDetailUiState import world.respect.shared.viewmodel.learningunit.detail.LearningUnitDetailViewModel diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index 8a389ead1..fb1932da2 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -3,7 +3,6 @@ package world.respect.shared.navigation -import androidx.lifecycle.SavedStateHandle import io.ktor.http.Url import kotlinx.serialization.Serializable import kotlinx.serialization.Transient @@ -13,7 +12,7 @@ import world.respect.shared.domain.account.invite.RespectRedeemInviteRequest import world.respect.datalayer.school.model.EnrollmentRoleEnum import world.respect.datalayer.school.model.PersonRoleEnum import world.respect.datalayer.school.model.report.ReportFilter -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping +import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMapping import world.respect.shared.viewmodel.learningunit.LearningUnitSelection import world.respect.shared.viewmodel.manageuser.profile.ProfileType diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt index ace97ffc6..de2e3071f 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt @@ -9,7 +9,6 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.json.Json import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject @@ -48,9 +47,8 @@ import world.respect.shared.util.ext.isAdmin import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.FabUiState import world.respect.shared.viewmodel.assignment.edit.AssignmentEditViewModel -import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel -import world.respect.shared.viewmodel.curriculum.mapping.list.PlaylistListViewModel -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping +import world.respect.shared.viewmodel.playlists.mapping.list.PlaylistListViewModel +import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMapping data class AppLauncherUiState( val apps: IPagingSourceFactory = EmptyPagingSourceFactory(), diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt index c43439e7e..dc16af244 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt @@ -48,8 +48,8 @@ import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.ActionBarButtonUiState import world.respect.shared.viewmodel.assignment.toAssignmentLearningUnitRefs -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping -import world.respect.shared.viewmodel.curriculum.mapping.toOpdsGroup +import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMapping +import world.respect.shared.viewmodel.playlists.mapping.toOpdsGroup import world.respect.shared.viewmodel.learningunit.LearningUnitSelection import kotlin.time.Clock diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt index bacfa01a5..56734189c 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt @@ -38,10 +38,10 @@ import world.respect.shared.util.ext.resolve import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.getTitle import world.respect.shared.viewmodel.apps.launcher.AppLauncherViewModel -import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel -import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingSectionUiState -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMappingSectionLink +import world.respect.shared.viewmodel.playlists.mapping.edit.CurriculumMappingEditViewModel +import world.respect.shared.viewmodel.playlists.mapping.edit.CurriculumMappingSectionUiState +import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMapping +import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMappingSectionLink import world.respect.shared.viewmodel.learningunit.LearningUnitSelection data class LearningUnitDetailUiState( diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/CurriculumMappingAdapter.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/CurriculumMappingAdapter.kt similarity index 91% rename from respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/CurriculumMappingAdapter.kt rename to respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/CurriculumMappingAdapter.kt index c45b7747b..e1631f401 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/CurriculumMappingAdapter.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/CurriculumMappingAdapter.kt @@ -1,4 +1,4 @@ -package world.respect.shared.viewmodel.curriculum.mapping +package world.respect.shared.viewmodel.playlists.mapping //Add functions that convert CurriculumMapping to OpdsFeed and vice versa. See the adapters in the //database module @@ -13,9 +13,9 @@ import world.respect.lib.opds.model.OpdsGroup import world.respect.lib.opds.model.OpdsPublication import world.respect.lib.opds.model.ReadiumLink import world.respect.lib.opds.model.ReadiumMetadata -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMappingSection -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMappingSectionLink +import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMapping +import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMappingSection +import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMappingSectionLink fun CurriculumMapping.toOpds(selfLink: String): OpdsFeed { diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/PlaylistEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/edit/PlaylistEditViewModel.kt similarity index 97% rename from respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/PlaylistEditViewModel.kt rename to respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/edit/PlaylistEditViewModel.kt index 86fc76067..ed9d2f4ca 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/edit/PlaylistEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/edit/PlaylistEditViewModel.kt @@ -1,4 +1,4 @@ -package world.respect.shared.viewmodel.curriculum.mapping.edit +package world.respect.shared.viewmodel.playlists.mapping.edit import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope @@ -41,9 +41,9 @@ import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.ActionBarButtonUiState import world.respect.shared.viewmodel.assignment.edit.AssignmentEditViewModel.Companion.KEY_LEARNING_UNIT -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMappingSection -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMappingSectionLink +import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMapping +import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMappingSection +import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMappingSectionLink import world.respect.shared.viewmodel.learningunit.LearningUnitSelection import world.respect.shared.navigation.RouteResultDest import world.respect.shared.viewmodel.app.appstate.getTitle diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/PlaylistListViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/list/PlaylistListViewModel.kt similarity index 95% rename from respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/PlaylistListViewModel.kt rename to respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/list/PlaylistListViewModel.kt index 331bec1df..5fc183814 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/list/PlaylistListViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/list/PlaylistListViewModel.kt @@ -1,4 +1,4 @@ -package world.respect.shared.viewmodel.curriculum.mapping.list +package world.respect.shared.viewmodel.playlists.mapping.list import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope @@ -17,8 +17,8 @@ import world.respect.shared.navigation.NavCommand import world.respect.shared.navigation.NavResultReturner import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel -import world.respect.shared.viewmodel.curriculum.mapping.edit.CurriculumMappingEditViewModel -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping +import world.respect.shared.viewmodel.playlists.mapping.edit.CurriculumMappingEditViewModel +import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMapping import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.error_unexpected_result_type import world.respect.shared.resources.UiText diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMapping.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/CurriculumMapping.kt similarity index 86% rename from respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMapping.kt rename to respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/CurriculumMapping.kt index 75235cf11..afd99d29e 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMapping.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/CurriculumMapping.kt @@ -1,4 +1,4 @@ -package world.respect.shared.viewmodel.curriculum.mapping.model +package world.respect.shared.viewmodel.playlists.mapping.model import io.ktor.http.Url import kotlinx.serialization.Serializable diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMappingSection.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/CurriculumMappingSection.kt similarity index 78% rename from respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMappingSection.kt rename to respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/CurriculumMappingSection.kt index 7aff9ea37..578cd8688 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMappingSection.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/CurriculumMappingSection.kt @@ -1,4 +1,4 @@ -package world.respect.shared.viewmodel.curriculum.mapping.model +package world.respect.shared.viewmodel.playlists.mapping.model import kotlinx.serialization.Serializable diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMappingSectionLink.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/CurriculumMappingSectionLink.kt similarity index 85% rename from respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMappingSectionLink.kt rename to respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/CurriculumMappingSectionLink.kt index 3ec577701..c1081bbcf 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/curriculum/mapping/model/CurriculumMappingSectionLink.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/CurriculumMappingSectionLink.kt @@ -1,4 +1,4 @@ -package world.respect.shared.viewmodel.curriculum.mapping.model +package world.respect.shared.viewmodel.playlists.mapping.model import io.ktor.http.Url import kotlinx.serialization.Serializable From 5704a2bae29628761f446893c3c7abed830440c0 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Wed, 31 Dec 2025 15:29:29 +0530 Subject: [PATCH 42/58] change to playlists --- .../kotlin/world/respect/AppKoinModule.kt | 4 +- .../world/respect/app/app/AppNavHost.kt | 4 +- .../mapping/edit/PlaylistEditScreen.kt | 26 ++++++------- .../mapping/list/PlaylistListScreen.kt | 12 +++--- .../detail/LearningUnitDetailScreen.kt | 12 +++--- .../respect/shared/navigation/AppRoutes.kt | 30 +++++++-------- .../apps/launcher/AppLauncherViewModel.kt | 8 ++-- .../edit/AssignmentEditViewModel.kt | 4 +- .../detail/LearningUnitDetailViewModel.kt | 24 ++++++------ ...mMappingAdapter.kt => PlaylistsAdapter.kt} | 18 ++++----- .../mapping/edit/PlaylistEditViewModel.kt | 38 +++++++++---------- .../mapping/list/PlaylistListViewModel.kt | 20 +++++----- ...rriculumMapping.kt => PlaylistsMapping.kt} | 4 +- ...gSection.kt => PlaylistsMappingSection.kt} | 4 +- ...Link.kt => PlaylistsMappingSectionLink.kt} | 2 +- 15 files changed, 105 insertions(+), 105 deletions(-) rename respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/{CurriculumMappingAdapter.kt => PlaylistsAdapter.kt} (85%) rename respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/{CurriculumMapping.kt => PlaylistsMapping.kt} (79%) rename respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/{CurriculumMappingSection.kt => PlaylistsMappingSection.kt} (65%) rename respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/{CurriculumMappingSectionLink.kt => PlaylistsMappingSectionLink.kt} (90%) diff --git a/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt b/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt index 65ed9bd71..7b40ae9ae 100644 --- a/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt +++ b/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt @@ -206,7 +206,7 @@ import world.respect.shared.viewmodel.report.list.ReportTemplateListViewModel import world.respect.sharedse.domain.account.authenticatepassword.AuthenticatePasswordUseCaseDbImpl import java.io.File import world.respect.shared.viewmodel.settings.SettingsViewModel -import world.respect.shared.viewmodel.playlists.mapping.edit.CurriculumMappingEditViewModel +import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistEditViewModel import world.respect.shared.viewmodel.schooldirectory.edit.SchoolDirectoryEditViewModel import world.respect.shared.viewmodel.schooldirectory.list.SchoolDirectoryListViewModel import world.respect.shared.viewmodel.playlists.mapping.list.PlaylistListViewModel @@ -316,7 +316,7 @@ val appKoinModule = module { viewModelOf(::IndicatorListViewModel) viewModelOf(::IndicatorDetailViewModel) viewModelOf(::SettingsViewModel) - viewModelOf(::CurriculumMappingEditViewModel) + viewModelOf(::PlaylistEditViewModel) viewModelOf(::SetUsernameAndPasswordViewModel) viewModelOf(::ChangePasswordViewModel) viewModelOf(::SchoolDirectoryListViewModel) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt index 9a0432891..b00b3931a 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt @@ -137,7 +137,7 @@ import world.respect.app.view.settings.SettingsScreenForViewModel import world.respect.app.view.curriculum.mapping.edit.CurriculumMappingEditScreenForViewModel import world.respect.shared.viewmodel.settings.SettingsViewModel -import world.respect.shared.viewmodel.playlists.mapping.edit.CurriculumMappingEditViewModel +import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistEditViewModel import world.respect.shared.navigation.Settings import world.respect.shared.navigation.CurriculumMappingEdit @@ -539,7 +539,7 @@ fun AppNavHost( } composable { - val viewModel: CurriculumMappingEditViewModel = respectViewModel( + val viewModel: PlaylistEditViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, navController = respectNavController ) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/PlaylistEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/PlaylistEditScreen.kt index 399a93200..4469bda61 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/PlaylistEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/PlaylistEditScreen.kt @@ -42,17 +42,17 @@ import world.respect.shared.generated.resources.sections import world.respect.shared.generated.resources.title import world.respect.shared.generated.resources.section_name import world.respect.shared.util.ext.asUiText -import world.respect.shared.viewmodel.playlists.mapping.edit.CurriculumMappingEditUiState -import world.respect.shared.viewmodel.playlists.mapping.edit.CurriculumMappingEditViewModel -import world.respect.shared.viewmodel.playlists.mapping.edit.CurriculumMappingSectionUiState -import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMappingSection -import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMappingSectionLink +import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistEditUiState +import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistEditViewModel +import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistSectionUiState +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMappingSection +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMappingSectionLink import androidx.compose.ui.draw.alpha @Composable fun CurriculumMappingEditScreenForViewModel( - viewModel: CurriculumMappingEditViewModel + viewModel: PlaylistEditViewModel ) { val uiState by viewModel.uiState.collectAsState() @@ -74,8 +74,8 @@ fun CurriculumMappingEditScreenForViewModel( @Composable fun CurriculumMappingEditScreen( - uiState: CurriculumMappingEditUiState = CurriculumMappingEditUiState(), - sectionLinkUiState: (CurriculumMappingSectionLink) -> Flow>, + uiState: PlaylistEditUiState = PlaylistEditUiState(), + sectionLinkUiState: (PlaylistsMappingSectionLink) -> Flow>, onTitleChanged: (String) -> Unit = {}, onDescriptionChanged: (String) -> Unit = {}, onClickAddSection: () -> Unit = {}, @@ -85,7 +85,7 @@ fun CurriculumMappingEditScreen( onClickAddLesson: (Int) -> Unit = {}, onClickRemoveLesson: (Int, Int) -> Unit = { _, _ -> }, onLessonMovedBetweenSections: (Int, Int, Int, Int) -> Unit = { _, _, _, _ -> }, - onClickLesson: (CurriculumMappingSectionLink) -> Unit = {}, + onClickLesson: (PlaylistsMappingSectionLink) -> Unit = {}, ) { val haptic = LocalHapticFeedback.current val lazyListState = rememberLazyListState() @@ -315,7 +315,7 @@ fun CurriculumMappingEditScreen( @Composable private fun SectionItem( - section: CurriculumMappingSection, + section: PlaylistsMappingSection, sectionIndex: Int, isDragging: Boolean, onSectionTitleChanged: (Int, String) -> Unit, @@ -406,12 +406,12 @@ private fun SectionItem( @Composable private fun LessonItem( - link: CurriculumMappingSectionLink, - sectionLinkUiState: (CurriculumMappingSectionLink) -> Flow>, + link: PlaylistsMappingSectionLink, + sectionLinkUiState: (PlaylistsMappingSectionLink) -> Flow>, sectionIndex: Int, linkIndex: Int, onClickRemoveLesson: (Int, Int) -> Unit, - onClickLesson: (CurriculumMappingSectionLink) -> Unit = {}, + onClickLesson: (PlaylistsMappingSectionLink) -> Unit = {}, isDragging: Boolean, isParentSectionDragging: Boolean = false, dragModifier: Modifier = Modifier diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/PlaylistListScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/PlaylistListScreen.kt index 7bc782f6f..1656301b8 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/PlaylistListScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/PlaylistListScreen.kt @@ -24,7 +24,7 @@ import world.respect.shared.generated.resources.* import world.respect.shared.viewmodel.playlists.mapping.list.PlaylistListUiState import world.respect.shared.viewmodel.playlists.mapping.list.PlaylistListViewModel -import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMapping +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMapping @Composable fun PlaylistListScreen( @@ -46,10 +46,10 @@ fun PlaylistListScreen( fun PlaylistListScreenContent( uiState: PlaylistListUiState, onFilterSelected: (Int) -> Unit, - onClickMapping: (CurriculumMapping) -> Unit, + onClickMapping: (PlaylistsMapping) -> Unit, onClickAddNew: () -> Unit, onClickAddLink: () -> Unit, - onRemoveMapping: (CurriculumMapping) -> Unit, + onRemoveMapping: (PlaylistsMapping) -> Unit, ) { var isFabMenuExpanded by remember { mutableStateOf(false) } @@ -207,9 +207,9 @@ fun PlaylistListScreenContent( @Composable private fun MappingListItem( - mapping: CurriculumMapping, - onClickMapping: (CurriculumMapping) -> Unit, - onRemoveMapping: (CurriculumMapping) -> Unit, + mapping: PlaylistsMapping, + onClickMapping: (PlaylistsMapping) -> Unit, + onRemoveMapping: (PlaylistsMapping) -> Unit, ) { var menuExpanded by remember { mutableStateOf(false) } diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt index 988e6c2fa..01bbadd6a 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt @@ -36,8 +36,8 @@ import world.respect.datalayer.DataLoadingState import world.respect.datalayer.ext.dataOrNull import world.respect.shared.generated.resources.* import world.respect.shared.viewmodel.app.appstate.getTitle -import world.respect.shared.viewmodel.playlists.mapping.edit.CurriculumMappingSectionUiState -import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMappingSectionLink +import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistSectionUiState +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMappingSectionLink import world.respect.shared.viewmodel.learningunit.detail.LearningUnitDetailUiState import world.respect.shared.viewmodel.learningunit.detail.LearningUnitDetailViewModel @@ -252,7 +252,7 @@ private fun SingleLessonDetailScreen( @Composable private fun PlaylistDetailScreen( uiState: LearningUnitDetailUiState, - onClickLesson: (CurriculumMappingSectionLink) -> Unit, + onClickLesson: (PlaylistsMappingSectionLink) -> Unit, onClickEdit: () -> Unit, onClickAssign: () -> Unit, onClickAssignSection: (Long) -> Unit, @@ -457,9 +457,9 @@ private fun PlaylistDetailScreen( @Composable private fun LessonListItem( - link: CurriculumMappingSectionLink, - sectionLinkUiState: (CurriculumMappingSectionLink) -> Flow>, - onClickLesson: (CurriculumMappingSectionLink) -> Unit + link: PlaylistsMappingSectionLink, + sectionLinkUiState: (PlaylistsMappingSectionLink) -> Flow>, + onClickLesson: (PlaylistsMappingSectionLink) -> Unit ) { val stateFlow = remember(link.href) { sectionLinkUiState(link) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index fb1932da2..065dda959 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -12,7 +12,7 @@ import world.respect.shared.domain.account.invite.RespectRedeemInviteRequest import world.respect.datalayer.school.model.EnrollmentRoleEnum import world.respect.datalayer.school.model.PersonRoleEnum import world.respect.datalayer.school.model.report.ReportFilter -import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMapping +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMapping import world.respect.shared.viewmodel.learningunit.LearningUnitSelection import world.respect.shared.viewmodel.manageuser.profile.ProfileType @@ -111,10 +111,10 @@ data class AssignmentEdit( } @Transient - val availablePlaylists: List? = availablePlaylistsJson?.let { jsonStr -> + val availablePlaylists: List? = availablePlaylistsJson?.let { jsonStr -> try { Json.decodeFromString( - ListSerializer(CurriculumMapping.serializer()), + ListSerializer(PlaylistsMapping.serializer()), jsonStr ) } catch (e: Exception) { @@ -126,7 +126,7 @@ data class AssignmentEdit( fun create( uid: String?, learningUnitSelected: LearningUnitSelection? = null, - availablePlaylists: List? = null, + availablePlaylists: List? = null, ): AssignmentEdit { val learningUnits = learningUnitSelected?.let { listOf(it) } return AssignmentEdit( @@ -139,7 +139,7 @@ data class AssignmentEdit( }, availablePlaylistsJson = availablePlaylists?.let { Json.encodeToString( - ListSerializer(CurriculumMapping.serializer()), + ListSerializer(PlaylistsMapping.serializer()), it ) } @@ -149,7 +149,7 @@ data class AssignmentEdit( fun createWithMultipleLessons( uid: String?, learningUnits: List, - availablePlaylists: List? = null, + availablePlaylists: List? = null, ): AssignmentEdit { return AssignmentEdit( guid = uid, @@ -159,7 +159,7 @@ data class AssignmentEdit( ), availablePlaylistsJson = availablePlaylists?.let { Json.encodeToString( - ListSerializer(CurriculumMapping.serializer()), + ListSerializer(PlaylistsMapping.serializer()), it ) } @@ -577,10 +577,10 @@ class LearningUnitDetail ( val refererUrl: Url? get() = refererUrlStr?.let { Url(it) } - val mappingData: CurriculumMapping? + val mappingData: PlaylistsMapping? get() = mappingDataJson?.let { try { - Json.decodeFromString(CurriculumMapping.serializer(), it) + Json.decodeFromString(PlaylistsMapping.serializer(), it) } catch (e: Exception) { null } @@ -601,9 +601,9 @@ class LearningUnitDetail ( mappingDataJson = null ) - fun createFromMapping(mapping: CurriculumMapping): LearningUnitDetail { + fun createFromMapping(mapping: PlaylistsMapping): LearningUnitDetail { val mappingJson = try { - Json.encodeToString(CurriculumMapping.serializer(), mapping) + Json.encodeToString(PlaylistsMapping.serializer(), mapping) } catch (e: Exception) { null } @@ -732,9 +732,9 @@ data class CurriculumMappingEdit( ) : RespectAppRoute { @Transient - val mappingData: CurriculumMapping? = mappingDataJson?.let { jsonString -> + val mappingData: PlaylistsMapping? = mappingDataJson?.let { jsonString -> try { - Json.decodeFromString(CurriculumMapping.serializer(), jsonString) + Json.decodeFromString(PlaylistsMapping.serializer(), jsonString) } catch (e: Exception) { null } @@ -743,12 +743,12 @@ data class CurriculumMappingEdit( companion object { fun create( uid: Long, - mappingData: CurriculumMapping? = null + mappingData: PlaylistsMapping? = null ) = CurriculumMappingEdit( textbookUid = uid, mappingDataJson = mappingData?.let { mapping -> try { - Json.encodeToString(CurriculumMapping.serializer(), mapping) + Json.encodeToString(PlaylistsMapping.serializer(), mapping) } catch (e: Exception) { null } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt index de2e3071f..8dd946a71 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt @@ -48,7 +48,7 @@ import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.FabUiState import world.respect.shared.viewmodel.assignment.edit.AssignmentEditViewModel import world.respect.shared.viewmodel.playlists.mapping.list.PlaylistListViewModel -import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMapping +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMapping data class AppLauncherUiState( val apps: IPagingSourceFactory = EmptyPagingSourceFactory(), @@ -177,10 +177,10 @@ class AppLauncherViewModel( } } - private fun loadMappingsFromSavedState(savedStateHandle: SavedStateHandle): List { + private fun loadMappingsFromSavedState(savedStateHandle: SavedStateHandle): List { val mappingsJson = savedStateHandle.get(KEY_MAPPINGS_LIST) ?: return emptyList() return try { - json.decodeFromString>(mappingsJson) + json.decodeFromString>(mappingsJson) } catch (e: Exception) { emptyList() } @@ -237,6 +237,6 @@ class AppLauncherViewModel( companion object { const val KEY_MAPPINGS_LIST = "mappings_list" - var cachedPlaylists: List = emptyList() + var cachedPlaylists: List = emptyList() } } \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt index dc16af244..e6d98ea49 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt @@ -48,7 +48,7 @@ import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.ActionBarButtonUiState import world.respect.shared.viewmodel.assignment.toAssignmentLearningUnitRefs -import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMapping +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMapping import world.respect.shared.viewmodel.playlists.mapping.toOpdsGroup import world.respect.shared.viewmodel.learningunit.LearningUnitSelection import kotlin.time.Clock @@ -140,7 +140,7 @@ class AssignmentEditViewModel( } viewModelScope.launch { resultReturner.filteredResultFlowForKey(KEY_PLAYLIST_SELECTION).collect { result -> - val mapping = result.result as? CurriculumMapping ?: return@collect + val mapping = result.result as? PlaylistsMapping ?: return@collect val group = mapping.toOpdsGroup() val newLearningUnits = group.toAssignmentLearningUnitRefs() diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt index 56734189c..3fce3b5f8 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt @@ -38,10 +38,10 @@ import world.respect.shared.util.ext.resolve import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.getTitle import world.respect.shared.viewmodel.apps.launcher.AppLauncherViewModel -import world.respect.shared.viewmodel.playlists.mapping.edit.CurriculumMappingEditViewModel -import world.respect.shared.viewmodel.playlists.mapping.edit.CurriculumMappingSectionUiState -import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMapping -import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMappingSectionLink +import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistEditViewModel +import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistSectionUiState +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMapping +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMappingSectionLink import world.respect.shared.viewmodel.learningunit.LearningUnitSelection data class LearningUnitDetailUiState( @@ -50,8 +50,8 @@ data class LearningUnitDetailUiState( val pinState: PublicationPinState = PublicationPinState( PublicationPinState.Status.NOT_PINNED, 0, 0 ), - val mapping: CurriculumMapping? = null, - val sectionLinkUiState: (CurriculumMappingSectionLink) -> Flow> = { + val mapping: PlaylistsMapping? = null, + val sectionLinkUiState: (PlaylistsMappingSectionLink) -> Flow> = { emptyFlow() }, val showCopyDialog: Boolean = false, @@ -178,7 +178,7 @@ class LearningUnitDetailViewModel( } } private suspend fun loadLessonPublications( - lessons: List + lessons: List ): List { return lessons.mapNotNull { lesson -> try { @@ -280,7 +280,7 @@ class LearningUnitDetailViewModel( } } - fun onClickLesson(link: CurriculumMappingSectionLink) { + fun onClickLesson(link: PlaylistsMappingSectionLink) { val publicationUrl = Url(link.href) val appManifestUrl = link.appManifestUrl ?: return @@ -339,7 +339,7 @@ class LearningUnitDetailViewModel( resultReturner.sendResult( NavResult( - key = CurriculumMappingEditViewModel.KEY_SAVED_MAPPING, + key = PlaylistEditViewModel.KEY_SAVED_MAPPING, result = copiedMapping ) ) @@ -357,8 +357,8 @@ class LearningUnitDetailViewModel( } fun sectionLinkUiStateFor( - link: CurriculumMappingSectionLink - ): Flow> { + link: PlaylistsMappingSectionLink + ): Flow> { val publicationUrl = Url(link.href) return appDataSource.opdsDataSource.loadOpdsPublication( url = publicationUrl, @@ -367,7 +367,7 @@ class LearningUnitDetailViewModel( expectedPublicationId = null, ).map { opdsLoadState -> opdsLoadState.map { publication -> - CurriculumMappingSectionUiState( + PlaylistSectionUiState( icon = publication.findIcons().firstOrNull()?.let { publicationUrl.resolve(it.href) }, diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/CurriculumMappingAdapter.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/PlaylistsAdapter.kt similarity index 85% rename from respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/CurriculumMappingAdapter.kt rename to respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/PlaylistsAdapter.kt index e1631f401..386ca208f 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/CurriculumMappingAdapter.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/PlaylistsAdapter.kt @@ -13,12 +13,12 @@ import world.respect.lib.opds.model.OpdsGroup import world.respect.lib.opds.model.OpdsPublication import world.respect.lib.opds.model.ReadiumLink import world.respect.lib.opds.model.ReadiumMetadata -import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMapping -import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMappingSection -import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMappingSectionLink +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMapping +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMappingSection +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMappingSectionLink -fun CurriculumMapping.toOpds(selfLink: String): OpdsFeed { +fun PlaylistsMapping.toOpds(selfLink: String): OpdsFeed { return OpdsFeed( metadata = OpdsFeedMetadata( title = this.title, @@ -47,7 +47,7 @@ fun CurriculumMapping.toOpds(selfLink: String): OpdsFeed { ) } -fun CurriculumMapping.toOpdsGroup(): OpdsGroup { +fun PlaylistsMapping.toOpdsGroup(): OpdsGroup { return OpdsGroup( metadata = OpdsFeedMetadata( title = this.title @@ -76,17 +76,17 @@ fun CurriculumMapping.toOpdsGroup(): OpdsGroup { ) } -fun OpdsFeed.toCurriculumMapping(): CurriculumMapping { - return CurriculumMapping( +fun OpdsFeed.toCurriculumMapping(): PlaylistsMapping { + return PlaylistsMapping( uid = System.currentTimeMillis(), title = this.metadata.title, description = this.metadata.description ?: "", sections = this.groups?.map { group -> - CurriculumMappingSection( + PlaylistsMappingSection( uid = System.currentTimeMillis(), title = group.metadata.title, items = group.navigation?.map { navLink -> - CurriculumMappingSectionLink( + PlaylistsMappingSectionLink( uid = System.currentTimeMillis(), href = navLink.href, title = navLink.title ?: "" diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/edit/PlaylistEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/edit/PlaylistEditViewModel.kt index ed9d2f4ca..42af52670 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/edit/PlaylistEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/edit/PlaylistEditViewModel.kt @@ -41,21 +41,21 @@ import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.ActionBarButtonUiState import world.respect.shared.viewmodel.assignment.edit.AssignmentEditViewModel.Companion.KEY_LEARNING_UNIT -import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMapping -import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMappingSection -import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMappingSectionLink +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMapping +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMappingSection +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMappingSectionLink import world.respect.shared.viewmodel.learningunit.LearningUnitSelection import world.respect.shared.navigation.RouteResultDest import world.respect.shared.viewmodel.app.appstate.getTitle -data class CurriculumMappingEditUiState( - val mapping: CurriculumMapping? = null, +data class PlaylistEditUiState( + val mapping: PlaylistsMapping? = null, val loading: Boolean = false, val isNew: Boolean = true, val titleError: UiText? = null, val error: UiText? = null, val pendingLessonSectionIndex: Int? = null, - val sectionUiState: (CurriculumMappingSection) -> Flow = { emptyFlow() }, + val sectionUiState: (PlaylistsMappingSection) -> Flow = { emptyFlow() }, ) { val fieldsEnabled: Boolean get() = !loading @@ -66,18 +66,18 @@ data class CurriculumMappingEditUiState( val description: String get() = mapping?.description ?: "" - val sections: List + val sections: List get() = mapping?.sections ?: emptyList() } -data class CurriculumMappingSectionUiState( +data class PlaylistSectionUiState( val icon: Url? = null, val title: String = "", val subtitle: String = "", val description: String = "", ) -class CurriculumMappingEditViewModel( +class PlaylistEditViewModel( savedStateHandle: SavedStateHandle, private val resultReturner: NavResultReturner, private val json: Json, @@ -92,8 +92,8 @@ class CurriculumMappingEditViewModel( private val mappingData = route.mappingData private val _uiState = MutableStateFlow( - CurriculumMappingEditUiState( - mapping = mappingData ?: CurriculumMapping(uid = mappingUid), + PlaylistEditUiState( + mapping = mappingData ?: PlaylistsMapping(uid = mappingUid), isNew = mappingUid == 0L ) ) @@ -130,7 +130,7 @@ class CurriculumMappingEditViewModel( mapping = prev.mapping?.copy( sections = prev.mapping.sections.updateAtIndex(pendingSectionIndex) { it.copy( - items = it.items + CurriculumMappingSectionLink( + items = it.items + PlaylistsMappingSectionLink( href = selectedLearningUnit.learningUnitManifestUrl.toString(), title = selectedLearningUnit.selectedPublication.metadata.title.getTitle(), appManifestUrl = selectedLearningUnit.appManifestUrl @@ -145,11 +145,11 @@ class CurriculumMappingEditViewModel( } } - private fun updateUiStateAndCommit(block: (CurriculumMappingEditUiState) -> CurriculumMappingEditUiState) { + private fun updateUiStateAndCommit(block: (PlaylistEditUiState) -> PlaylistEditUiState) { val mappingToCommit = _uiState.updateAndGet(block).mapping ?: return savedStateHandle[KEY_MAPPING] = json.encodeToString( - CurriculumMapping.serializer(), mappingToCommit + PlaylistsMapping.serializer(), mappingToCommit ) } @@ -175,7 +175,7 @@ class CurriculumMappingEditViewModel( updateUiStateAndCommit { prev -> prev.copy( mapping = prev.mapping?.copy( - sections = prev.mapping.sections + CurriculumMappingSection(title = "") + sections = prev.mapping.sections + PlaylistsMappingSection(title = "") ) ) } @@ -295,7 +295,7 @@ class CurriculumMappingEditViewModel( } } - fun onClickLesson(link: CurriculumMappingSectionLink) { + fun onClickLesson(link: PlaylistsMappingSectionLink) { val publicationUrl = Url(link.href) val appManifestUrl = link.appManifestUrl ?: return @@ -312,8 +312,8 @@ class CurriculumMappingEditViewModel( } fun sectionLinkUiStateFor( - link: CurriculumMappingSectionLink - ): Flow> { + link: PlaylistsMappingSectionLink + ): Flow> { val publicationUrl = Url(link.href) return respectAppDataSource.opdsDataSource.loadOpdsPublication( url = Url(link.href), @@ -322,7 +322,7 @@ class CurriculumMappingEditViewModel( expectedPublicationId = null, ).map { opdsLoadState -> opdsLoadState.map { publication -> - CurriculumMappingSectionUiState( + PlaylistSectionUiState( icon = publication.findIcons().firstOrNull()?.let { publicationUrl.resolve(it.href) } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/list/PlaylistListViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/list/PlaylistListViewModel.kt index 5fc183814..b757465db 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/list/PlaylistListViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/list/PlaylistListViewModel.kt @@ -17,20 +17,20 @@ import world.respect.shared.navigation.NavCommand import world.respect.shared.navigation.NavResultReturner import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel -import world.respect.shared.viewmodel.playlists.mapping.edit.CurriculumMappingEditViewModel -import world.respect.shared.viewmodel.playlists.mapping.model.CurriculumMapping +import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistEditViewModel +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMapping import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.error_unexpected_result_type import world.respect.shared.resources.UiText data class PlaylistListUiState( - val mappings: List = emptyList(), + val mappings: List = emptyList(), val selectedFilterIndex: Int = 0, val error: UiText? = null, val currentUserGuid: String? = null, val currentSchoolUrl: Url? = null, ) { - val filteredMappings: List + val filteredMappings: List get() = when (selectedFilterIndex) { 0 -> mappings 1 -> mappings.filter { mapping -> @@ -71,9 +71,9 @@ class PlaylistListViewModel( viewModelScope.launch { resultReturner.filteredResultFlowForKey( - CurriculumMappingEditViewModel.KEY_SAVED_MAPPING + PlaylistEditViewModel.KEY_SAVED_MAPPING ).collect { result -> - val savedMapping = result.result as? CurriculumMapping + val savedMapping = result.result as? PlaylistsMapping if (savedMapping == null) { _uiState.update { it.copy(error = Res.string.error_unexpected_result_type.asUiText()) @@ -85,14 +85,14 @@ class PlaylistListViewModel( } } - fun setMappings(mappings: List) { + fun setMappings(mappings: List) { _uiState.update { it.copy(mappings = mappings) } } fun onFilterSelected(index: Int) { _uiState.update { it.copy(selectedFilterIndex = index) } } - fun onClickMapping(mapping: CurriculumMapping) { + fun onClickMapping(mapping: PlaylistsMapping) { _navCommandFlow.tryEmit( NavCommand.Navigate( LearningUnitDetail.createFromMapping(mapping) @@ -115,12 +115,12 @@ class PlaylistListViewModel( ) } - fun removeMapping(mapping: CurriculumMapping) { + fun removeMapping(mapping: PlaylistsMapping) { val updated = _uiState.value.mappings.filter { it.uid != mapping.uid } _uiState.update { it.copy(mappings = updated) } } - private fun addOrUpdateMapping(mapping: CurriculumMapping) { + private fun addOrUpdateMapping(mapping: PlaylistsMapping) { val currentMappings = _uiState.value.mappings.toMutableList() val existingIndex = currentMappings.indexOfFirst { it.uid == mapping.uid } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/CurriculumMapping.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/PlaylistsMapping.kt similarity index 79% rename from respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/CurriculumMapping.kt rename to respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/PlaylistsMapping.kt index afd99d29e..53b7c4ca8 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/CurriculumMapping.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/PlaylistsMapping.kt @@ -4,11 +4,11 @@ import io.ktor.http.Url import kotlinx.serialization.Serializable @Serializable -data class CurriculumMapping( +data class PlaylistsMapping( val uid: Long = System.currentTimeMillis(), val title: String = "", val description: String = "", - val sections: List = emptyList(), + val sections: List = emptyList(), val createdBy: String? = null, val isSchoolWide: Boolean = false, val schoolUrl: Url? = null, diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/CurriculumMappingSection.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/PlaylistsMappingSection.kt similarity index 65% rename from respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/CurriculumMappingSection.kt rename to respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/PlaylistsMappingSection.kt index 578cd8688..fbd163f84 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/CurriculumMappingSection.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/PlaylistsMappingSection.kt @@ -3,8 +3,8 @@ package world.respect.shared.viewmodel.playlists.mapping.model import kotlinx.serialization.Serializable @Serializable -data class CurriculumMappingSection( +data class PlaylistsMappingSection( val uid: Long = System.currentTimeMillis(), val title: String, - val items: List = emptyList() + val items: List = emptyList() ) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/CurriculumMappingSectionLink.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/PlaylistsMappingSectionLink.kt similarity index 90% rename from respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/CurriculumMappingSectionLink.kt rename to respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/PlaylistsMappingSectionLink.kt index c1081bbcf..dc4abe804 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/CurriculumMappingSectionLink.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/PlaylistsMappingSectionLink.kt @@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable * @property href Absolute URL to the OPDS publication linked (NOT the Learning Unit ID URL). */ @Serializable -data class CurriculumMappingSectionLink( +data class PlaylistsMappingSectionLink( val uid: Long = System.currentTimeMillis(), val href: String, val title: String? = "", From b69ba54a3a3adb56599d52dea6b7be5e5f28b83f Mon Sep 17 00:00:00 2001 From: pooja Date: Wed, 31 Dec 2025 19:03:30 +0400 Subject: [PATCH 43/58] updated test- added assignment flow --- .maestro/flows/002_browse_lessons_test.yaml | 56 +++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/.maestro/flows/002_browse_lessons_test.yaml b/.maestro/flows/002_browse_lessons_test.yaml index a4e5ebfd6..44c9a7efb 100644 --- a/.maestro/flows/002_browse_lessons_test.yaml +++ b/.maestro/flows/002_browse_lessons_test.yaml @@ -141,6 +141,8 @@ onFlowComplete: - assertVisible: id: "app_title" text: "Playlist - Grade 1" + +# Assigning a playlist to a assignment - tapOn: id: "assign_btn" - assertVisible: @@ -192,3 +194,57 @@ onFlowComplete: text: "copy the Playlist - Grade 1" - assertVisible: "Day 1" - assertVisible: "Day 2" + +# Adding playlist to an assignment +- tapOn: "Assignments" +- tapOn: + id: "floating_action_button" # +Assignment button +- assertVisible: + id: "app_title" + text: "Add assignment" +- tapOn: "Name*" +- inputText: "Homework 2" +- tapOn: "Class" +- tapOn: "New Class" +- tapOn: "Date" +- runScript: + file: "scripts/setDate.js" +- inputText: ${output.futureDate} +- tapOn: "Time" +- runScript: + file: "scripts/setDate.js" +- inputText: ${output.currentTime} +- tapOn: "Lesson/assessment" +- assertVisible: + id: "app_title" + text: "Add task to assignment" +- tapOn: "Playlists" +- tapOn: "copy the Playlist - Grade 1" +- assertVisible: + id: "app_title" + text: "copy the Playlist - Grade 1" +- assertVisible: "Select all" +- assertVisible: "Select none" +- tapOn: "Select all" +- assertVisible: + id: "check_box" + checked: true +- tapOn: "Select none" +- assertVisible: + id: "check_box" + checked: false +- tapOn: + id: "check_box" + index: 1 +- tapOn: "Add 1 task to assignment" +- assertVisible: + id: "app_title" + text: "Add assignment" +- tapOn: "Save" +- assertVisible: + id: "app_title" + text: "Homework 2" +- assertVisible: "Lesson 001" +- back +- assertVisible: "Homework 2" + From 4ea08e8e6c262f218e92cef471609f2f50613f0f Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Fri, 2 Jan 2026 10:53:40 +0530 Subject: [PATCH 44/58] add the select task to assignment --- .../world/respect/app/app/AppNavHost.kt | 2 +- .../view/apps/launcher/AppLauncherScreen.kt | 2 +- .../detail/LearningUnitDetailScreen.kt | 148 +++++++++++------- .../mapping/edit/PlaylistEditScreen.kt | 2 +- .../mapping/list/PlaylistListScreen.kt | 2 +- .../respect/shared/navigation/AppRoutes.kt | 14 +- .../apps/launcher/AppLauncherViewModel.kt | 32 +++- .../detail/AssignmentDetailViewModel.kt | 23 ++- .../list/AssignmentListViewModel.kt | 23 ++- .../detail/LearningUnitDetailViewModel.kt | 104 +++++++++--- .../mapping/list/PlaylistListViewModel.kt | 13 +- 11 files changed, 272 insertions(+), 93 deletions(-) rename respect-app-compose/src/commonMain/kotlin/world/respect/app/view/{curriculum => playlists}/mapping/edit/PlaylistEditScreen.kt (99%) rename respect-app-compose/src/commonMain/kotlin/world/respect/app/view/{curriculum => playlists}/mapping/list/PlaylistListScreen.kt (99%) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt index b00b3931a..9282f0c26 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt @@ -134,7 +134,7 @@ import world.respect.shared.viewmodel.report.list.ReportListViewModel import world.respect.shared.viewmodel.report.list.ReportTemplateListViewModel import world.respect.shared.viewmodel.manageuser.signup.CreateAccountViewModel import world.respect.app.view.settings.SettingsScreenForViewModel -import world.respect.app.view.curriculum.mapping.edit.CurriculumMappingEditScreenForViewModel +import world.respect.app.view.playlists.mapping.edit.CurriculumMappingEditScreenForViewModel import world.respect.shared.viewmodel.settings.SettingsViewModel import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistEditViewModel diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/launcher/AppLauncherScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/launcher/AppLauncherScreen.kt index 7f52b3306..5e40b804b 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/launcher/AppLauncherScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/apps/launcher/AppLauncherScreen.kt @@ -22,7 +22,7 @@ import org.jetbrains.compose.resources.stringResource import world.respect.app.app.RespectAsyncImage import world.respect.app.components.respectRememberPager import world.respect.app.components.uiTextStringResource -import world.respect.app.view.curriculum.mapping.list.PlaylistListScreen +import world.respect.app.view.playlists.mapping.list.PlaylistListScreen import world.respect.datalayer.DataLoadState import world.respect.datalayer.NoDataLoadedState import world.respect.datalayer.compatibleapps.model.RespectAppManifest diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt index 01bbadd6a..c6cdec239 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt @@ -67,6 +67,7 @@ fun LearningUnitDetailScreen( onClickShare = viewModel::onClickShare, onClickCopy = viewModel::onClickCopy, onClickDelete = viewModel::onClickDelete, + onConfirmSelection = viewModel::onConfirmSelection ) } else { SingleLessonDetailScreen( @@ -248,7 +249,6 @@ private fun SingleLessonDetailScreen( } } } - @Composable private fun PlaylistDetailScreen( uiState: LearningUnitDetailUiState, @@ -259,6 +259,7 @@ private fun PlaylistDetailScreen( onClickShare: () -> Unit, onClickCopy: () -> Unit, onClickDelete: () -> Unit, + onConfirmSelection: () -> Unit, ) { var expandedSections by remember { mutableStateOf(setOf()) } val mapping = uiState.mapping @@ -266,7 +267,9 @@ private fun PlaylistDetailScreen( Box(modifier = Modifier.fillMaxSize()) { LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(bottom = 88.dp) + contentPadding = PaddingValues( + bottom = if (uiState.isSelectionMode && uiState.selectedLessons.isNotEmpty()) 88.dp else 16.dp + ) ) { item("header") { Card( @@ -301,47 +304,59 @@ private fun PlaylistDetailScreen( ) } - if (!mapping?.description.isNullOrEmpty()) { - Text( - text = mapping?.description.orEmpty(), - fontSize = 14.sp, - color = MaterialTheme.colorScheme.onSurface, - lineHeight = 18.sp, - modifier = Modifier.weight(1f) - ) + Column(modifier = Modifier.weight(1f)) { + if (!mapping?.description.isNullOrEmpty()) { + Text( + text = mapping.description, + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurface, + lineHeight = 18.sp, + ) + } + + if (uiState.isSelectionMode && uiState.selectedLessons.isNotEmpty()) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "${uiState.selectedLessons.size} lessons selected", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Medium + ) + } } } - Spacer(modifier = Modifier.height(16.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly - ) { - ActionButton( - icon = Icons.Default.Share, - label = stringResource(Res.string.share), - onClick = onClickShare, - modifier = Modifier - .testTag("share_btn") - ) - ActionButton( - icon = Icons.Default.ContentCopy, - label = stringResource(Res.string.copy), - onClick = onClickCopy, - modifier = Modifier.testTag("copy_btn") - ) - ActionButton( - icon = Icons.Default.Task, - label = stringResource(Res.string.assign), - onClick = onClickAssign, - modifier = Modifier.testTag("assign_btn") - ) - ActionButton( - icon = Icons.Default.Delete, - label = stringResource(Res.string.delete), - onClick = onClickDelete, - modifier = Modifier.testTag("delete_btn") - ) + if (!uiState.isSelectionMode) { + Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + ActionButton( + icon = Icons.Default.Share, + label = stringResource(Res.string.share), + onClick = onClickShare, + modifier = Modifier.testTag("share_btn") + ) + ActionButton( + icon = Icons.Default.ContentCopy, + label = stringResource(Res.string.copy), + onClick = onClickCopy, + modifier = Modifier.testTag("copy_btn") + ) + ActionButton( + icon = Icons.Default.Task, + label = stringResource(Res.string.assign), + onClick = onClickAssign, + modifier = Modifier.testTag("assign_btn") + ) + ActionButton( + icon = Icons.Default.Delete, + label = stringResource(Res.string.delete), + onClick = onClickDelete, + modifier = Modifier.testTag("delete_btn") + ) + } } } } @@ -379,15 +394,17 @@ private fun PlaylistDetailScreen( modifier = Modifier.weight(1f) ) - IconButton( - onClick = { onClickAssignSection(section.uid) }, - modifier = Modifier.size(40.dp) - ) { - Icon( - Icons.Default.Task, - contentDescription = stringResource(Res.string.assign), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) + if (!uiState.isSelectionMode) { + IconButton( + onClick = { onClickAssignSection(section.uid) }, + modifier = Modifier.size(40.dp) + ) { + Icon( + Icons.Default.Task, + contentDescription = stringResource(Res.string.assign), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } } IconButton( @@ -398,8 +415,7 @@ private fun PlaylistDetailScreen( expandedSections + section.uid } }, - modifier = Modifier - .testTag("expand_collapse_icon_") + modifier = Modifier.testTag("expand_collapse_icon_") ) { Icon( if (isExpanded) Icons.Default.KeyboardArrowUp @@ -419,14 +435,35 @@ private fun PlaylistDetailScreen( LessonListItem( link = link, sectionLinkUiState = uiState.sectionLinkUiState, - onClickLesson = onClickLesson + onClickLesson = onClickLesson, + isSelectionMode = uiState.isSelectionMode, + isSelected = uiState.selectedLessons.contains(link) ) } } } } - if (uiState.showEditButton) { + if (uiState.isSelectionMode && uiState.selectedLessons.isNotEmpty()) { + Button( + onClick = onConfirmSelection, + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .padding(16.dp), + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary + ) + ) { + Text( + text = "Add ${uiState.selectedLessons.size} task${if (uiState.selectedLessons.size > 1) "s" else ""} to assignment", + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + modifier = Modifier.padding(vertical = 8.dp) + ) + } + } else if (uiState.showEditButton) { FloatingActionButton( onClick = onClickEdit, modifier = Modifier @@ -454,12 +491,13 @@ private fun PlaylistDetailScreen( } } } - @Composable private fun LessonListItem( link: PlaylistsMappingSectionLink, sectionLinkUiState: (PlaylistsMappingSectionLink) -> Flow>, - onClickLesson: (PlaylistsMappingSectionLink) -> Unit + onClickLesson: (PlaylistsMappingSectionLink) -> Unit, + isSelectionMode: Boolean = false, + isSelected: Boolean = false ) { val stateFlow = remember(link.href) { sectionLinkUiState(link) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/PlaylistEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/edit/PlaylistEditScreen.kt similarity index 99% rename from respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/PlaylistEditScreen.kt rename to respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/edit/PlaylistEditScreen.kt index 4469bda61..c2d950428 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/edit/PlaylistEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/edit/PlaylistEditScreen.kt @@ -1,4 +1,4 @@ -package world.respect.app.view.curriculum.mapping.edit +package world.respect.app.view.playlists.mapping.edit import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/PlaylistListScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/list/PlaylistListScreen.kt similarity index 99% rename from respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/PlaylistListScreen.kt rename to respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/list/PlaylistListScreen.kt index 1656301b8..346dcf9a0 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/curriculum/mapping/list/PlaylistListScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/list/PlaylistListScreen.kt @@ -1,4 +1,4 @@ -package world.respect.app.view.curriculum.mapping.list +package world.respect.app.view.playlists.mapping.list import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index 065dda959..fe30697a8 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -565,7 +565,8 @@ class LearningUnitDetail ( private val appManifestUrlStr: String, private val refererUrlStr: String? = null, val expectedIdentifier: String? = null, - private val mappingDataJson: String? = null + private val mappingDataJson: String? = null, + val isSelectionMode: Boolean = false, ) : RespectAppRoute { val learningUnitManifestUrl: Url @@ -598,10 +599,14 @@ class LearningUnitDetail ( appManifestUrlStr = appManifestUrl.toString(), refererUrlStr = refererUrl?.toString(), expectedIdentifier = expectedIdentifier, - mappingDataJson = null + mappingDataJson = null, + isSelectionMode = false, ) - fun createFromMapping(mapping: PlaylistsMapping): LearningUnitDetail { + fun createFromMapping( + mapping: PlaylistsMapping, + isSelectionMode: Boolean = false + ): LearningUnitDetail { val mappingJson = try { Json.encodeToString(PlaylistsMapping.serializer(), mapping) } catch (e: Exception) { @@ -613,7 +618,8 @@ class LearningUnitDetail ( appManifestUrlStr = "", refererUrlStr = null, expectedIdentifier = null, - mappingDataJson = mappingJson + mappingDataJson = mappingJson, + isSelectionMode = isSelectionMode, ) } } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt index 8dd946a71..237272440 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.json.Json import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject @@ -56,6 +57,7 @@ data class AppLauncherUiState( val canRemove: Boolean = false, val emptyListDescription: UiText? = null, val selectedTabIndex: Int = 0, + val isSelectionMode: Boolean = false, ) class AppLauncherViewModel( @@ -86,6 +88,7 @@ class AppLauncherViewModel( params = SchoolAppDataSource.GetListParams() ) } + init { _appUiState.update { it.copy( @@ -93,6 +96,7 @@ class AppLauncherViewModel( when (route.resultDest.resultKey) { AssignmentEditViewModel.KEY_PLAYLIST_SELECTION -> Res.string.add_task_to_assignment.asUiText() + else -> Res.string.home.asUiText() } @@ -104,14 +108,21 @@ class AppLauncherViewModel( showBackButton = route.resultDest != null, ) } - val showPlaylistTab = route.resultDest?.resultKey == AssignmentEditViewModel.KEY_PLAYLIST_SELECTION + + val isFromAssignmentEdit = + route.resultDest?.resultKey == AssignmentEditViewModel.KEY_PLAYLIST_SELECTION + _uiState.update { prev -> prev.copy( respectAppForSchoolApp = this@AppLauncherViewModel::respectAppForSchoolApp, apps = pagingSourceHolder, - selectedTabIndex = if (showPlaylistTab) 1 else 0, + selectedTabIndex = if (isFromAssignmentEdit) 1 else 0, + isSelectionMode = isFromAssignmentEdit, ) } + + playlistListViewModel.setSelectionMode(isFromAssignmentEdit) + viewModelScope.launch { playlistListViewModel.navCommandFlow.collect { navCommand -> _navCommandFlow.tryEmit(navCommand) @@ -122,7 +133,10 @@ class AppLauncherViewModel( playlistListViewModel.setMappings(savedMappings) viewModelScope.launch { playlistListViewModel.uiState.collect { state -> - cachedPlaylists = state.mappings + savedStateHandle[KEY_MAPPINGS_LIST] = json.encodeToString( + ListSerializer(PlaylistsMapping.serializer()), + state.mappings + ) } } @@ -142,7 +156,7 @@ class AppLauncherViewModel( _uiState.update { it.copy( canRemove = isAdmin, - emptyListDescription = if(isAdmin) + emptyListDescription = if (isAdmin) Res.string.empty_list_description_admin.asUiText() else Res.string.empty_list_description_non_admin.asUiText() @@ -180,7 +194,10 @@ class AppLauncherViewModel( private fun loadMappingsFromSavedState(savedStateHandle: SavedStateHandle): List { val mappingsJson = savedStateHandle.get(KEY_MAPPINGS_LIST) ?: return emptyList() return try { - json.decodeFromString>(mappingsJson) + json.decodeFromString( + ListSerializer(PlaylistsMapping.serializer()), + mappingsJson + ) } catch (e: Exception) { emptyList() } @@ -191,13 +208,13 @@ class AppLauncherViewModel( _navCommandFlow.tryEmit( NavCommand.Navigate( - if(route.resultDest != null) { + if (route.resultDest != null) { LearningUnitList.create( opdsFeedUrl = url.resolve(appData.learningUnits.toString()), appManifestUrl = url, resultDest = route.resultDest, ) - }else { + } else { AppsDetail.create( manifestUrl = url, resultDest = route.resultDest, @@ -237,6 +254,5 @@ class AppLauncherViewModel( companion object { const val KEY_MAPPINGS_LIST = "mappings_list" - var cachedPlaylists: List = emptyList() } } \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/detail/AssignmentDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/detail/AssignmentDetailViewModel.kt index 0f4d669be..3c12584dd 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/detail/AssignmentDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/detail/AssignmentDetailViewModel.kt @@ -14,6 +14,8 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.json.Json import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.koin.core.scope.Scope @@ -44,6 +46,7 @@ import world.respect.shared.util.ext.isAdminOrTeacher import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.FabUiState import world.respect.shared.viewmodel.apps.launcher.AppLauncherViewModel +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMapping data class AssignmentDetailUiState( val assignment: DataLoadState = DataLoadingState(), @@ -55,6 +58,7 @@ class AssignmentDetailViewModel( savedStateHandle: SavedStateHandle, accountManager: RespectAccountManager, private val respectAppDataSource: RespectAppDataSource, + private val json: Json, ) : RespectViewModel(savedStateHandle), KoinScopeComponent { override val scope: Scope = accountManager.requireActiveAccountScope() @@ -136,16 +140,31 @@ class AssignmentDetailViewModel( } fun onClickEdit() { + val availablePlaylists = getAvailablePlaylists() _navCommandFlow.tryEmit( NavCommand.Navigate( AssignmentEdit.create( uid = route.uid, - availablePlaylists = AppLauncherViewModel.cachedPlaylists + availablePlaylists = availablePlaylists ) ) ) } - + private fun getAvailablePlaylists(): List { + val mappingsJson = savedStateHandle.get(AppLauncherViewModel.KEY_MAPPINGS_LIST) + return if (mappingsJson != null) { + try { + json.decodeFromString( + ListSerializer(PlaylistsMapping.serializer()), + mappingsJson + ) + } catch (e: Exception) { + emptyList() + } + } else { + emptyList() + } + } fun learningUnitInfoFlowFor(url: Url): Flow> { return respectAppDataSource.opdsDataSource.loadOpdsPublication( url = url, params = DataLoadParams(), null, null diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/list/AssignmentListViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/list/AssignmentListViewModel.kt index 4a6740518..1b8a7ac40 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/list/AssignmentListViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/list/AssignmentListViewModel.kt @@ -10,6 +10,8 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.json.Json import org.koin.core.component.KoinScopeComponent import org.koin.core.component.inject import org.koin.core.scope.Scope @@ -35,6 +37,7 @@ import world.respect.shared.util.ext.isAdminOrTeacher import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.FabUiState import world.respect.shared.viewmodel.apps.launcher.AppLauncherViewModel +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMapping data class AssignmentListUiState( val assignments: IPagingSourceFactory = EmptyPagingSourceFactory(), @@ -45,6 +48,7 @@ class AssignmentListViewModel( savedStateHandle: SavedStateHandle, accountManager: RespectAccountManager, private val respectAppDataSource: RespectAppDataSource, + private val json: Json, ) : RespectViewModel(savedStateHandle), KoinScopeComponent { override val scope: Scope = accountManager.requireActiveAccountScope() @@ -105,17 +109,34 @@ class AssignmentListViewModel( ) } + fun onClickAdd() { + val availablePlaylists = getAvailablePlaylists() _navCommandFlow.tryEmit( NavCommand.Navigate( AssignmentEdit.create( uid = null, - availablePlaylists = AppLauncherViewModel.cachedPlaylists + availablePlaylists = availablePlaylists ) ) ) } + private fun getAvailablePlaylists(): List { + val mappingsJson = savedStateHandle.get(AppLauncherViewModel.KEY_MAPPINGS_LIST) + return if (mappingsJson != null) { + try { + json.decodeFromString( + ListSerializer(PlaylistsMapping.serializer()), + mappingsJson + ) + } catch (e: Exception) { + emptyList() + } + } else { + emptyList() + } + } fun learningUnitInfoFlowFor(url: Url): Flow> { return respectAppDataSource.opdsDataSource.loadOpdsPublication( url = url, params = DataLoadParams(), null, null diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt index 3fce3b5f8..63580bf31 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt @@ -14,6 +14,8 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.json.Json import world.respect.datalayer.DataLoadParams import world.respect.datalayer.DataLoadState import world.respect.datalayer.DataLoadingState @@ -38,6 +40,7 @@ import world.respect.shared.util.ext.resolve import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.getTitle import world.respect.shared.viewmodel.apps.launcher.AppLauncherViewModel +import world.respect.shared.viewmodel.assignment.edit.AssignmentEditViewModel import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistEditViewModel import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistSectionUiState import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMapping @@ -56,12 +59,14 @@ data class LearningUnitDetailUiState( }, val showCopyDialog: Boolean = false, val copyDialogName: String = "", + val isSelectionMode: Boolean = false, + val selectedLessons: Set = emptySet(), ) { val buttonsEnabled: Boolean get() = lessonDetail != null || mapping != null val showEditButton: Boolean - get() = mapping != null + get() = mapping != null && !isSelectionMode } class LearningUnitDetailViewModel( @@ -70,6 +75,7 @@ class LearningUnitDetailViewModel( private val launchAppUseCase: LaunchAppUseCase, private val ustadCache: UstadCache, private val resultReturner: NavResultReturner, + private val json: Json, ) : RespectViewModel(savedStateHandle) { private val _uiState = MutableStateFlow(LearningUnitDetailUiState()) @@ -80,17 +86,21 @@ class LearningUnitDetailViewModel( init { val mappingData = route.mappingData + val isSelectionMode = route.isSelectionMode if (mappingData != null) { _uiState.update { it.copy( mapping = mappingData, - sectionLinkUiState = this@LearningUnitDetailViewModel::sectionLinkUiStateFor + sectionLinkUiState = this@LearningUnitDetailViewModel::sectionLinkUiStateFor, + isSelectionMode = isSelectionMode ) } _appUiState.update { it.copy( - title = mappingData.title.asUiText() + title = mappingData.title.asUiText(), + hideBottomNavigation = isSelectionMode, + showBackButton = true, ) } } else { @@ -113,7 +123,9 @@ class LearningUnitDetailViewModel( _appUiState.update { it.copy( - title = result.data.metadata.title.getTitle().asUiText() + title = result.data.metadata.title.getTitle().asUiText(), + showBackButton = true, + hideBottomNavigation = false ) } } @@ -177,6 +189,7 @@ class LearningUnitDetailViewModel( } } } + private suspend fun loadLessonPublications( lessons: List ): List { @@ -204,8 +217,26 @@ class LearningUnitDetailViewModel( } } } + + private fun getAvailablePlaylists(): List { + val mappingsJson = savedStateHandle.get(AppLauncherViewModel.KEY_MAPPINGS_LIST) + return if (mappingsJson != null) { + try { + json.decodeFromString( + ListSerializer(PlaylistsMapping.serializer()), + mappingsJson + ) + } catch (e: Exception) { + emptyList() + } + } else { + emptyList() + } + } + fun onClickAssign() { val mapping = uiState.value.mapping + val availablePlaylists = getAvailablePlaylists() if (mapping != null) { val allLessons = mapping.sections.flatMap { section -> @@ -222,7 +253,7 @@ class LearningUnitDetailViewModel( destination = AssignmentEdit.createWithMultipleLessons( uid = null, learningUnits = learningUnitSelections, - availablePlaylists = AppLauncherViewModel.cachedPlaylists + availablePlaylists = availablePlaylists ) ) ) @@ -234,7 +265,7 @@ class LearningUnitDetailViewModel( destination = AssignmentEdit.create( uid = null, learningUnitSelected = null, - availablePlaylists = AppLauncherViewModel.cachedPlaylists + availablePlaylists = availablePlaylists ) ) ) @@ -250,16 +281,18 @@ class LearningUnitDetailViewModel( selectedPublication = publicationVal, appManifestUrl = route.appManifestUrl, ), - availablePlaylists = AppLauncherViewModel.cachedPlaylists + availablePlaylists = availablePlaylists ) ) ) } } + fun onClickAssignSection(sectionUid: Long) { val mapping = _uiState.value.mapping ?: return val section = mapping.sections.find { it.uid == sectionUid } ?: return val sectionLessons = section.items.filter { it.appManifestUrl != null } + val availablePlaylists = getAvailablePlaylists() if (sectionLessons.isNotEmpty()) { viewModelScope.launch { @@ -271,7 +304,7 @@ class LearningUnitDetailViewModel( destination = AssignmentEdit.createWithMultipleLessons( uid = null, learningUnits = learningUnitSelections, - availablePlaylists = AppLauncherViewModel.cachedPlaylists + availablePlaylists = availablePlaylists ) ) ) @@ -280,20 +313,55 @@ class LearningUnitDetailViewModel( } } + fun onLessonSelectionToggle(link: PlaylistsMappingSectionLink) { + _uiState.update { prev -> + val selected = if (prev.selectedLessons.contains(link)) { + prev.selectedLessons - link + } else { + prev.selectedLessons + link + } + prev.copy(selectedLessons = selected) + } + } + + fun onConfirmSelection() { + val selectedLessons = _uiState.value.selectedLessons.toList() + + if (selectedLessons.isEmpty()) return + + viewModelScope.launch { + val learningUnitSelections = loadLessonPublications(selectedLessons) + + if (learningUnitSelections.isNotEmpty()) { + resultReturner.sendResult( + NavResult( + key = AssignmentEditViewModel.KEY_PLAYLIST_SELECTION, + result = learningUnitSelections + ) + ) + _navCommandFlow.tryEmit(NavCommand.PopUp()) + } + } + } + fun onClickLesson(link: PlaylistsMappingSectionLink) { - val publicationUrl = Url(link.href) - val appManifestUrl = link.appManifestUrl ?: return + if (_uiState.value.isSelectionMode) { + onLessonSelectionToggle(link) + } else { + val publicationUrl = Url(link.href) + val appManifestUrl = link.appManifestUrl ?: return - _navCommandFlow.tryEmit( - NavCommand.Navigate( - LearningUnitDetail.create( - learningUnitManifestUrl = publicationUrl, - appManifestUrl = appManifestUrl, - refererUrl = publicationUrl, - expectedIdentifier = null + _navCommandFlow.tryEmit( + NavCommand.Navigate( + LearningUnitDetail.create( + learningUnitManifestUrl = publicationUrl, + appManifestUrl = appManifestUrl, + refererUrl = publicationUrl, + expectedIdentifier = null + ) ) ) - ) + } } fun onClickEdit() { diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/list/PlaylistListViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/list/PlaylistListViewModel.kt index b757465db..7d4076460 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/list/PlaylistListViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/list/PlaylistListViewModel.kt @@ -17,6 +17,7 @@ import world.respect.shared.navigation.NavCommand import world.respect.shared.navigation.NavResultReturner import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel +import world.respect.shared.viewmodel.assignment.edit.AssignmentEditViewModel import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistEditViewModel import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMapping import world.respect.shared.generated.resources.Res @@ -29,6 +30,7 @@ data class PlaylistListUiState( val error: UiText? = null, val currentUserGuid: String? = null, val currentSchoolUrl: Url? = null, + val isSelectionMode: Boolean = false, ) { val filteredMappings: List get() = when (selectedFilterIndex) { @@ -89,16 +91,25 @@ class PlaylistListViewModel( _uiState.update { it.copy(mappings = mappings) } } + fun setSelectionMode(isSelectionMode: Boolean) { + _uiState.update { it.copy(isSelectionMode = isSelectionMode) } + } + fun onFilterSelected(index: Int) { _uiState.update { it.copy(selectedFilterIndex = index) } } + fun onClickMapping(mapping: PlaylistsMapping) { _navCommandFlow.tryEmit( NavCommand.Navigate( - LearningUnitDetail.createFromMapping(mapping) + LearningUnitDetail.createFromMapping( + mapping = mapping, + isSelectionMode = _uiState.value.isSelectionMode + ) ) ) } + fun onClickAddNew() { _navCommandFlow.tryEmit( NavCommand.Navigate( From 1f7c04313ac2a363e232fbf59a6dd92961c9da5b Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Fri, 2 Jan 2026 11:07:25 +0530 Subject: [PATCH 45/58] change the button name add from playlist to Task. --- .../assignment/edit/AssignmentEditScreen.kt | 18 +----------------- .../composeResources/values/strings.xml | 6 +++--- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt index 324767be5..0aaa00eff 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/assignment/edit/AssignmentEditScreen.kt @@ -202,22 +202,6 @@ fun AssignmentEditScreen( text = stringResource(Res.string.assignment_tasks), style = MaterialTheme.typography.titleMedium ) - ListItem( - modifier = Modifier.fillMaxWidth().clickable { - onClickAddLearningUnit() - }, - leadingContent = { - Icon( - imageVector = Icons.Default.Add, - modifier = Modifier.size(40.dp).padding(8.dp), - contentDescription = "" - ) - }, - headlineContent = { - Text(stringResource(Res.string.lesson_assessment)) - } - ) - if (uiState.showPlaylistButton) { ListItem( modifier = Modifier.fillMaxWidth().clickable { @@ -225,7 +209,7 @@ fun AssignmentEditScreen( }, leadingContent = { Icon( - imageVector = Icons.Default.LibraryAdd, + imageVector = Icons.Default.Add, modifier = Modifier.size(40.dp).padding(8.dp), contentDescription = "", ) diff --git a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml index f3cb5ce50..97af65c79 100644 --- a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml +++ b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml @@ -440,7 +440,7 @@ Date Time - Assignment tasks + Tasks Lesson/assessment Due date @@ -479,7 +479,7 @@ copy playlist Make a copy %1$d sections • %2$d items - Add From Playlist + Task Select Playlist Unit - Add task to assignment + Add task to assignment From 7a4701814119d0301f10acf37c430266b30bbc2a Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Fri, 2 Jan 2026 17:20:04 +0530 Subject: [PATCH 46/58] Add checkboox --- .../detail/LearningUnitDetailScreen.kt | 66 +++++++++++-------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt index c6cdec239..561e1e04d 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt @@ -513,33 +513,44 @@ private fun LessonListItem( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp) ) { - linkData?.icon?.let { iconUrl -> - Box( - modifier = Modifier - .size(48.dp) - .clip(RoundedCornerShape(4.dp)) - ) { - RespectAsyncImage( - uri = iconUrl.toString(), - contentDescription = "", - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize() - ) - } - } ?: run { - Box( - modifier = Modifier - .size(48.dp) - .clip(RoundedCornerShape(4.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant), - contentAlignment = Alignment.Center - ) { - Icon( - Icons.Default.Android, - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) + if (isSelectionMode) { + Icon( + imageVector = if (isSelected) Icons.Default.CheckBox + else Icons.Default.CheckBoxOutlineBlank, + contentDescription = null, + tint = if (isSelected) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(24.dp) + ) + } else { + linkData?.icon?.let { iconUrl -> + Box( + modifier = Modifier + .size(48.dp) + .clip(RoundedCornerShape(4.dp)) + ) { + RespectAsyncImage( + uri = iconUrl.toString(), + contentDescription = "", + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + } + } ?: run { + Box( + modifier = Modifier + .size(48.dp) + .clip(RoundedCornerShape(4.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant), + contentAlignment = Alignment.Center + ) { + Icon( + Icons.Default.Android, + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } } } @@ -563,7 +574,6 @@ private fun LessonListItem( } } } - @Composable private fun ActionButton( icon: ImageVector, From e968384f78df86243f0a893ec073be155ec6e430 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Mon, 5 Jan 2026 13:10:34 +0530 Subject: [PATCH 47/58] fix assignmentedit --- .../edit/AssignmentEditViewModel.kt | 129 +++++++++++++----- 1 file changed, 93 insertions(+), 36 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt index 08ba47d74..2ffbbcc90 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt @@ -25,7 +25,6 @@ import world.respect.datalayer.ext.dataOrNull import world.respect.datalayer.ext.isReadyAndSettled import world.respect.datalayer.school.ClassDataSource import world.respect.datalayer.school.model.Assignment -import world.respect.datalayer.school.model.AssignmentAssigneeRef import world.respect.datalayer.school.model.AssignmentLearningUnitRef import world.respect.datalayer.school.model.Clazz import world.respect.lib.opds.model.OpdsPublication @@ -47,7 +46,10 @@ import world.respect.shared.util.LaunchDebouncer import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.ActionBarButtonUiState +import world.respect.shared.viewmodel.assignment.toAssignmentLearningUnitRefs import world.respect.shared.viewmodel.learningunit.LearningUnitSelection +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMapping +import world.respect.shared.viewmodel.playlists.mapping.toOpdsGroup import kotlin.time.Clock data class AssignmentEditUiState( @@ -57,6 +59,7 @@ data class AssignmentEditUiState( val classOptions: List = emptyList(), val classError: UiText? = null, val learningUnitInfoFlow: (Url) -> Flow> = { flowOf(DataLoadingState()) }, + val showPlaylistButton: Boolean = true, ) { val fieldsEnabled: Boolean get() = assignment.isReadyAndSettled() @@ -116,6 +119,53 @@ class AssignmentEditViewModel( ) } + viewModelScope.launch { + resultReturner.filteredResultFlowForKey(KEY_LEARNING_UNIT).collect { result -> + val learningUnit = result.result as? LearningUnitSelection ?: return@collect + val assignmentResourceRef = learningUnit.toRef() + + _uiState.update { prev -> + val prevAssignment = prev.assignment.dataOrNull() ?: return@update prev + + prev.copy( + assignment = DataReadyState( + data = prevAssignment.copy( + learningUnits = prevAssignment.learningUnits + assignmentResourceRef + ) + ) + ) + } + } + } + + viewModelScope.launch { + resultReturner.filteredResultFlowForKey(KEY_PLAYLIST_SELECTION).collect { result -> + val mapping = result.result as? PlaylistsMapping ?: return@collect + val group = mapping.toOpdsGroup() + val newLearningUnits = group.toAssignmentLearningUnitRefs() + + val assignment = _uiState.value.assignment.dataOrNull() ?: return@collect + + val existingUrls = assignment.learningUnits.map { + it.learningUnitManifestUrl + }.toSet() + + val uniqueNewUnits = newLearningUnits.filter { + it.learningUnitManifestUrl !in existingUrls + } + + _uiState.update { prev -> + prev.copy( + assignment = DataReadyState( + assignment.copy( + learningUnits = assignment.learningUnits + uniqueNewUnits + ) + ) + ) + } + } + } + launchWithLoadingIndicator { val classes = schoolDataSource.classDataSource.list( DataLoadParams(), @@ -140,7 +190,7 @@ class AssignmentEditViewModel( }, uiUpdateFn = { entity -> _uiState.update { prev -> - val assigneeClassUid = entity.dataOrNull()?.assignees?.firstOrNull()?.uid + val assigneeClassUid = entity.dataOrNull()?.classUid prev.copy( assignment = entity, assigneeText = classes.firstOrNull { @@ -150,7 +200,25 @@ class AssignmentEditViewModel( } } ) + + viewModelScope.launch { + schoolDataSource.assignmentDataSource.findByGuidAsFlow( + route.guid + ).collect { assignmentState -> + if (assignmentState is DataReadyState) { + val currentLearningUnits = _uiState.value.assignment.dataOrNull()?.learningUnits + val newLearningUnits = assignmentState.data.learningUnits + if (currentLearningUnits != newLearningUnits) { + _uiState.update { prev -> + prev.copy(assignment = assignmentState) + } + } + } + } + } }else { + val initialLearningUnits = route.learningUnitSelectedList?.map { it.toRef() } ?: emptyList() + _uiState.update { prev -> prev.copy( assignment = DataReadyState( @@ -158,36 +226,29 @@ class AssignmentEditViewModel( uid = uid, title = "", description = "", - learningUnits = route.learningUnitSelected?.let { - listOf(it.toRef()) - } ?: emptyList() + classUid = "", + learningUnits = initialLearningUnits ) ) ) } } - - viewModelScope.launch { - resultReturner.filteredResultFlowForKey(KEY_LEARNING_UNIT).collect { result -> - val learningUnit = result.result as? LearningUnitSelection ?: return@collect - val assignmentResourceRef = learningUnit.toRef() - - _uiState.update { prev -> - val prevAssignment = prev.assignment.dataOrNull() ?: return@update prev - - prev.copy( - assignment = DataReadyState( - data = prevAssignment.copy( - learningUnits = prevAssignment.learningUnits + assignmentResourceRef - ) - ) - ) - } - } - } } } + fun onClickAddFromPlaylists() { + _navCommandFlow.tryEmit( + NavCommand.Navigate( + RespectAppLauncher.create( + resultDest = RouteResultDest( + resultPopUpTo = route, + resultKey = KEY_PLAYLIST_SELECTION, + ) + ) + ) + ) + } + fun learningUnitInfoFlowFor(url: Url): Flow> { return respectAppDataSource.opdsDataSource.loadOpdsPublication( url = url, params = DataLoadParams(), null, null @@ -200,9 +261,7 @@ class AssignmentEditViewModel( it.copy( assignment = DataReadyState( assignment.copy( - assignees = listOf( - AssignmentAssigneeRef(uid = clazz.guid) - ) + classUid = clazz.guid ) ), assigneeText = clazz.title, @@ -222,7 +281,10 @@ class AssignmentEditViewModel( } debouncer.launch(DEFAULT_SAVED_STATE_KEY) { - savedStateHandle[DEFAULT_SAVED_STATE_KEY] = json.encodeToString(assignment) + savedStateHandle[DEFAULT_SAVED_STATE_KEY] = json.encodeToString( + Assignment.serializer(), + assignment + ) } } @@ -232,7 +294,6 @@ class AssignmentEditViewModel( } } - fun onClickAddLearningUnit() { _navCommandFlow.tryEmit( NavCommand.Navigate( @@ -246,9 +307,7 @@ class AssignmentEditViewModel( ) } - fun onClickRemoveLearningUnit( - ref: AssignmentLearningUnitRef - ) { + fun onClickRemoveLearningUnit(ref: AssignmentLearningUnitRef) { val assignment = uiState.value.assignment.dataOrNull() ?: return _uiState.update { prev -> @@ -273,7 +332,7 @@ class AssignmentEditViewModel( assignmentVal?.title.isNullOrBlank() }, classError = Res.string.required_field.asUiText().takeIf { - assignmentVal?.assignees?.isEmpty() != false + assignmentVal?.classUid.isNullOrBlank() } ) } @@ -303,9 +362,7 @@ class AssignmentEditViewModel( } companion object { - const val KEY_LEARNING_UNIT = "result_learning_unit" - + const val KEY_PLAYLIST_SELECTION = "result_playlist_selection" } - } \ No newline at end of file From bbb2188498adc512975e5d047baaba798a999d79 Mon Sep 17 00:00:00 2001 From: pooja Date: Mon, 5 Jan 2026 13:17:19 +0400 Subject: [PATCH 48/58] removed 000 test --- .maestro/flows/000_000_hello_world.yaml | 44 ------------------------- 1 file changed, 44 deletions(-) delete mode 100644 .maestro/flows/000_000_hello_world.yaml diff --git a/.maestro/flows/000_000_hello_world.yaml b/.maestro/flows/000_000_hello_world.yaml deleted file mode 100644 index d4b1e93c8..000000000 --- a/.maestro/flows/000_000_hello_world.yaml +++ /dev/null @@ -1,44 +0,0 @@ -appId: world.respect.app -onFlowStart: - - clearState: world.respect.app - - runScript: - file: "scripts/school_init.js" - env: - TESTCONTROLLER_URL: ${TESTCONTROLLER_URL} - SCHOOL_ADMIN_PASSWORD: ${SCHOOL_ADMIN_PASSWORD} - DIR_ADMIN_AUTH_HEADER: ${DIR_ADMIN_AUTH_HEADER} - SCHOOL_URL: ${SCHOOL_URL} - SCHOOL_NAME: ${SCHOOL_NAME} - URL_SUBSTITUTION: ${URL_SUBSTITUTION} - NAME: "000_000_hello_world" - -onFlowComplete: - - runScript: - file: "scripts/teardown.js" ---- - -- launchApp: - arguments: - respect_directory: ${output.SCHOOL_URL} - -- tapOn: "Get Started" - -- runFlow: - file: "subflows/get_started_select_school_by_name.yaml" - env: - SCHOOL_NAME: ${SCHOOL_NAME} - -- tapOn: - id: "username" -- inputText: "admin" -- tapOn: - id : "password" -- inputText: ${SCHOOL_ADMIN_PASSWORD} -- tapOn: "Login" -- runFlow: - when: - visible: "Save password for Respect?" - file: "subflows/save_password_prompt_cancel.yaml" -- assertVisible: "Apps" -- tapOn: - id: "user_account_icon" \ No newline at end of file From d6b4fd9471bb7273a14e323903faa4faa1b8016c Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Mon, 5 Jan 2026 15:46:49 +0530 Subject: [PATCH 49/58] update learningunitDetailscreen --- .../view/learningunit/detail/LearningUnitDetailScreen.kt | 8 +++++++- .../src/commonMain/composeResources/values/strings.xml | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt index 561e1e04d..0490af9ca 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt @@ -249,6 +249,7 @@ private fun SingleLessonDetailScreen( } } } + @Composable private fun PlaylistDetailScreen( uiState: LearningUnitDetailUiState, @@ -457,7 +458,10 @@ private fun PlaylistDetailScreen( ) ) { Text( - text = "Add ${uiState.selectedLessons.size} task${if (uiState.selectedLessons.size > 1) "s" else ""} to assignment", + text = stringResource( + Res.string.add_tasks_to_assignment, + uiState.selectedLessons.size + ), fontSize = 16.sp, fontWeight = FontWeight.Medium, modifier = Modifier.padding(vertical = 8.dp) @@ -491,6 +495,7 @@ private fun PlaylistDetailScreen( } } } + @Composable private fun LessonListItem( link: PlaylistsMappingSectionLink, @@ -574,6 +579,7 @@ private fun LessonListItem( } } } + @Composable private fun ActionButton( icon: ImageVector, diff --git a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml index 97af65c79..38cccfb8e 100644 --- a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml +++ b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml @@ -482,4 +482,5 @@ Task Select Playlist Unit Add task to assignment + Add %1$d tasks to assignment From 5ae0a8d64994dd70f51e54db75fa1182fee135c1 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Tue, 6 Jan 2026 12:18:03 +0530 Subject: [PATCH 50/58] Added the share screen --- gradle/libs.versions.toml | 4 +- respect-app-compose/build.gradle.kts | 1 + .../src/androidMain/AndroidManifest.xml | 17 +- .../kotlin/world/respect/AppKoinModule.kt | 2 + .../world/respect/app/app/AppNavHost.kt | 12 + .../detail/LearningUnitDetailScreen.kt | 130 +++++++--- .../mapping/share/PlaylistShareScreen.kt | 234 ++++++++++++++++++ .../composeResources/values/strings.xml | 11 + .../respect/shared/navigation/AppRoutes.kt | 9 + .../detail/LearningUnitDetailViewModel.kt | 37 ++- .../mapping/share/PlaylistShareViewModel.kt | 102 ++++++++ 11 files changed, 512 insertions(+), 47 deletions(-) create mode 100644 respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/share/PlaylistShareScreen.kt create mode 100644 respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/share/PlaylistShareViewModel.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9b0235504..b808ca507 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -62,6 +62,8 @@ webauthn4j-core = "0.25.0.RELEASE" androidx-preference = "1.2.1" buildconfig-plugin = "6.0.6" +qr-kit = "3.1.3" + [libraries] argparse4j = { module = "net.sourceforge.argparse4j:argparse4j", version.ref = "argparse4j" } coil-network-okhttp = { module = "io.coil-kt.coil3:coil-network-okhttp", version.ref = "coil3" } @@ -158,7 +160,7 @@ acra-http = { module = "ch.acra:acra-http", version.ref = "acra" } acra-core = { module = "ch.acra:acra-core", version.ref = "acra" } reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" } - +qr-kit = { module = "network.chaintech:qr-kit", version.ref = "qr-kit" } diff --git a/respect-app-compose/build.gradle.kts b/respect-app-compose/build.gradle.kts index 6c4ebefe7..828a80c72 100644 --- a/respect-app-compose/build.gradle.kts +++ b/respect-app-compose/build.gradle.kts @@ -142,6 +142,7 @@ kotlin { implementation(libs.kotlinx.io.core) implementation(libs.androidx.paging.compose) implementation(libs.reorderable) + implementation(libs.qr.kit) } desktopMain.dependencies { diff --git a/respect-app-compose/src/androidMain/AndroidManifest.xml b/respect-app-compose/src/androidMain/AndroidManifest.xml index 470deae11..7f7a8ea5b 100644 --- a/respect-app-compose/src/androidMain/AndroidManifest.xml +++ b/respect-app-compose/src/androidMain/AndroidManifest.xml @@ -1,8 +1,17 @@ - + - + + + + + + + + android:usesCleartextTraffic="true" + tools:replace="android:allowBackup,android:name"> + diff --git a/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt b/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt index 710bb5f2a..e6c9f1163 100644 --- a/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt +++ b/respect-app-compose/src/androidMain/kotlin/world/respect/AppKoinModule.kt @@ -219,6 +219,7 @@ import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistEditViewMod import world.respect.shared.viewmodel.schooldirectory.edit.SchoolDirectoryEditViewModel import world.respect.shared.viewmodel.schooldirectory.list.SchoolDirectoryListViewModel import world.respect.shared.viewmodel.playlists.mapping.list.PlaylistListViewModel +import world.respect.shared.viewmodel.playlists.mapping.share.PlaylistShareViewModel const val SHARED_PREF_SETTINGS_NAME = "respect_settings3_" @@ -337,6 +338,7 @@ val appKoinModule = module { viewModelOf(::EnrollmentListViewModel) viewModelOf(::EnrollmentEditViewModel) viewModelOf(::PlaylistListViewModel) + viewModelOf(::PlaylistShareViewModel) single { diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt index 9282f0c26..513bf492b 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt @@ -135,13 +135,16 @@ import world.respect.shared.viewmodel.report.list.ReportTemplateListViewModel import world.respect.shared.viewmodel.manageuser.signup.CreateAccountViewModel import world.respect.app.view.settings.SettingsScreenForViewModel import world.respect.app.view.playlists.mapping.edit.CurriculumMappingEditScreenForViewModel +import world.respect.app.view.playlists.mapping.share.PlaylistShareScreen import world.respect.shared.viewmodel.settings.SettingsViewModel import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistEditViewModel import world.respect.shared.navigation.Settings import world.respect.shared.navigation.CurriculumMappingEdit +import world.respect.shared.navigation.PlaylistShare import world.respect.shared.viewmodel.onboarding.OnboardingViewModel +import world.respect.shared.viewmodel.playlists.mapping.share.PlaylistShareViewModel import world.respect.shared.viewmodel.schooldirectory.edit.SchoolDirectoryEditViewModel import world.respect.shared.viewmodel.schooldirectory.list.SchoolDirectoryListViewModel @@ -547,6 +550,15 @@ fun AppNavHost( viewModel = viewModel ) } + composable { + val viewModel: PlaylistShareViewModel = respectViewModel( + onSetAppUiState = onSetAppUiState, + navController = respectNavController + ) + PlaylistShareScreen( + viewModel = viewModel + ) + } composable{ val viewModel: SchoolDirectoryListViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt index 0490af9ca..067b28717 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt @@ -1,5 +1,6 @@ package world.respect.app.view.learningunit.detail +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -67,7 +68,10 @@ fun LearningUnitDetailScreen( onClickShare = viewModel::onClickShare, onClickCopy = viewModel::onClickCopy, onClickDelete = viewModel::onClickDelete, - onConfirmSelection = viewModel::onConfirmSelection + onConfirmSelection = viewModel::onConfirmSelection, + onClickSelectAll = viewModel::onClickSelectAll, + onClickSelectNone = viewModel::onClickSelectNone, + onClickToggleSectionSelection = viewModel::onClickToggleSectionSelection ) } else { SingleLessonDetailScreen( @@ -261,6 +265,9 @@ private fun PlaylistDetailScreen( onClickCopy: () -> Unit, onClickDelete: () -> Unit, onConfirmSelection: () -> Unit, + onClickSelectAll: () -> Unit, + onClickSelectNone: () -> Unit, + onClickToggleSectionSelection: (Long) -> Unit, ) { var expandedSections by remember { mutableStateOf(setOf()) } val mapping = uiState.mapping @@ -363,68 +370,109 @@ private fun PlaylistDetailScreen( } } + if (uiState.isSelectionMode) { + item("selection_controls") { + Row( + modifier = Modifier + .fillMaxWidth() + .defaultItemPadding(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedButton( + onClick = onClickSelectAll, + modifier = Modifier.testTag("select_all_btn"), + shape = RoundedCornerShape(20.dp), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline) + ) { + Text( + text = stringResource(Res.string.select_all), + fontSize = 14.sp + ) + } + + OutlinedButton( + onClick = onClickSelectNone, + modifier = Modifier.testTag("select_none_btn"), + shape = RoundedCornerShape(20.dp), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline) + ) { + Text( + text = stringResource(Res.string.select_none), + fontSize = 14.sp + ) + } + } + } + } + mapping?.sections?.forEachIndexed { sectionIndex, section -> item(key = "section_${section.uid}") { val isExpanded = expandedSections.contains(section.uid) + val sectionLessons = section.items + val allSectionLessonsSelected = sectionLessons.all { + uiState.selectedLessons.contains(it) + } - Card( + Row( modifier = Modifier .fillMaxWidth() - .defaultItemPadding() .clickable { expandedSections = if (isExpanded) { expandedSections - section.uid } else { expandedSections + section.uid } - }, - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) + } + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = section.title, - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, - modifier = Modifier.weight(1f) + if (uiState.isSelectionMode) { + Checkbox( + checked = allSectionLessonsSelected, + onCheckedChange = { checked -> + onClickToggleSectionSelection(section.uid) + }, + modifier = Modifier.size(24.dp) ) + Spacer(modifier = Modifier.width(12.dp)) + } - if (!uiState.isSelectionMode) { - IconButton( - onClick = { onClickAssignSection(section.uid) }, - modifier = Modifier.size(40.dp) - ) { - Icon( - Icons.Default.Task, - contentDescription = stringResource(Res.string.assign), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } + Text( + text = section.title, + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.weight(1f) + ) + if (!uiState.isSelectionMode) { IconButton( - onClick = { - expandedSections = if (isExpanded) { - expandedSections - section.uid - } else { - expandedSections + section.uid - } - }, - modifier = Modifier.testTag("expand_collapse_icon_") + onClick = { onClickAssignSection(section.uid) }, + modifier = Modifier.size(40.dp) ) { Icon( - if (isExpanded) Icons.Default.KeyboardArrowUp - else Icons.Default.KeyboardArrowDown, - contentDescription = null + Icons.Default.Task, + contentDescription = stringResource(Res.string.assign), + tint = MaterialTheme.colorScheme.onSurfaceVariant ) } } + + IconButton( + onClick = { + expandedSections = if (isExpanded) { + expandedSections - section.uid + } else { + expandedSections + section.uid + } + }, + modifier = Modifier.testTag("expand_collapse_icon_") + ) { + Icon( + if (isExpanded) Icons.Default.KeyboardArrowUp + else Icons.Default.KeyboardArrowDown, + contentDescription = null + ) + } } } diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/share/PlaylistShareScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/share/PlaylistShareScreen.kt new file mode 100644 index 000000000..10cd68e57 --- /dev/null +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/share/PlaylistShareScreen.kt @@ -0,0 +1,234 @@ +package world.respect.app.view.playlists.mapping.share + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.jetbrains.compose.resources.stringResource +import qrgenerator.qrkitpainter.rememberQrKitPainter +import world.respect.app.components.defaultItemPadding +import world.respect.shared.generated.resources.Res +import world.respect.shared.generated.resources.anyone_with_the_link +import world.respect.shared.generated.resources.copy_link +import world.respect.shared.generated.resources.send_link_via_email +import world.respect.shared.generated.resources.send_link_via_sms +import world.respect.shared.generated.resources.share_link +import world.respect.shared.generated.resources.teacher_admin_in_my_school +import world.respect.shared.generated.resources.who_can_edit +import world.respect.shared.generated.resources.who_can_view +import world.respect.shared.viewmodel.playlists.mapping.share.PlaylistShareUiState +import world.respect.shared.viewmodel.playlists.mapping.share.PlaylistShareViewModel + + +@Composable +fun PlaylistShareScreen( + viewModel: PlaylistShareViewModel +) { + val uiState by viewModel.uiState.collectAsState() + + PlaylistShareScreen( + uiState = uiState, + onClickShareLink = viewModel::onClickShareLink, + onClickCopyLink = viewModel::onClickCopyLink, + onClickSendViaSms = viewModel::onClickSendViaSms, + onClickSendViaEmail = viewModel::onClickSendViaEmail, + onViewPermissionChanged = viewModel::onViewPermissionChanged, + onEditPermissionChanged = viewModel::onEditPermissionChanged, + ) +} + +@Composable +fun PlaylistShareScreen( + uiState: PlaylistShareUiState, + onClickShareLink: () -> Unit = {}, + onClickCopyLink: () -> Unit = {}, + onClickSendViaSms: () -> Unit = {}, + onClickSendViaEmail: () -> Unit = {}, + onViewPermissionChanged: (String) -> Unit = {}, + onEditPermissionChanged: (String) -> Unit = {}, +) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.surface) + ) { + item("qr_code") { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + val shareUrlStr = uiState.shareUrl?.toString().orEmpty() + + if (shareUrlStr.isNotEmpty()) { + val qrCodePainter = rememberQrKitPainter(data = shareUrlStr) + + Image( + painter = qrCodePainter, + contentDescription = "QR Code for sharing playlist", + modifier = Modifier.size(200.dp) + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = shareUrlStr, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + item("permissions") { + Column( + modifier = Modifier + .fillMaxWidth() + .defaultItemPadding() + ) { + PermissionDropdown( + label = stringResource(Res.string.who_can_view), + selectedValue = uiState.viewPermission, + options = listOf( + stringResource(Res.string.anyone_with_the_link), + stringResource(Res.string.teacher_admin_in_my_school) + ), + onValueChanged = onViewPermissionChanged + ) + + Spacer(modifier = Modifier.height(16.dp)) + + PermissionDropdown( + label = stringResource(Res.string.who_can_edit), + selectedValue = uiState.editPermission, + options = listOf( + stringResource(Res.string.teacher_admin_in_my_school) + ), + onValueChanged = onEditPermissionChanged + ) + } + } + + item("share_link") { + ListItem( + modifier = Modifier + .fillMaxWidth() + .clickable { onClickShareLink() }, + leadingContent = { + Icon( + Icons.Default.Share, + contentDescription = null + ) + }, + headlineContent = { + Text(stringResource(Res.string.share_link)) + } + ) + } + + item("copy_link") { + ListItem( + modifier = Modifier + .fillMaxWidth() + .clickable { onClickCopyLink() }, + leadingContent = { + Icon( + Icons.Default.ContentCopy, + contentDescription = null + ) + }, + headlineContent = { + Text(stringResource(Res.string.copy_link)) + } + ) + } + + item("send_sms") { + ListItem( + modifier = Modifier + .fillMaxWidth() + .clickable { onClickSendViaSms() }, + leadingContent = { + Icon( + Icons.Default.Sms, + contentDescription = null + ) + }, + headlineContent = { + Text(stringResource(Res.string.send_link_via_sms)) + } + ) + } + + item("send_email") { + ListItem( + modifier = Modifier + .fillMaxWidth() + .clickable { onClickSendViaEmail() }, + leadingContent = { + Icon( + Icons.Default.Email, + contentDescription = null + ) + }, + headlineContent = { + Text(stringResource(Res.string.send_link_via_email)) + } + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun PermissionDropdown( + label: String, + selectedValue: String, + options: List, + onValueChanged: (String) -> Unit +) { + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = it } + ) { + OutlinedTextField( + value = selectedValue, + onValueChange = {}, + readOnly = true, + label = { Text(label) }, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) + }, + modifier = Modifier + .fillMaxWidth() + .menuAnchor(MenuAnchorType.PrimaryNotEditable, enabled = true) + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + options.forEach { option -> + DropdownMenuItem( + text = { Text(option) }, + onClick = { + onValueChanged(option) + expanded = false + } + ) + } + } + } +} \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml index 38cccfb8e..b9ec38681 100644 --- a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml +++ b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml @@ -483,4 +483,15 @@ Select Playlist Unit Add task to assignment Add %1$d tasks to assignment + Select all + Select none + + Who can view + Who can edit + Anyone with the link + Teacher/admin in my school + Share link + Copy link + Send link via SMS + Send link via email diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index 891a081bb..094e26360 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -773,3 +773,12 @@ data class ChangePassword( val guid: String, ): RespectAppRoute +@Serializable +data class PlaylistShare( + val playlistUid: Long +) : RespectAppRoute { + companion object { + fun create(playlistUid: Long) = PlaylistShare(playlistUid) + } +} + diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt index 63580bf31..5fc102fce 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt @@ -35,6 +35,7 @@ import world.respect.shared.navigation.LearningUnitDetail import world.respect.shared.navigation.NavCommand import world.respect.shared.navigation.NavResult import world.respect.shared.navigation.NavResultReturner +import world.respect.shared.navigation.PlaylistShare import world.respect.shared.util.ext.asUiText import world.respect.shared.util.ext.resolve import world.respect.shared.viewmodel.RespectViewModel @@ -324,6 +325,33 @@ class LearningUnitDetailViewModel( } } + fun onClickSelectAll() { + val mapping = _uiState.value.mapping ?: return + val allLessons = mapping.sections.flatMap { it.items }.toSet() + _uiState.update { it.copy(selectedLessons = allLessons) } + } + + fun onClickSelectNone() { + _uiState.update { it.copy(selectedLessons = emptySet()) } + } + + fun onClickToggleSectionSelection(sectionUid: Long) { + val mapping = _uiState.value.mapping ?: return + val section = mapping.sections.find { it.uid == sectionUid } ?: return + val sectionLessons = section.items + + val allSelected = sectionLessons.all { _uiState.value.selectedLessons.contains(it) } + + _uiState.update { prev -> + val newSelection = if (allSelected) { + prev.selectedLessons - sectionLessons.toSet() + } else { + prev.selectedLessons + sectionLessons.toSet() + } + prev.copy(selectedLessons = newSelection) + } + } + fun onConfirmSelection() { val selectedLessons = _uiState.value.selectedLessons.toList() @@ -377,7 +405,12 @@ class LearningUnitDetailViewModel( } fun onClickShare() { - // TODO: Implement share functionality + val mapping = _uiState.value.mapping ?: return + _navCommandFlow.tryEmit( + NavCommand.Navigate( + PlaylistShare.create(playlistUid = mapping.uid) + ) + ) } fun onClickCopy() { @@ -421,7 +454,7 @@ class LearningUnitDetailViewModel( } fun onClickDelete() { - // TODO: Implement delete functionality + // TODO: } fun sectionLinkUiStateFor( diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/share/PlaylistShareViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/share/PlaylistShareViewModel.kt new file mode 100644 index 000000000..e7a6203a7 --- /dev/null +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/share/PlaylistShareViewModel.kt @@ -0,0 +1,102 @@ +package world.respect.shared.viewmodel.playlists.mapping.share + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute +import io.ktor.http.Url +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.koin.core.component.KoinScopeComponent +import org.koin.core.scope.Scope +import world.respect.libutil.ext.appendEndpointSegments +import world.respect.shared.domain.account.RespectAccountManager +import world.respect.shared.navigation.PlaylistShare +import world.respect.shared.util.ext.asUiText +import world.respect.shared.viewmodel.RespectViewModel +import world.respect.shared.generated.resources.Res +import world.respect.shared.generated.resources.share +import world.respect.shared.generated.resources.anyone_with_the_link +import world.respect.shared.generated.resources.teacher_admin_in_my_school + +data class PlaylistShareUiState( + val playlistUid: Long = 0L, + val shareUrl: Url? = null, + val viewPermission: String = "", + val editPermission: String = "", +) + +class PlaylistShareViewModel( + savedStateHandle: SavedStateHandle, + private val accountManager: RespectAccountManager, +) : RespectViewModel(savedStateHandle), KoinScopeComponent { + + override val scope: Scope = accountManager.requireActiveAccountScope() + + private val _uiState = MutableStateFlow(PlaylistShareUiState()) + + val uiState = _uiState.asStateFlow() + + private val route: PlaylistShare = savedStateHandle.toRoute() + + init { + _appUiState.update { + it.copy( + title = Res.string.share.asUiText(), + hideBottomNavigation = true, + ) + } + + val playlistUid = route.playlistUid + + _uiState.update { + it.copy( + playlistUid = playlistUid, + viewPermission = "", + editPermission = "" + ) + } + + viewModelScope.launch { + accountManager.selectedAccountAndPersonFlow.collect { sessionAndPerson -> + val schoolUrl = sessionAndPerson?.session?.account?.school?.self + + if (schoolUrl != null) { + val shareUrl = buildShareUrl(schoolUrl, playlistUid) + _uiState.update { prev -> + prev.copy(shareUrl = shareUrl) + } + } + } + } + } + + private fun buildShareUrl(schoolUrl: Url, playlistUid: Long): Url { + return schoolUrl.appendEndpointSegments("playlist", playlistUid.toString()) + } + + fun onClickShareLink() { + // TODO: Implement native share + } + + fun onClickCopyLink() { + // TODO: Implement copy to clipboard + } + + fun onClickSendViaSms() { + // TODO: Implement SMS share + } + + fun onClickSendViaEmail() { + // TODO: Implement email share + } + + fun onViewPermissionChanged(permission: String) { + _uiState.update { it.copy(viewPermission = permission) } + } + + fun onEditPermissionChanged(permission: String) { + _uiState.update { it.copy(editPermission = permission) } + } +} \ No newline at end of file From 8b8beebd62e46ca5ed355daefdd6fda4a4e096c0 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Fri, 9 Jan 2026 13:03:33 +0530 Subject: [PATCH 51/58] Rename the file names --- .../detail/LearningUnitDetailScreen.kt | 10 +++---- .../mapping/edit/PlaylistEditScreen.kt | 16 +++++----- .../mapping/list/PlaylistListScreen.kt | 12 ++++---- .../respect/shared/navigation/AppRoutes.kt | 30 +++++++++---------- .../apps/launcher/AppLauncherViewModel.kt | 8 ++--- .../detail/AssignmentDetailViewModel.kt | 12 ++------ .../edit/AssignmentEditViewModel.kt | 4 +-- .../list/AssignmentListViewModel.kt | 6 ++-- .../detail/LearningUnitDetailViewModel.kt | 22 +++++++------- .../playlists/mapping/PlaylistsAdapter.kt | 18 +++++------ .../mapping/edit/PlaylistEditViewModel.kt | 24 +++++++-------- .../mapping/list/PlaylistListViewModel.kt | 17 +++++------ .../{PlaylistsMapping.kt => Playlists.kt} | 4 +-- ...sMappingSection.kt => PlaylistsSection.kt} | 4 +-- ...SectionLink.kt => PlaylistsSectionLink.kt} | 2 +- .../mapping/share/PlaylistShareViewModel.kt | 11 ++++--- 16 files changed, 96 insertions(+), 104 deletions(-) rename respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/{PlaylistsMapping.kt => Playlists.kt} (79%) rename respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/{PlaylistsMappingSection.kt => PlaylistsSection.kt} (66%) rename respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/{PlaylistsMappingSectionLink.kt => PlaylistsSectionLink.kt} (90%) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt index 067b28717..f4cdbe2bf 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt @@ -38,7 +38,7 @@ import world.respect.datalayer.ext.dataOrNull import world.respect.shared.generated.resources.* import world.respect.shared.viewmodel.app.appstate.getTitle import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistSectionUiState -import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMappingSectionLink +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsSectionLink import world.respect.shared.viewmodel.learningunit.detail.LearningUnitDetailUiState import world.respect.shared.viewmodel.learningunit.detail.LearningUnitDetailViewModel @@ -257,7 +257,7 @@ private fun SingleLessonDetailScreen( @Composable private fun PlaylistDetailScreen( uiState: LearningUnitDetailUiState, - onClickLesson: (PlaylistsMappingSectionLink) -> Unit, + onClickLesson: (PlaylistsSectionLink) -> Unit, onClickEdit: () -> Unit, onClickAssign: () -> Unit, onClickAssignSection: (Long) -> Unit, @@ -546,9 +546,9 @@ private fun PlaylistDetailScreen( @Composable private fun LessonListItem( - link: PlaylistsMappingSectionLink, - sectionLinkUiState: (PlaylistsMappingSectionLink) -> Flow>, - onClickLesson: (PlaylistsMappingSectionLink) -> Unit, + link: PlaylistsSectionLink, + sectionLinkUiState: (PlaylistsSectionLink) -> Flow>, + onClickLesson: (PlaylistsSectionLink) -> Unit, isSelectionMode: Boolean = false, isSelected: Boolean = false ) { diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/edit/PlaylistEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/edit/PlaylistEditScreen.kt index c2d950428..b3230bbce 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/edit/PlaylistEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/edit/PlaylistEditScreen.kt @@ -45,8 +45,8 @@ import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistEditUiState import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistEditViewModel import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistSectionUiState -import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMappingSection -import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMappingSectionLink +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsSection +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsSectionLink import androidx.compose.ui.draw.alpha @@ -75,7 +75,7 @@ fun CurriculumMappingEditScreenForViewModel( @Composable fun CurriculumMappingEditScreen( uiState: PlaylistEditUiState = PlaylistEditUiState(), - sectionLinkUiState: (PlaylistsMappingSectionLink) -> Flow>, + sectionLinkUiState: (PlaylistsSectionLink) -> Flow>, onTitleChanged: (String) -> Unit = {}, onDescriptionChanged: (String) -> Unit = {}, onClickAddSection: () -> Unit = {}, @@ -85,7 +85,7 @@ fun CurriculumMappingEditScreen( onClickAddLesson: (Int) -> Unit = {}, onClickRemoveLesson: (Int, Int) -> Unit = { _, _ -> }, onLessonMovedBetweenSections: (Int, Int, Int, Int) -> Unit = { _, _, _, _ -> }, - onClickLesson: (PlaylistsMappingSectionLink) -> Unit = {}, + onClickLesson: (PlaylistsSectionLink) -> Unit = {}, ) { val haptic = LocalHapticFeedback.current val lazyListState = rememberLazyListState() @@ -315,7 +315,7 @@ fun CurriculumMappingEditScreen( @Composable private fun SectionItem( - section: PlaylistsMappingSection, + section: PlaylistsSection, sectionIndex: Int, isDragging: Boolean, onSectionTitleChanged: (Int, String) -> Unit, @@ -406,12 +406,12 @@ private fun SectionItem( @Composable private fun LessonItem( - link: PlaylistsMappingSectionLink, - sectionLinkUiState: (PlaylistsMappingSectionLink) -> Flow>, + link: PlaylistsSectionLink, + sectionLinkUiState: (PlaylistsSectionLink) -> Flow>, sectionIndex: Int, linkIndex: Int, onClickRemoveLesson: (Int, Int) -> Unit, - onClickLesson: (PlaylistsMappingSectionLink) -> Unit = {}, + onClickLesson: (PlaylistsSectionLink) -> Unit = {}, isDragging: Boolean, isParentSectionDragging: Boolean = false, dragModifier: Modifier = Modifier diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/list/PlaylistListScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/list/PlaylistListScreen.kt index 346dcf9a0..ccf2ff19e 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/list/PlaylistListScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/list/PlaylistListScreen.kt @@ -24,7 +24,7 @@ import world.respect.shared.generated.resources.* import world.respect.shared.viewmodel.playlists.mapping.list.PlaylistListUiState import world.respect.shared.viewmodel.playlists.mapping.list.PlaylistListViewModel -import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMapping +import world.respect.shared.viewmodel.playlists.mapping.model.Playlists @Composable fun PlaylistListScreen( @@ -46,10 +46,10 @@ fun PlaylistListScreen( fun PlaylistListScreenContent( uiState: PlaylistListUiState, onFilterSelected: (Int) -> Unit, - onClickMapping: (PlaylistsMapping) -> Unit, + onClickMapping: (Playlists) -> Unit, onClickAddNew: () -> Unit, onClickAddLink: () -> Unit, - onRemoveMapping: (PlaylistsMapping) -> Unit, + onRemoveMapping: (Playlists) -> Unit, ) { var isFabMenuExpanded by remember { mutableStateOf(false) } @@ -207,9 +207,9 @@ fun PlaylistListScreenContent( @Composable private fun MappingListItem( - mapping: PlaylistsMapping, - onClickMapping: (PlaylistsMapping) -> Unit, - onRemoveMapping: (PlaylistsMapping) -> Unit, + mapping: Playlists, + onClickMapping: (Playlists) -> Unit, + onRemoveMapping: (Playlists) -> Unit, ) { var menuExpanded by remember { mutableStateOf(false) } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index 094e26360..9b29c34b3 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -12,7 +12,7 @@ import world.respect.shared.domain.account.invite.RespectRedeemInviteRequest import world.respect.datalayer.school.model.EnrollmentRoleEnum import world.respect.datalayer.school.model.PersonRoleEnum import world.respect.datalayer.school.model.report.ReportFilter -import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMapping +import world.respect.shared.viewmodel.playlists.mapping.model.Playlists import world.respect.shared.viewmodel.learningunit.LearningUnitSelection import world.respect.shared.viewmodel.manageuser.profile.ProfileType @@ -111,10 +111,10 @@ data class AssignmentEdit( } @Transient - val availablePlaylists: List? = availablePlaylistsJson?.let { jsonStr -> + val availablePlaylists: List? = availablePlaylistsJson?.let { jsonStr -> try { Json.decodeFromString( - ListSerializer(PlaylistsMapping.serializer()), + ListSerializer(Playlists.serializer()), jsonStr ) } catch (e: Exception) { @@ -126,7 +126,7 @@ data class AssignmentEdit( fun create( uid: String?, learningUnitSelected: LearningUnitSelection? = null, - availablePlaylists: List? = null, + availablePlaylists: List? = null, ): AssignmentEdit { val learningUnits = learningUnitSelected?.let { listOf(it) } return AssignmentEdit( @@ -139,7 +139,7 @@ data class AssignmentEdit( }, availablePlaylistsJson = availablePlaylists?.let { Json.encodeToString( - ListSerializer(PlaylistsMapping.serializer()), + ListSerializer(Playlists.serializer()), it ) } @@ -149,7 +149,7 @@ data class AssignmentEdit( fun createWithMultipleLessons( uid: String?, learningUnits: List, - availablePlaylists: List? = null, + availablePlaylists: List? = null, ): AssignmentEdit { return AssignmentEdit( guid = uid, @@ -159,7 +159,7 @@ data class AssignmentEdit( ), availablePlaylistsJson = availablePlaylists?.let { Json.encodeToString( - ListSerializer(PlaylistsMapping.serializer()), + ListSerializer(Playlists.serializer()), it ) } @@ -578,10 +578,10 @@ class LearningUnitDetail ( val refererUrl: Url? get() = refererUrlStr?.let { Url(it) } - val mappingData: PlaylistsMapping? + val mappingData: Playlists? get() = mappingDataJson?.let { try { - Json.decodeFromString(PlaylistsMapping.serializer(), it) + Json.decodeFromString(Playlists.serializer(), it) } catch (e: Exception) { null } @@ -604,11 +604,11 @@ class LearningUnitDetail ( ) fun createFromMapping( - mapping: PlaylistsMapping, + mapping: Playlists, isSelectionMode: Boolean = false ): LearningUnitDetail { val mappingJson = try { - Json.encodeToString(PlaylistsMapping.serializer(), mapping) + Json.encodeToString(Playlists.serializer(), mapping) } catch (e: Exception) { null } @@ -738,9 +738,9 @@ data class CurriculumMappingEdit( ) : RespectAppRoute { @Transient - val mappingData: PlaylistsMapping? = mappingDataJson?.let { jsonString -> + val mappingData: Playlists? = mappingDataJson?.let { jsonString -> try { - Json.decodeFromString(PlaylistsMapping.serializer(), jsonString) + Json.decodeFromString(Playlists.serializer(), jsonString) } catch (e: Exception) { null } @@ -749,12 +749,12 @@ data class CurriculumMappingEdit( companion object { fun create( uid: Long, - mappingData: PlaylistsMapping? = null + mappingData: Playlists? = null ) = CurriculumMappingEdit( textbookUid = uid, mappingDataJson = mappingData?.let { mapping -> try { - Json.encodeToString(PlaylistsMapping.serializer(), mapping) + Json.encodeToString(Playlists.serializer(), mapping) } catch (e: Exception) { null } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt index 6a3bab1eb..edf55c102 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/apps/launcher/AppLauncherViewModel.kt @@ -49,7 +49,7 @@ import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.FabUiState import world.respect.shared.viewmodel.assignment.edit.AssignmentEditViewModel import world.respect.shared.viewmodel.playlists.mapping.list.PlaylistListViewModel -import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMapping +import world.respect.shared.viewmodel.playlists.mapping.model.Playlists data class AppLauncherUiState( val apps: IPagingSourceFactory = EmptyPagingSourceFactory(), @@ -134,7 +134,7 @@ class AppLauncherViewModel( viewModelScope.launch { playlistListViewModel.uiState.collect { state -> savedStateHandle[KEY_MAPPINGS_LIST] = json.encodeToString( - ListSerializer(PlaylistsMapping.serializer()), + ListSerializer(Playlists.serializer()), state.mappings ) } @@ -191,11 +191,11 @@ class AppLauncherViewModel( } } - private fun loadMappingsFromSavedState(savedStateHandle: SavedStateHandle): List { + private fun loadMappingsFromSavedState(savedStateHandle: SavedStateHandle): List { val mappingsJson = savedStateHandle.get(KEY_MAPPINGS_LIST) ?: return emptyList() return try { json.decodeFromString( - ListSerializer(PlaylistsMapping.serializer()), + ListSerializer(Playlists.serializer()), mappingsJson ) } catch (e: Exception) { diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/detail/AssignmentDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/detail/AssignmentDetailViewModel.kt index 7fbe2af63..11a256576 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/detail/AssignmentDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/detail/AssignmentDetailViewModel.kt @@ -11,7 +11,6 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -26,13 +25,8 @@ import world.respect.datalayer.DataLoadingState import world.respect.datalayer.RespectAppDataSource import world.respect.datalayer.SchoolDataSource import world.respect.datalayer.ext.dataOrNull -import world.respect.datalayer.school.PersonDataSource import world.respect.datalayer.school.model.Assignment import world.respect.datalayer.school.model.AssignmentLearningUnitRef -import world.respect.datalayer.school.model.EnrollmentRoleEnum -import world.respect.datalayer.school.model.Person -import world.respect.datalayer.shared.paging.IPagingSourceFactory -import world.respect.datalayer.shared.paging.PagingSourceFactoryHolder import world.respect.lib.opds.model.OpdsPublication import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.ext.whenSubscribed @@ -48,7 +42,7 @@ import world.respect.datalayer.school.model.Clazz import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.FabUiState import world.respect.shared.viewmodel.apps.launcher.AppLauncherViewModel -import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMapping +import world.respect.shared.viewmodel.playlists.mapping.model.Playlists data class AssignmentDetailUiState( val assignment: DataLoadState = DataLoadingState(), @@ -147,12 +141,12 @@ class AssignmentDetailViewModel( ) ) } - private fun getAvailablePlaylists(): List { + private fun getAvailablePlaylists(): List { val mappingsJson = savedStateHandle.get(AppLauncherViewModel.KEY_MAPPINGS_LIST) return if (mappingsJson != null) { try { json.decodeFromString( - ListSerializer(PlaylistsMapping.serializer()), + ListSerializer(Playlists.serializer()), mappingsJson ) } catch (e: Exception) { diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt index 2ffbbcc90..5716c746e 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt @@ -48,7 +48,7 @@ import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.ActionBarButtonUiState import world.respect.shared.viewmodel.assignment.toAssignmentLearningUnitRefs import world.respect.shared.viewmodel.learningunit.LearningUnitSelection -import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMapping +import world.respect.shared.viewmodel.playlists.mapping.model.Playlists import world.respect.shared.viewmodel.playlists.mapping.toOpdsGroup import kotlin.time.Clock @@ -140,7 +140,7 @@ class AssignmentEditViewModel( viewModelScope.launch { resultReturner.filteredResultFlowForKey(KEY_PLAYLIST_SELECTION).collect { result -> - val mapping = result.result as? PlaylistsMapping ?: return@collect + val mapping = result.result as? Playlists ?: return@collect val group = mapping.toOpdsGroup() val newLearningUnits = group.toAssignmentLearningUnitRefs() diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/list/AssignmentListViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/list/AssignmentListViewModel.kt index c1bddf519..7fd6e2ba9 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/list/AssignmentListViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/list/AssignmentListViewModel.kt @@ -37,7 +37,7 @@ import world.respect.datalayer.db.school.ext.isAdminOrTeacher import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.FabUiState import world.respect.shared.viewmodel.apps.launcher.AppLauncherViewModel -import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMapping +import world.respect.shared.viewmodel.playlists.mapping.model.Playlists data class AssignmentListUiState( val assignments: IPagingSourceFactory = EmptyPagingSourceFactory(), @@ -122,12 +122,12 @@ class AssignmentListViewModel( ) } - private fun getAvailablePlaylists(): List { + private fun getAvailablePlaylists(): List { val mappingsJson = savedStateHandle.get(AppLauncherViewModel.KEY_MAPPINGS_LIST) return if (mappingsJson != null) { try { json.decodeFromString( - ListSerializer(PlaylistsMapping.serializer()), + ListSerializer(Playlists.serializer()), mappingsJson ) } catch (e: Exception) { diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt index 5fc102fce..418deb462 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt @@ -44,8 +44,8 @@ import world.respect.shared.viewmodel.apps.launcher.AppLauncherViewModel import world.respect.shared.viewmodel.assignment.edit.AssignmentEditViewModel import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistEditViewModel import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistSectionUiState -import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMapping -import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMappingSectionLink +import world.respect.shared.viewmodel.playlists.mapping.model.Playlists +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsSectionLink import world.respect.shared.viewmodel.learningunit.LearningUnitSelection data class LearningUnitDetailUiState( @@ -54,14 +54,14 @@ data class LearningUnitDetailUiState( val pinState: PublicationPinState = PublicationPinState( PublicationPinState.Status.NOT_PINNED, 0, 0 ), - val mapping: PlaylistsMapping? = null, - val sectionLinkUiState: (PlaylistsMappingSectionLink) -> Flow> = { + val mapping: Playlists? = null, + val sectionLinkUiState: (PlaylistsSectionLink) -> Flow> = { emptyFlow() }, val showCopyDialog: Boolean = false, val copyDialogName: String = "", val isSelectionMode: Boolean = false, - val selectedLessons: Set = emptySet(), + val selectedLessons: Set = emptySet(), ) { val buttonsEnabled: Boolean get() = lessonDetail != null || mapping != null @@ -192,7 +192,7 @@ class LearningUnitDetailViewModel( } private suspend fun loadLessonPublications( - lessons: List + lessons: List ): List { return lessons.mapNotNull { lesson -> try { @@ -219,12 +219,12 @@ class LearningUnitDetailViewModel( } } - private fun getAvailablePlaylists(): List { + private fun getAvailablePlaylists(): List { val mappingsJson = savedStateHandle.get(AppLauncherViewModel.KEY_MAPPINGS_LIST) return if (mappingsJson != null) { try { json.decodeFromString( - ListSerializer(PlaylistsMapping.serializer()), + ListSerializer(Playlists.serializer()), mappingsJson ) } catch (e: Exception) { @@ -314,7 +314,7 @@ class LearningUnitDetailViewModel( } } - fun onLessonSelectionToggle(link: PlaylistsMappingSectionLink) { + fun onLessonSelectionToggle(link: PlaylistsSectionLink) { _uiState.update { prev -> val selected = if (prev.selectedLessons.contains(link)) { prev.selectedLessons - link @@ -372,7 +372,7 @@ class LearningUnitDetailViewModel( } } - fun onClickLesson(link: PlaylistsMappingSectionLink) { + fun onClickLesson(link: PlaylistsSectionLink) { if (_uiState.value.isSelectionMode) { onLessonSelectionToggle(link) } else { @@ -458,7 +458,7 @@ class LearningUnitDetailViewModel( } fun sectionLinkUiStateFor( - link: PlaylistsMappingSectionLink + link: PlaylistsSectionLink ): Flow> { val publicationUrl = Url(link.href) return appDataSource.opdsDataSource.loadOpdsPublication( diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/PlaylistsAdapter.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/PlaylistsAdapter.kt index 386ca208f..acf43205e 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/PlaylistsAdapter.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/PlaylistsAdapter.kt @@ -13,12 +13,12 @@ import world.respect.lib.opds.model.OpdsGroup import world.respect.lib.opds.model.OpdsPublication import world.respect.lib.opds.model.ReadiumLink import world.respect.lib.opds.model.ReadiumMetadata -import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMapping -import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMappingSection -import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMappingSectionLink +import world.respect.shared.viewmodel.playlists.mapping.model.Playlists +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsSection +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsSectionLink -fun PlaylistsMapping.toOpds(selfLink: String): OpdsFeed { +fun Playlists.toOpds(selfLink: String): OpdsFeed { return OpdsFeed( metadata = OpdsFeedMetadata( title = this.title, @@ -47,7 +47,7 @@ fun PlaylistsMapping.toOpds(selfLink: String): OpdsFeed { ) } -fun PlaylistsMapping.toOpdsGroup(): OpdsGroup { +fun Playlists.toOpdsGroup(): OpdsGroup { return OpdsGroup( metadata = OpdsFeedMetadata( title = this.title @@ -76,17 +76,17 @@ fun PlaylistsMapping.toOpdsGroup(): OpdsGroup { ) } -fun OpdsFeed.toCurriculumMapping(): PlaylistsMapping { - return PlaylistsMapping( +fun OpdsFeed.toCurriculumMapping(): Playlists { + return Playlists( uid = System.currentTimeMillis(), title = this.metadata.title, description = this.metadata.description ?: "", sections = this.groups?.map { group -> - PlaylistsMappingSection( + PlaylistsSection( uid = System.currentTimeMillis(), title = group.metadata.title, items = group.navigation?.map { navLink -> - PlaylistsMappingSectionLink( + PlaylistsSectionLink( uid = System.currentTimeMillis(), href = navLink.href, title = navLink.title ?: "" diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/edit/PlaylistEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/edit/PlaylistEditViewModel.kt index 42af52670..35ad073f1 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/edit/PlaylistEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/edit/PlaylistEditViewModel.kt @@ -41,21 +41,21 @@ import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.ActionBarButtonUiState import world.respect.shared.viewmodel.assignment.edit.AssignmentEditViewModel.Companion.KEY_LEARNING_UNIT -import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMapping -import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMappingSection -import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMappingSectionLink +import world.respect.shared.viewmodel.playlists.mapping.model.Playlists +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsSection +import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsSectionLink import world.respect.shared.viewmodel.learningunit.LearningUnitSelection import world.respect.shared.navigation.RouteResultDest import world.respect.shared.viewmodel.app.appstate.getTitle data class PlaylistEditUiState( - val mapping: PlaylistsMapping? = null, + val mapping: Playlists? = null, val loading: Boolean = false, val isNew: Boolean = true, val titleError: UiText? = null, val error: UiText? = null, val pendingLessonSectionIndex: Int? = null, - val sectionUiState: (PlaylistsMappingSection) -> Flow = { emptyFlow() }, + val sectionUiState: (PlaylistsSection) -> Flow = { emptyFlow() }, ) { val fieldsEnabled: Boolean get() = !loading @@ -66,7 +66,7 @@ data class PlaylistEditUiState( val description: String get() = mapping?.description ?: "" - val sections: List + val sections: List get() = mapping?.sections ?: emptyList() } @@ -93,7 +93,7 @@ class PlaylistEditViewModel( private val _uiState = MutableStateFlow( PlaylistEditUiState( - mapping = mappingData ?: PlaylistsMapping(uid = mappingUid), + mapping = mappingData ?: Playlists(uid = mappingUid), isNew = mappingUid == 0L ) ) @@ -130,7 +130,7 @@ class PlaylistEditViewModel( mapping = prev.mapping?.copy( sections = prev.mapping.sections.updateAtIndex(pendingSectionIndex) { it.copy( - items = it.items + PlaylistsMappingSectionLink( + items = it.items + PlaylistsSectionLink( href = selectedLearningUnit.learningUnitManifestUrl.toString(), title = selectedLearningUnit.selectedPublication.metadata.title.getTitle(), appManifestUrl = selectedLearningUnit.appManifestUrl @@ -149,7 +149,7 @@ class PlaylistEditViewModel( val mappingToCommit = _uiState.updateAndGet(block).mapping ?: return savedStateHandle[KEY_MAPPING] = json.encodeToString( - PlaylistsMapping.serializer(), mappingToCommit + Playlists.serializer(), mappingToCommit ) } @@ -175,7 +175,7 @@ class PlaylistEditViewModel( updateUiStateAndCommit { prev -> prev.copy( mapping = prev.mapping?.copy( - sections = prev.mapping.sections + PlaylistsMappingSection(title = "") + sections = prev.mapping.sections + PlaylistsSection(title = "") ) ) } @@ -295,7 +295,7 @@ class PlaylistEditViewModel( } } - fun onClickLesson(link: PlaylistsMappingSectionLink) { + fun onClickLesson(link: PlaylistsSectionLink) { val publicationUrl = Url(link.href) val appManifestUrl = link.appManifestUrl ?: return @@ -312,7 +312,7 @@ class PlaylistEditViewModel( } fun sectionLinkUiStateFor( - link: PlaylistsMappingSectionLink + link: PlaylistsSectionLink ): Flow> { val publicationUrl = Url(link.href) return respectAppDataSource.opdsDataSource.loadOpdsPublication( diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/list/PlaylistListViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/list/PlaylistListViewModel.kt index 7d4076460..129752be1 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/list/PlaylistListViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/list/PlaylistListViewModel.kt @@ -17,22 +17,21 @@ import world.respect.shared.navigation.NavCommand import world.respect.shared.navigation.NavResultReturner import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel -import world.respect.shared.viewmodel.assignment.edit.AssignmentEditViewModel import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistEditViewModel -import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsMapping +import world.respect.shared.viewmodel.playlists.mapping.model.Playlists import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.error_unexpected_result_type import world.respect.shared.resources.UiText data class PlaylistListUiState( - val mappings: List = emptyList(), + val mappings: List = emptyList(), val selectedFilterIndex: Int = 0, val error: UiText? = null, val currentUserGuid: String? = null, val currentSchoolUrl: Url? = null, val isSelectionMode: Boolean = false, ) { - val filteredMappings: List + val filteredMappings: List get() = when (selectedFilterIndex) { 0 -> mappings 1 -> mappings.filter { mapping -> @@ -75,7 +74,7 @@ class PlaylistListViewModel( resultReturner.filteredResultFlowForKey( PlaylistEditViewModel.KEY_SAVED_MAPPING ).collect { result -> - val savedMapping = result.result as? PlaylistsMapping + val savedMapping = result.result as? Playlists if (savedMapping == null) { _uiState.update { it.copy(error = Res.string.error_unexpected_result_type.asUiText()) @@ -87,7 +86,7 @@ class PlaylistListViewModel( } } - fun setMappings(mappings: List) { + fun setMappings(mappings: List) { _uiState.update { it.copy(mappings = mappings) } } @@ -99,7 +98,7 @@ class PlaylistListViewModel( _uiState.update { it.copy(selectedFilterIndex = index) } } - fun onClickMapping(mapping: PlaylistsMapping) { + fun onClickMapping(mapping: Playlists) { _navCommandFlow.tryEmit( NavCommand.Navigate( LearningUnitDetail.createFromMapping( @@ -126,12 +125,12 @@ class PlaylistListViewModel( ) } - fun removeMapping(mapping: PlaylistsMapping) { + fun removeMapping(mapping: Playlists) { val updated = _uiState.value.mappings.filter { it.uid != mapping.uid } _uiState.update { it.copy(mappings = updated) } } - private fun addOrUpdateMapping(mapping: PlaylistsMapping) { + private fun addOrUpdateMapping(mapping: Playlists) { val currentMappings = _uiState.value.mappings.toMutableList() val existingIndex = currentMappings.indexOfFirst { it.uid == mapping.uid } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/PlaylistsMapping.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/Playlists.kt similarity index 79% rename from respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/PlaylistsMapping.kt rename to respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/Playlists.kt index 53b7c4ca8..ae9eca2dc 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/PlaylistsMapping.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/Playlists.kt @@ -4,11 +4,11 @@ import io.ktor.http.Url import kotlinx.serialization.Serializable @Serializable -data class PlaylistsMapping( +data class Playlists( val uid: Long = System.currentTimeMillis(), val title: String = "", val description: String = "", - val sections: List = emptyList(), + val sections: List = emptyList(), val createdBy: String? = null, val isSchoolWide: Boolean = false, val schoolUrl: Url? = null, diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/PlaylistsMappingSection.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/PlaylistsSection.kt similarity index 66% rename from respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/PlaylistsMappingSection.kt rename to respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/PlaylistsSection.kt index fbd163f84..bd0724a3c 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/PlaylistsMappingSection.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/PlaylistsSection.kt @@ -3,8 +3,8 @@ package world.respect.shared.viewmodel.playlists.mapping.model import kotlinx.serialization.Serializable @Serializable -data class PlaylistsMappingSection( +data class PlaylistsSection( val uid: Long = System.currentTimeMillis(), val title: String, - val items: List = emptyList() + val items: List = emptyList() ) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/PlaylistsMappingSectionLink.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/PlaylistsSectionLink.kt similarity index 90% rename from respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/PlaylistsMappingSectionLink.kt rename to respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/PlaylistsSectionLink.kt index dc4abe804..7b1c3c925 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/PlaylistsMappingSectionLink.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/PlaylistsSectionLink.kt @@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable * @property href Absolute URL to the OPDS publication linked (NOT the Learning Unit ID URL). */ @Serializable -data class PlaylistsMappingSectionLink( +data class PlaylistsSectionLink( val uid: Long = System.currentTimeMillis(), val href: String, val title: String? = "", diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/share/PlaylistShareViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/share/PlaylistShareViewModel.kt index e7a6203a7..8f797c235 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/share/PlaylistShareViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/share/PlaylistShareViewModel.kt @@ -17,8 +17,7 @@ import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.generated.resources.Res import world.respect.shared.generated.resources.share -import world.respect.shared.generated.resources.anyone_with_the_link -import world.respect.shared.generated.resources.teacher_admin_in_my_school + data class PlaylistShareUiState( val playlistUid: Long = 0L, @@ -77,19 +76,19 @@ class PlaylistShareViewModel( } fun onClickShareLink() { - // TODO: Implement native share + // TODO: } fun onClickCopyLink() { - // TODO: Implement copy to clipboard + // TODO: } fun onClickSendViaSms() { - // TODO: Implement SMS share + // TODO: } fun onClickSendViaEmail() { - // TODO: Implement email share + // TODO: } fun onViewPermissionChanged(permission: String) { From d62876a9bde9853686573b15c7ab16b520ee9329 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Mon, 12 Jan 2026 11:42:17 +0530 Subject: [PATCH 52/58] tidy up assignmenteditviewmodel --- .../assignment/AssignmentOpdsAdapters.kt | 39 ------------------- .../edit/AssignmentEditViewModel.kt | 16 ++++---- 2 files changed, 7 insertions(+), 48 deletions(-) delete mode 100644 respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/AssignmentOpdsAdapters.kt diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/AssignmentOpdsAdapters.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/AssignmentOpdsAdapters.kt deleted file mode 100644 index 50457064c..000000000 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/AssignmentOpdsAdapters.kt +++ /dev/null @@ -1,39 +0,0 @@ -package world.respect.shared.viewmodel.assignment - -import io.ktor.http.Url -import world.respect.datalayer.school.model.AssignmentLearningUnitRef -import world.respect.lib.opds.model.OpdsGroup - -/** - * Convert OpdsGroup publications to AssignmentLearningUnitRef list. - * Extracts learning unit manifest URLs and their corresponding app manifest URLs - * from OPDS publications. - */ -fun OpdsGroup.toAssignmentLearningUnitRefs(): List { - val publications = this.publications ?: emptyList() - - return publications.mapNotNull { publication -> - try { - val acquisitionLink = publication.links.firstOrNull { link -> - link.rel?.any { it.startsWith("http://opds-spec.org/acquisition") } == true - } ?: return@mapNotNull null - - val publicationUrl = Url(acquisitionLink.href) - - val appManifestLink = publication.links.firstOrNull { link -> - link.rel?.contains("http://opds-spec.org/compatible-app") == true - } - - val appManifestUrl = appManifestLink?.let { Url(it.href) } - ?: return@mapNotNull null - - AssignmentLearningUnitRef( - learningUnitManifestUrl = publicationUrl, - appManifestUrl = appManifestUrl - ) - } catch (e: Exception) { - e.printStackTrace() - null - } - } -} \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt index 5716c746e..d27cf7424 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/assignment/edit/AssignmentEditViewModel.kt @@ -46,10 +46,7 @@ import world.respect.shared.util.LaunchDebouncer import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.ActionBarButtonUiState -import world.respect.shared.viewmodel.assignment.toAssignmentLearningUnitRefs import world.respect.shared.viewmodel.learningunit.LearningUnitSelection -import world.respect.shared.viewmodel.playlists.mapping.model.Playlists -import world.respect.shared.viewmodel.playlists.mapping.toOpdsGroup import kotlin.time.Clock data class AssignmentEditUiState( @@ -118,7 +115,6 @@ class AssignmentEditViewModel( hideBottomNavigation = true, ) } - viewModelScope.launch { resultReturner.filteredResultFlowForKey(KEY_LEARNING_UNIT).collect { result -> val learningUnit = result.result as? LearningUnitSelection ?: return@collect @@ -137,15 +133,16 @@ class AssignmentEditViewModel( } } } - viewModelScope.launch { resultReturner.filteredResultFlowForKey(KEY_PLAYLIST_SELECTION).collect { result -> - val mapping = result.result as? Playlists ?: return@collect - val group = mapping.toOpdsGroup() - val newLearningUnits = group.toAssignmentLearningUnitRefs() + val learningUnitSelections = result.result as? List<*> ?: return@collect + val newLearningUnits = learningUnitSelections.mapNotNull { item -> + (item as? LearningUnitSelection)?.toRef() + } - val assignment = _uiState.value.assignment.dataOrNull() ?: return@collect + if (newLearningUnits.isEmpty()) return@collect + val assignment = _uiState.value.assignment.dataOrNull() ?: return@collect val existingUrls = assignment.learningUnits.map { it.learningUnitManifestUrl }.toSet() @@ -153,6 +150,7 @@ class AssignmentEditViewModel( val uniqueNewUnits = newLearningUnits.filter { it.learningUnitManifestUrl !in existingUrls } + if (uniqueNewUnits.isEmpty()) return@collect _uiState.update { prev -> prev.copy( From b037c9ac330be15e076f31c13cafc6eeec39fbba Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Mon, 19 Jan 2026 14:25:39 +0530 Subject: [PATCH 53/58] tidy up playlisteditscreen --- .../commonMain/kotlin/world/respect/app/app/AppNavHost.kt | 4 ++-- .../app/view/playlists/mapping/edit/PlaylistEditScreen.kt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt index 513bf492b..929e93dc2 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt @@ -134,7 +134,7 @@ import world.respect.shared.viewmodel.report.list.ReportListViewModel import world.respect.shared.viewmodel.report.list.ReportTemplateListViewModel import world.respect.shared.viewmodel.manageuser.signup.CreateAccountViewModel import world.respect.app.view.settings.SettingsScreenForViewModel -import world.respect.app.view.playlists.mapping.edit.CurriculumMappingEditScreenForViewModel +import world.respect.app.view.playlists.mapping.edit.PlaylistEditScreenForViewModel import world.respect.app.view.playlists.mapping.share.PlaylistShareScreen import world.respect.shared.viewmodel.settings.SettingsViewModel @@ -546,7 +546,7 @@ fun AppNavHost( onSetAppUiState = onSetAppUiState, navController = respectNavController ) - CurriculumMappingEditScreenForViewModel( + PlaylistEditScreenForViewModel( viewModel = viewModel ) } diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/edit/PlaylistEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/edit/PlaylistEditScreen.kt index b3230bbce..61ffed532 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/edit/PlaylistEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/edit/PlaylistEditScreen.kt @@ -51,12 +51,12 @@ import androidx.compose.ui.draw.alpha @Composable -fun CurriculumMappingEditScreenForViewModel( +fun PlaylistEditScreenForViewModel( viewModel: PlaylistEditViewModel ) { val uiState by viewModel.uiState.collectAsState() - CurriculumMappingEditScreen( + PlaylistEditScreen( uiState = uiState, sectionLinkUiState = viewModel::sectionLinkUiStateFor, onTitleChanged = viewModel::onTitleChanged, @@ -73,7 +73,7 @@ fun CurriculumMappingEditScreenForViewModel( } @Composable -fun CurriculumMappingEditScreen( +fun PlaylistEditScreen( uiState: PlaylistEditUiState = PlaylistEditUiState(), sectionLinkUiState: (PlaylistsSectionLink) -> Flow>, onTitleChanged: (String) -> Unit = {}, From ccc06f7013e8ed89bf5cffa4f385db147b7d21ad Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Tue, 20 Jan 2026 09:56:48 +0530 Subject: [PATCH 54/58] modify the copy playlist --- .../detail/LearningUnitDetailScreen.kt | 54 ++++--- .../mapping/edit/PlaylistEditScreen.kt | 135 ++++++++++++++---- .../composeResources/values/strings.xml | 1 + .../detail/LearningUnitDetailViewModel.kt | 9 +- .../mapping/edit/PlaylistEditViewModel.kt | 116 ++++++++++++++- .../playlists/mapping/model/Playlists.kt | 4 + 6 files changed, 270 insertions(+), 49 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt index f4cdbe2bf..f5226a67e 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt @@ -122,17 +122,13 @@ private fun CopyPlaylistDialog( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - TextButton( - onClick = onDismiss - ) { + TextButton(onClick = onDismiss) { Text( text = stringResource(Res.string.cancel), color = MaterialTheme.colorScheme.primary ) } - TextButton( - onClick = onConfirm - ) { + TextButton(onClick = onConfirm) { Text( text = stringResource(Res.string.copy_playlist), color = MaterialTheme.colorScheme.primary @@ -199,9 +195,7 @@ private fun SingleLessonDetailScreen( Spacer(modifier = Modifier.width(12.dp)) - Text( - text = stringResource(Res.string.app_name), - ) + Text(text = stringResource(Res.string.app_name)) } Text( @@ -229,15 +223,13 @@ private fun SingleLessonDetailScreen( verticalAlignment = Alignment.CenterVertically ) { RespectQuickActionButton( - labelText = when(uiState.pinState.status) { + labelText = when (uiState.pinState.status) { PublicationPinState.Status.IN_PROGRESS -> stringResource(Res.string.cancel) PublicationPinState.Status.READY -> stringResource(Res.string.downloaded) else -> stringResource(Res.string.download) }, iconContent = { - RespectOfflineItemStatusIcon( - state = uiState.pinState, - ) + RespectOfflineItemStatusIcon(state = uiState.pinState) }, onClick = onClickDownload, enabled = uiState.buttonsEnabled, @@ -322,6 +314,38 @@ private fun PlaylistDetailScreen( ) } + if (mapping?.subject != null || mapping?.grade != null || mapping?.language != null) { + Spacer(modifier = Modifier.height(8.dp)) + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + mapping.subject?.let { subject -> + Text( + text = subject.name.getTitle(), + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + mapping.grade?.let { grade -> + Text( + text = grade, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + mapping.language?.let { language -> + Text( + text = language, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + if (uiState.isSelectionMode && uiState.selectedLessons.isNotEmpty()) { Spacer(modifier = Modifier.height(8.dp)) Text( @@ -429,9 +453,7 @@ private fun PlaylistDetailScreen( if (uiState.isSelectionMode) { Checkbox( checked = allSectionLessonsSelected, - onCheckedChange = { checked -> - onClickToggleSectionSelection(section.uid) - }, + onCheckedChange = { onClickToggleSectionSelection(section.uid) }, modifier = Modifier.size(24.dp) ) Spacer(modifier = Modifier.width(12.dp)) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/edit/PlaylistEditScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/edit/PlaylistEditScreen.kt index 61ffed532..bf99e6f03 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/edit/PlaylistEditScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/playlists/mapping/edit/PlaylistEditScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.DragHandle import androidx.compose.material.icons.outlined.ContentPaste @@ -13,6 +14,7 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalHapticFeedback @@ -29,26 +31,15 @@ import world.respect.app.components.uiTextStringResource import world.respect.datalayer.DataLoadState import world.respect.datalayer.DataLoadingState import world.respect.datalayer.ext.dataOrNull -import world.respect.shared.generated.resources.Res -import world.respect.shared.generated.resources.description -import world.respect.shared.generated.resources.drag -import world.respect.shared.generated.resources.lesson -import world.respect.shared.generated.resources.no_sections_yet -import world.respect.shared.generated.resources.remove_chapter -import world.respect.shared.generated.resources.remove_lesson -import world.respect.shared.generated.resources.required -import world.respect.shared.generated.resources.section -import world.respect.shared.generated.resources.sections -import world.respect.shared.generated.resources.title -import world.respect.shared.generated.resources.section_name +import world.respect.lib.opds.model.ReadiumSubjectObject +import world.respect.shared.generated.resources.* import world.respect.shared.util.ext.asUiText +import world.respect.shared.viewmodel.app.appstate.getTitle import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistEditUiState import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistEditViewModel import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistSectionUiState import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsSection import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsSectionLink -import androidx.compose.ui.draw.alpha - @Composable fun PlaylistEditScreenForViewModel( @@ -61,6 +52,8 @@ fun PlaylistEditScreenForViewModel( sectionLinkUiState = viewModel::sectionLinkUiStateFor, onTitleChanged = viewModel::onTitleChanged, onDescriptionChanged = viewModel::onDescriptionChanged, + onSubjectSelected = viewModel::onSubjectSelected, + onGradeSelected = viewModel::onGradeSelected, onClickAddSection = viewModel::onClickAddSection, onClickRemoveSection = viewModel::onClickRemoveSection, onSectionTitleChanged = viewModel::onSectionTitleChanged, @@ -72,12 +65,15 @@ fun PlaylistEditScreenForViewModel( ) } +@OptIn(ExperimentalMaterial3Api::class) @Composable fun PlaylistEditScreen( uiState: PlaylistEditUiState = PlaylistEditUiState(), sectionLinkUiState: (PlaylistsSectionLink) -> Flow>, onTitleChanged: (String) -> Unit = {}, onDescriptionChanged: (String) -> Unit = {}, + onSubjectSelected: (ReadiumSubjectObject?) -> Unit = {}, + onGradeSelected: (String?) -> Unit = {}, onClickAddSection: () -> Unit = {}, onClickRemoveSection: (Int) -> Unit = {}, onSectionTitleChanged: (Int, String) -> Unit = { _, _ -> }, @@ -91,10 +87,13 @@ fun PlaylistEditScreen( val lazyListState = rememberLazyListState() var draggingSectionIndex by remember { mutableStateOf(null) } var isDraggingAnySection by remember { mutableStateOf(false) } + var subjectExpanded by remember { mutableStateOf(false) } + var gradeExpanded by remember { mutableStateOf(false) } + val reorderableLazyListState = rememberReorderableLazyListState( lazyListState = lazyListState, onMove = { from, to -> - val headerItemCount = 4 //TODO: This MUST be explained + val headerItemCount = 5 val fromIndex = from.index - headerItemCount val toIndex = to.index - headerItemCount @@ -159,7 +158,6 @@ fun PlaylistEditScreen( } ) - LazyColumn( state = lazyListState, modifier = Modifier.fillMaxWidth(), @@ -168,7 +166,7 @@ fun PlaylistEditScreen( OutlinedTextField( value = uiState.mapping?.title ?: "", onValueChange = onTitleChanged, - label = { Text(stringResource(Res.string.title)+ "*") }, + label = { Text(stringResource(Res.string.title) + "*") }, modifier = Modifier .fillMaxWidth() .defaultItemPadding() @@ -197,7 +195,93 @@ fun PlaylistEditScreen( ) } - item("mapping_title") { + item("subject_grade_language_row") { + Row( + modifier = Modifier + .fillMaxWidth() + .defaultItemPadding(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + ExposedDropdownMenuBox( + expanded = subjectExpanded, + onExpandedChange = { subjectExpanded = it }, + modifier = Modifier.weight(1f) + ) { + OutlinedTextField( + value = uiState.selectedSubject?.name?.getTitle() ?: "", + onValueChange = {}, + readOnly = true, + label = { Text(stringResource(Res.string.subject)) }, + trailingIcon = { + Icon( + Icons.Filled.ArrowDropDown, + contentDescription = null + ) + }, + modifier = Modifier + .fillMaxWidth() + .menuAnchor() + .testTag("subject_dropdown"), + singleLine = true + ) + + ExposedDropdownMenu( + expanded = subjectExpanded, + onDismissRequest = { subjectExpanded = false } + ) { + uiState.availableSubjects.forEach { subject -> + DropdownMenuItem( + text = { Text(subject.name.getTitle()) }, + onClick = { + onSubjectSelected(subject) + subjectExpanded = false + } + ) + } + } + } + + ExposedDropdownMenuBox( + expanded = gradeExpanded, + onExpandedChange = { gradeExpanded = it }, + modifier = Modifier.weight(1f) + ) { + OutlinedTextField( + value = uiState.selectedGrade ?: "", + onValueChange = {}, + readOnly = true, + label = { Text(stringResource(Res.string.grade)) }, + trailingIcon = { + Icon( + Icons.Filled.ArrowDropDown, + contentDescription = null + ) + }, + modifier = Modifier + .fillMaxWidth() + .menuAnchor() + .testTag("grade_dropdown"), + singleLine = true + ) + + ExposedDropdownMenu( + expanded = gradeExpanded, + onDismissRequest = { gradeExpanded = false } + ) { + uiState.availableGrades.forEach { grade -> + DropdownMenuItem( + text = { Text(grade) }, + onClick = { + onGradeSelected(grade) + gradeExpanded = false + } + ) + } + } + } + } + } + item("sections_title") { Text( modifier = Modifier.defaultItemPadding(), text = stringResource(Res.string.sections), @@ -312,7 +396,6 @@ fun PlaylistEditScreen( } } - @Composable private fun SectionItem( section: PlaylistsSection, @@ -346,8 +429,7 @@ private fun SectionItem( Icon( Icons.Filled.DragHandle, contentDescription = stringResource(Res.string.drag), - modifier = dragModifier - .size(24.dp), + modifier = dragModifier.size(24.dp), tint = if (isDragging) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant ) @@ -387,8 +469,7 @@ private fun SectionItem( ) { OutlinedButton( onClick = { onClickAddLesson(sectionIndex) }, - modifier = Modifier.fillMaxWidth() - .testTag("add_item"), + modifier = Modifier.fillMaxWidth().testTag("add_item"), enabled = !isDragging ) { Icon( @@ -416,7 +497,6 @@ private fun LessonItem( isParentSectionDragging: Boolean = false, dragModifier: Modifier = Modifier ) { - val stateFlow = remember(link.href) { sectionLinkUiState(link) } @@ -453,7 +533,9 @@ private fun LessonItem( contentDescription = stringResource(Res.string.drag), modifier = dragModifier.size(20.dp), tint = if (isDragging) MaterialTheme.colorScheme.primary - else if (isParentSectionDragging) MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + else if (isParentSectionDragging) MaterialTheme.colorScheme.onSurfaceVariant.copy( + alpha = 0.5f + ) else MaterialTheme.colorScheme.onSurfaceVariant ) @@ -464,8 +546,7 @@ private fun LessonItem( uri = iconUrl.toString(), contentDescription = "", contentScale = ContentScale.Crop, - modifier = Modifier - .size(36.dp) + modifier = Modifier.size(36.dp) ) } diff --git a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml index 020686c53..42f6d982f 100644 --- a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml +++ b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml @@ -496,4 +496,5 @@ Copy link Send link via SMS Send link via email + Grade diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt index 418deb462..8b3c69cdb 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt @@ -414,7 +414,14 @@ class LearningUnitDetailViewModel( } fun onClickCopy() { - _uiState.update { it.copy(showCopyDialog = true) } + val mapping = _uiState.value.mapping ?: return + val defaultName = "Copy of ${mapping.title}" + _uiState.update { + it.copy( + showCopyDialog = true, + copyDialogName = defaultName + ) + } } fun onCopyDialogDismiss() { diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/edit/PlaylistEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/edit/PlaylistEditViewModel.kt index 35ad073f1..cc843c98c 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/edit/PlaylistEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/edit/PlaylistEditViewModel.kt @@ -15,12 +15,17 @@ import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch import kotlinx.serialization.json.Json import org.koin.core.component.KoinScopeComponent +import org.koin.core.component.inject import org.koin.core.scope.Scope import world.respect.datalayer.DataLoadParams import world.respect.datalayer.DataLoadState +import world.respect.datalayer.DataReadyState import world.respect.datalayer.RespectAppDataSource +import world.respect.datalayer.SchoolDataSource +import world.respect.datalayer.school.SchoolAppDataSource import world.respect.datalayer.ext.map import world.respect.lib.opds.model.findIcons +import world.respect.lib.opds.model.ReadiumSubjectObject import world.respect.libutil.ext.moveItem import world.respect.libutil.ext.updateAtIndex import world.respect.libutil.ext.resolve @@ -36,17 +41,17 @@ import world.respect.shared.navigation.NavCommand import world.respect.shared.navigation.NavResult import world.respect.shared.navigation.NavResultReturner import world.respect.shared.navigation.RespectAppLauncher +import world.respect.shared.navigation.RouteResultDest import world.respect.shared.resources.UiText import world.respect.shared.util.ext.asUiText import world.respect.shared.viewmodel.RespectViewModel import world.respect.shared.viewmodel.app.appstate.ActionBarButtonUiState +import world.respect.shared.viewmodel.app.appstate.getTitle import world.respect.shared.viewmodel.assignment.edit.AssignmentEditViewModel.Companion.KEY_LEARNING_UNIT import world.respect.shared.viewmodel.playlists.mapping.model.Playlists import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsSection import world.respect.shared.viewmodel.playlists.mapping.model.PlaylistsSectionLink import world.respect.shared.viewmodel.learningunit.LearningUnitSelection -import world.respect.shared.navigation.RouteResultDest -import world.respect.shared.viewmodel.app.appstate.getTitle data class PlaylistEditUiState( val mapping: Playlists? = null, @@ -55,6 +60,9 @@ data class PlaylistEditUiState( val titleError: UiText? = null, val error: UiText? = null, val pendingLessonSectionIndex: Int? = null, + val availableSubjects: List = emptyList(), + val availableGrades: List = emptyList(), + val availableLanguages: List = emptyList(), val sectionUiState: (PlaylistsSection) -> Flow = { emptyFlow() }, ) { val fieldsEnabled: Boolean @@ -68,6 +76,15 @@ data class PlaylistEditUiState( val sections: List get() = mapping?.sections ?: emptyList() + + val selectedSubject: ReadiumSubjectObject? + get() = mapping?.subject + + val selectedGrade: String? + get() = mapping?.grade + + val selectedLanguage: String? + get() = mapping?.language } data class PlaylistSectionUiState( @@ -86,6 +103,9 @@ class PlaylistEditViewModel( ) : RespectViewModel(savedStateHandle), KoinScopeComponent { override val scope: Scope = accountManager.requireActiveAccountScope() + + private val schoolDataSource: SchoolDataSource by inject() + private val route: CurriculumMappingEdit = savedStateHandle.toRoute() private val mappingUid = route.textbookUid @@ -117,6 +137,9 @@ class PlaylistEditViewModel( hideBottomNavigation = true ) } + viewModelScope.launch { + loadSubjectsAndGrades() + } viewModelScope.launch { resultReturner.filteredResultFlowForKey( @@ -145,6 +168,64 @@ class PlaylistEditViewModel( } } + private fun loadSubjectsAndGrades() { + viewModelScope.launch { + try { + val session = accountManager.selectedAccountAndPersonFlow.first() + val catalogUrl = session?.session?.account?.school?.respectExt ?: return@launch + + respectAppDataSource.opdsDataSource.loadOpdsFeed( + url = catalogUrl, + params = DataLoadParams() + ).collect { feedState -> + if (feedState is DataReadyState) { + val allSubjects = mutableListOf() + val allGrades = mutableSetOf() + val allLanguages = mutableSetOf() + + feedState.data.publications?.forEach { publication -> + publication.metadata.subject?.forEach { subject -> + if (subject is ReadiumSubjectObject && subject.code != null) { + allSubjects.add(subject) + } + } + publication.metadata.language?.forEach { language -> + allLanguages.add(language) + } + } + + feedState.data.groups?.forEach { group -> + group.publications?.forEach { publication -> + publication.metadata.subject?.forEach { subject -> + if (subject is ReadiumSubjectObject && subject.code != null) { + allSubjects.add(subject) + } + } + publication.metadata.language?.forEach { language -> + allLanguages.add(language) + } + } + } + + val uniqueSubjects = allSubjects + .distinctBy { subject -> subject.code } + .sortedBy { subject -> subject.name.getTitle() } + + _uiState.update { prev -> + prev.copy( + availableSubjects = uniqueSubjects, + availableGrades = allGrades.toList().sorted(), + availableLanguages = allLanguages.toList().sorted() + ) + } + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + private fun updateUiStateAndCommit(block: (PlaylistEditUiState) -> PlaylistEditUiState) { val mappingToCommit = _uiState.updateAndGet(block).mapping ?: return @@ -153,7 +234,6 @@ class PlaylistEditViewModel( ) } - fun onTitleChanged(title: String) { updateUiStateAndCommit { prev -> prev.copy( @@ -171,6 +251,30 @@ class PlaylistEditViewModel( } } + fun onSubjectSelected(subject: ReadiumSubjectObject?) { + updateUiStateAndCommit { prev -> + prev.copy( + mapping = prev.mapping?.copy(subject = subject) + ) + } + } + + fun onGradeSelected(grade: String?) { + updateUiStateAndCommit { prev -> + prev.copy( + mapping = prev.mapping?.copy(grade = grade) + ) + } + } + + fun onLanguageSelected(language: String?) { + updateUiStateAndCommit { prev -> + prev.copy( + mapping = prev.mapping?.copy(language = language) + ) + } + } + fun onClickAddSection() { updateUiStateAndCommit { prev -> prev.copy( @@ -287,7 +391,7 @@ class PlaylistEditViewModel( mapping = prev.mapping?.copy( sections = prev.mapping.sections.updateAtIndex(sectionIndex) { section -> section.copy( - items = section.items.filterIndexed { index, _ -> index != linkIndex } + items = section.items.filterIndexed { index, _ -> index != linkIndex } ) } ) @@ -330,6 +434,7 @@ class PlaylistEditViewModel( } } } + fun onClickSave() { val mapping = _uiState.value.mapping ?: return if (mapping.title.isBlank()) { @@ -344,7 +449,7 @@ class PlaylistEditViewModel( mapping.copy( uid = System.currentTimeMillis(), createdBy = currentSession?.person?.guid, - schoolUrl= currentSession?.session?.account?.school?.self + schoolUrl = currentSession?.session?.account?.school?.self ) } else { mapping @@ -365,6 +470,7 @@ class PlaylistEditViewModel( ) } } + fun onClearError() { _uiState.update { it.copy(titleError = null) } } diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/Playlists.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/Playlists.kt index ae9eca2dc..70067812b 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/Playlists.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/model/Playlists.kt @@ -2,12 +2,16 @@ package world.respect.shared.viewmodel.playlists.mapping.model import io.ktor.http.Url import kotlinx.serialization.Serializable +import world.respect.lib.opds.model.ReadiumSubjectObject @Serializable data class Playlists( val uid: Long = System.currentTimeMillis(), val title: String = "", val description: String = "", + val subject: ReadiumSubjectObject? = null, + val grade: String? = null, + val language: String? = null, val sections: List = emptyList(), val createdBy: String? = null, val isSchoolWide: Boolean = false, From fb1dafc5f1d40a53ed425b97d30a6c08e85f06bc Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Tue, 20 Jan 2026 18:58:21 +0530 Subject: [PATCH 55/58] change to playlist --- .../kotlin/world/respect/app/app/AppNavHost.kt | 4 ++-- .../world/respect/shared/navigation/AppRoutes.kt | 4 ++-- .../detail/LearningUnitDetailViewModel.kt | 4 ++-- .../playlists/mapping/edit/PlaylistEditViewModel.kt | 13 ++----------- .../playlists/mapping/list/PlaylistListViewModel.kt | 4 ++-- 5 files changed, 10 insertions(+), 19 deletions(-) diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt index 929e93dc2..f0e6af9ae 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt @@ -141,7 +141,7 @@ import world.respect.shared.viewmodel.settings.SettingsViewModel import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistEditViewModel import world.respect.shared.navigation.Settings -import world.respect.shared.navigation.CurriculumMappingEdit +import world.respect.shared.navigation.PlaylistEdit import world.respect.shared.navigation.PlaylistShare import world.respect.shared.viewmodel.onboarding.OnboardingViewModel import world.respect.shared.viewmodel.playlists.mapping.share.PlaylistShareViewModel @@ -541,7 +541,7 @@ fun AppNavHost( ) } - composable { + composable { val viewModel: PlaylistEditViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, navController = respectNavController diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index 9b29c34b3..bdeeb679c 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -732,7 +732,7 @@ data object Settings : RespectAppRoute @Serializable -data class CurriculumMappingEdit( +data class PlaylistEdit( val textbookUid: Long = 0L, private val mappingDataJson: String? = null ) : RespectAppRoute { @@ -750,7 +750,7 @@ data class CurriculumMappingEdit( fun create( uid: Long, mappingData: Playlists? = null - ) = CurriculumMappingEdit( + ) = PlaylistEdit( textbookUid = uid, mappingDataJson = mappingData?.let { mapping -> try { diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt index 8b3c69cdb..3abddb704 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/learningunit/detail/LearningUnitDetailViewModel.kt @@ -30,7 +30,7 @@ import world.respect.lib.opds.model.findIcons import world.respect.libutil.ext.resolve import world.respect.shared.domain.launchapp.LaunchAppUseCase import world.respect.shared.navigation.AssignmentEdit -import world.respect.shared.navigation.CurriculumMappingEdit +import world.respect.shared.navigation.PlaylistEdit import world.respect.shared.navigation.LearningUnitDetail import world.respect.shared.navigation.NavCommand import world.respect.shared.navigation.NavResult @@ -396,7 +396,7 @@ class LearningUnitDetailViewModel( val mapping = _uiState.value.mapping ?: return _navCommandFlow.tryEmit( NavCommand.Navigate( - CurriculumMappingEdit.create( + PlaylistEdit.create( uid = mapping.uid, mappingData = mapping ) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/edit/PlaylistEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/edit/PlaylistEditViewModel.kt index cc843c98c..86dfe7a3b 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/edit/PlaylistEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/edit/PlaylistEditViewModel.kt @@ -22,7 +22,6 @@ import world.respect.datalayer.DataLoadState import world.respect.datalayer.DataReadyState import world.respect.datalayer.RespectAppDataSource import world.respect.datalayer.SchoolDataSource -import world.respect.datalayer.school.SchoolAppDataSource import world.respect.datalayer.ext.map import world.respect.lib.opds.model.findIcons import world.respect.lib.opds.model.ReadiumSubjectObject @@ -35,7 +34,7 @@ import world.respect.shared.generated.resources.create_playlist import world.respect.shared.generated.resources.edit_playlist import world.respect.shared.generated.resources.required_field import world.respect.shared.generated.resources.save -import world.respect.shared.navigation.CurriculumMappingEdit +import world.respect.shared.navigation.PlaylistEdit import world.respect.shared.navigation.LearningUnitDetail import world.respect.shared.navigation.NavCommand import world.respect.shared.navigation.NavResult @@ -106,7 +105,7 @@ class PlaylistEditViewModel( private val schoolDataSource: SchoolDataSource by inject() - private val route: CurriculumMappingEdit = savedStateHandle.toRoute() + private val route: PlaylistEdit = savedStateHandle.toRoute() private val mappingUid = route.textbookUid private val mappingData = route.mappingData @@ -267,14 +266,6 @@ class PlaylistEditViewModel( } } - fun onLanguageSelected(language: String?) { - updateUiStateAndCommit { prev -> - prev.copy( - mapping = prev.mapping?.copy(language = language) - ) - } - } - fun onClickAddSection() { updateUiStateAndCommit { prev -> prev.copy( diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/list/PlaylistListViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/list/PlaylistListViewModel.kt index 129752be1..30504aec8 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/list/PlaylistListViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/list/PlaylistListViewModel.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.launch import org.koin.core.component.KoinScopeComponent import org.koin.core.scope.Scope import world.respect.shared.domain.account.RespectAccountManager -import world.respect.shared.navigation.CurriculumMappingEdit +import world.respect.shared.navigation.PlaylistEdit import world.respect.shared.navigation.EnterLink import world.respect.shared.navigation.LearningUnitDetail import world.respect.shared.navigation.NavCommand @@ -112,7 +112,7 @@ class PlaylistListViewModel( fun onClickAddNew() { _navCommandFlow.tryEmit( NavCommand.Navigate( - CurriculumMappingEdit.create(uid = 0L, mappingData = null) + PlaylistEdit.create(uid = 0L, mappingData = null) ) ) } From 591c4d8b5e6a42cb1d7bc50924f23597408ae815 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Tue, 20 Jan 2026 18:59:16 +0530 Subject: [PATCH 56/58] change to playlist --- .../viewmodel/playlists/mapping/edit/PlaylistEditViewModel.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/edit/PlaylistEditViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/edit/PlaylistEditViewModel.kt index 86dfe7a3b..e5ffce8f7 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/edit/PlaylistEditViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/playlists/mapping/edit/PlaylistEditViewModel.kt @@ -81,9 +81,6 @@ data class PlaylistEditUiState( val selectedGrade: String? get() = mapping?.grade - - val selectedLanguage: String? - get() = mapping?.language } data class PlaylistSectionUiState( From dea0ba026b3b3bad71f41409b310a75f96fbfc06 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Thu, 22 Jan 2026 10:42:29 +0530 Subject: [PATCH 57/58] Resolve merge conflict --- .../src/androidMain/AndroidManifest.xml | 8 +------- .../androidMain/kotlin/world/respect/AppKoinModule.kt | 9 +-------- .../kotlin/world/respect/app/app/AppNavHost.kt | 11 ----------- .../commonMain/composeResources/values/strings.xml | 2 -- .../world/respect/shared/navigation/AppRoutes.kt | 1 - 5 files changed, 2 insertions(+), 29 deletions(-) diff --git a/respect-app-compose/src/androidMain/AndroidManifest.xml b/respect-app-compose/src/androidMain/AndroidManifest.xml index 6a61a1b85..8ff9a1579 100644 --- a/respect-app-compose/src/androidMain/AndroidManifest.xml +++ b/respect-app-compose/src/androidMain/AndroidManifest.xml @@ -2,15 +2,10 @@ - - android:minSdkVersion="23" android:targetSdkVersion="34" - tools:overrideLibrary=" - org.ncgroup.kscan - " /> + tools:overrideLibrary="org.qrcodeScanner, network.chaintech.cmpimagepickncrop, org.ncgroup.kscan" /> @@ -31,7 +26,6 @@ android:usesCleartextTraffic="true" tools:replace="android:allowBackup,android:name"> - Date Time Tasks - Assignment tasks Assignment name Lesson/assessment @@ -552,7 +551,6 @@ Send link via SMS Send link via email Grade - Section name Not set diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index 0a9bf1af7..cbe621801 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -14,7 +14,6 @@ import world.respect.datalayer.school.model.PersonRoleEnum import world.respect.datalayer.school.model.report.ReportFilter import world.respect.shared.viewmodel.playlists.mapping.model.Playlists import world.respect.shared.ext.NextAfterScan -import world.respect.shared.viewmodel.curriculum.mapping.model.CurriculumMapping import world.respect.shared.viewmodel.learningunit.LearningUnitSelection import world.respect.shared.viewmodel.manageuser.profile.ProfileType From 86f13a7e9952ae120201b5bf5c88a726d3aad361 Mon Sep 17 00:00:00 2001 From: lipsa-b Date: Thu, 19 Feb 2026 11:55:49 +0530 Subject: [PATCH 58/58] resolve merge conflicts --- .../src/androidMain/AndroidManifest.xml | 4 +- .../world/respect/AbstractAppActivity.kt | 2 +- .../kotlin/world/respect/AppKoinModule.kt | 197 +++++++++++---- .../kotlin/world/respect/app/app/App.kt | 42 +++- .../world/respect/app/app/AppNavHost.kt | 117 ++++++--- .../detail/LearningUnitDetailScreen.kt | 2 +- .../composeResources/values/strings.xml | 59 ++++- .../respect/shared/navigation/AppRoutes.kt | 227 +++++++++++++----- .../acceptinvite/AcceptInviteViewModel.kt | 2 +- .../manageuser/profile/SignupViewModel.kt | 3 +- 10 files changed, 488 insertions(+), 167 deletions(-) diff --git a/respect-app-compose/src/androidMain/AndroidManifest.xml b/respect-app-compose/src/androidMain/AndroidManifest.xml index a2e08f198..07b64614f 100644 --- a/respect-app-compose/src/androidMain/AndroidManifest.xml +++ b/respect-app-compose/src/androidMain/AndroidManifest.xml @@ -1,9 +1,8 @@ - @@ -26,6 +25,7 @@ android:usesCleartextTraffic="true" tools:replace="android:allowBackup,android:name"> + { + LaunchSendSmsAndroid(androidContext()) + } + + single { + LaunchSendEmailAndroid(androidContext()) + } + + single { + LaunchShareLinkAndroid(androidContext()) + } + + single(createdAtStart = true) { + GetDeferredDeepLinkUseCaseAndroid( + context = androidContext(), + settings = get() + ) + } + single { XXHashUidNumberMapper(xxStringHasher = get()) } @@ -293,6 +332,7 @@ val appKoinModule = module { appContext = androidContext().applicationContext ) } + viewModelOf(::OnboardingViewModel) viewModelOf(::AppsDetailViewModel) viewModelOf(::AppLauncherViewModel) @@ -305,9 +345,9 @@ val appKoinModule = module { viewModelOf(::LearningUnitDetailViewModel) viewModelOf(::ReportViewModel) viewModelOf(::AcknowledgementViewModel) - viewModelOf(::JoinClazzWithCodeViewModel) + viewModelOf(::EnterInviteCodeViewModel) viewModelOf(::LoginViewModel) - viewModelOf(::ConfirmationViewModel) + viewModelOf(::AcceptInviteViewModel) viewModelOf(::SignupViewModel) viewModelOf(::TermsAndConditionViewModel) viewModelOf(::WaitingForApprovalViewModel) @@ -321,6 +361,9 @@ val appKoinModule = module { viewModelOf(::AccountListViewModel) viewModelOf(::ManageAccountViewModel) viewModelOf(::PersonListViewModel) + viewModelOf(::InvitePersonViewModel) + viewModelOf(::CopyInviteCodeViewModel) + viewModelOf(::InviteQrViewModel) viewModelOf(::PersonEditViewModel) viewModelOf(::PersonDetailViewModel) viewModelOf(::ReportDetailViewModel) @@ -332,22 +375,20 @@ val appKoinModule = module { viewModelOf(::IndicatorListViewModel) viewModelOf(::IndicatorDetailViewModel) viewModelOf(::SettingsViewModel) - viewModelOf(::PlaylistEditViewModel) viewModelOf(::ScanQRCodeViewModel) + viewModelOf(::PlaylistEditViewModel) + viewModelOf(::PlaylistListViewModel) + viewModelOf(::PlaylistShareViewModel) viewModelOf(::CreateAccountSetUserNameViewModel) + viewModelOf(::CreateAccountSetPasswordViewModel) viewModelOf(::ChangePasswordViewModel) viewModelOf(::SchoolDirectoryListViewModel) viewModelOf(::SchoolDirectoryEditViewModel) viewModelOf(::AssignmentListViewModel) viewModelOf(::AssignmentEditViewModel) viewModelOf(::AssignmentDetailViewModel) - viewModelOf(::AssignmentDetailViewModel) viewModelOf(::EnrollmentListViewModel) viewModelOf(::EnrollmentEditViewModel) - viewModelOf(::PlaylistListViewModel) - viewModelOf(::PlaylistShareViewModel) - viewModelOf(::CreateAccountSetPasswordViewModel) - single { GetOfflineStorageOptionsUseCaseAndroid( @@ -410,6 +451,7 @@ val appKoinModule = module { okHttpClient = get() ) } + single(named(TAG_TMP_DIR)) { File(androidContext().applicationContext.cacheDir, "tmp").apply { mkdirs() } } @@ -429,6 +471,7 @@ val appKoinModule = module { json = get(), ) } + single { ValidateUsernameUseCase() } @@ -440,6 +483,7 @@ val appKoinModule = module { single { EncodeUserHandleUseCaseImpl() } + single { CreatePublicKeyCredentialRequestOptionsJsonUseCase() } @@ -450,11 +494,13 @@ val appKoinModule = module { createPublicKeyCredentialRequestOptionsJsonUseCase = get() ) } + single { VerifyDomainUseCaseImpl( context = androidApplication() ) } + single { SavePasswordUseCaseAndroidImpl() } @@ -507,6 +553,7 @@ val appKoinModule = module { json = get() ) } + single { XXHasher64FactoryCommonJvm() } @@ -522,6 +569,11 @@ val appKoinModule = module { single { SetClipboardStringUseCaseAndroid(androidContext().applicationContext) } + + single { + LaunchCustomTabUseCaseAndroid(androidContext().applicationContext) + } + single { ShouldShowOnboardingUseCase(settings = get()) } @@ -582,6 +634,18 @@ val appKoinModule = module { get() } + single { + ResolveUrlToNavCommandUseCase() + } + + single { + InitDeepLinkUriProviderUseCaseAndroid() + } + + single { + get() + } + single { PhoneNumValidatorAndroid(iPhoneNumberUtil = get()) } @@ -634,6 +698,18 @@ val appKoinModule = module { single { get() } + + single { + NavigateOnAppStartUseCase( + accountManager = get(), + initDeepLinkUriProvider = get(), + getDeferredDeepLinkUseCase = get(), + customDeepLinkToUrlUseCase = get(), + resolveUrlToNavCommandUseCase = get(), + settings = get(), + ) + } + /** * The SchoolDirectoryEntry scope might be one instance per school url or one instance per account * per url. @@ -686,7 +762,6 @@ val appKoinModule = module { ) } - scoped { GetInviteInfoUseCaseClient( schoolUrl = SchoolDirectoryEntryScopeId.parse(id).schoolUrl, @@ -695,6 +770,12 @@ val appKoinModule = module { ) } + scoped { + CreateInviteLinkUseCase( + schoolUrl = SchoolDirectoryEntryScopeId.parse(id).schoolUrl, + ) + } + scoped { UsernameSuggestionUseCaseClient( schoolUrl = SchoolDirectoryEntryScopeId.parse(id).schoolUrl, @@ -737,11 +818,18 @@ val appKoinModule = module { uidNumberMapper = get(), ) } + scoped { ValidateQrCodeUseCase( schoolUrl = SchoolDirectoryEntryScopeId.parse(id).schoolUrl ) } + + scoped { + NavigateOnAccountCreatedUseCase( + schoolUrl = SchoolDirectoryEntryScopeId.parse(id).schoolUrl + ) + } } /** @@ -751,7 +839,7 @@ val appKoinModule = module { */ scope { /* Koin doesn't have an onScopeCreated kind of function or event listener. The - * RespectAccount scope is linked ot the SchoolDirectoryEntry scope when + * RespectAccount scope is linked to the SchoolDirectoryEntry scope when * RespectAccountSchoolScopeLink is retrieved. RespectAccountSchoolScopeLink is a root * dependency that all dependencies on RespectAccountScope require. */ @@ -771,7 +859,6 @@ val appKoinModule = module { RespectAccountSchoolScopeLink(accountScopeId.schoolUrl) } - scoped { get().providerFor(id) } @@ -793,12 +880,14 @@ val appKoinModule = module { httpClient = get(), ) } + scoped { RevokePasskeyUseCaseClient( schoolUrl = SchoolDirectoryEntryScopeId.parse(id).schoolUrl, httpClient = get(), ) } + scoped { EnqueueDrainRemoteWriteQueueUseCaseAndroidImpl( context = androidContext().applicationContext, @@ -814,19 +903,24 @@ val appKoinModule = module { ) } - scoped { + scoped { val accountScopeId = RespectAccountScopeId.parse(id) + + SchoolDataSourceDb( + schoolDb = get(), + uidNumberMapper = get(), + authenticatedUser = AuthenticatedUserPrincipalId( + accountScopeId.accountPrincipalId.guid + ), + checkPersonPermissionUseCase = get(), + ) + } + + scoped { val schoolUrl = get() SchoolDataSourceRepository( - local = SchoolDataSourceDb( - schoolDb = get(), - uidNumberMapper = get(), - authenticatedUser = AuthenticatedUserPrincipalId( - accountScopeId.accountPrincipalId.guid - ), - checkPersonPermissionUseCase = get(), - ), + local = get(), remote = SchoolDataSourceHttp( schoolUrl = schoolUrl.url, schoolDirectoryEntryDataSource = get().schoolDirectoryEntryDataSource, @@ -844,11 +938,14 @@ val appKoinModule = module { schoolDataSource = get(), ) } + scoped { - AddChildAccountUseCaseDataSource( - schoolDataSource = get(), - schoolPrimaryKeyGenerator = get(), - authenticatedUser = RespectAccountScopeId.parse(id).accountPrincipalId, + AddChildAccountUseCaseClient( + schoolUrl = RespectAccountScopeId.parse(id).schoolUrl, + authTokenProvider = get(), + httpClient = get(), + schoolDirectoryEntryDataSource = get().schoolDirectoryEntryDataSource, + schoolDataSourceLocal = get(), ) } @@ -909,14 +1006,24 @@ val appKoinModule = module { ) } + scoped { + GetWritableRolesListUseCaseImpl() + } + + scoped { + CreateClassUseCase(dataSource = get()) + } } + single { MockRunReportUseCaseClientImpl() } - single{ + + single { ValidateEmailUseCase() } + single { CreateGraphFormatterUseCase() } -} +} \ No newline at end of file diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/App.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/App.kt index d295c6154..647f4a6a5 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/App.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/App.kt @@ -33,6 +33,7 @@ import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.runtime.collectAsState import androidx.compose.runtime.rememberCoroutineScope import androidx.navigation.compose.rememberNavController +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.jetbrains.compose.resources.StringResource @@ -42,6 +43,7 @@ import org.koin.compose.getKoin import org.koin.compose.koinInject import world.respect.app.components.uiTextStringResource import world.respect.app.effects.NavControllerLogEffect +import world.respect.navigation.NavCommandEffect import world.respect.shared.domain.account.RespectAccountManager import world.respect.shared.domain.biometric.BiometricAuthUseCase import world.respect.shared.generated.resources.Res @@ -57,7 +59,9 @@ import world.respect.shared.navigation.AccountList import world.respect.shared.navigation.RespectAppLauncher import world.respect.shared.navigation.AssignmentList import world.respect.shared.navigation.ClazzList +import world.respect.shared.navigation.NavCommand import world.respect.shared.navigation.PersonList +import world.respect.shared.navigation.RespectComposeNavController import world.respect.shared.resources.StringResourceUiText import world.respect.shared.resources.StringUiText import world.respect.shared.viewmodel.app.appstate.AppUiState @@ -121,9 +125,11 @@ val APP_TOP_LEVEL_NAV_ITEMS_FOR_CHILD = listOf( @OptIn(ExperimentalLayoutApi::class) @Composable fun App( + activityNavCommandFlow: Flow, widthClass: SizeClass = SizeClass.MEDIUM, useBottomBar: Boolean = true, - onAppStateChanged: (AppUiState) -> Unit = { }) { + onAppStateChanged: (AppUiState) -> Unit = { } +) { val appUiState = remember { mutableStateOf( AppUiState( @@ -134,6 +140,10 @@ fun App( } val navController = rememberNavController() + val respectNavController = remember(Unit) { + RespectComposeNavController(navController) + } + val coroutineScope = rememberCoroutineScope() val accountManager: RespectAccountManager = koinInject() @@ -148,6 +158,11 @@ fun App( NavControllerLogEffect(navController) + NavCommandEffect( + navHostController = respectNavController, + navCommandFlow = activityNavCommandFlow, + ) + var appUiStateVal by appUiState LaunchedEffect(appUiStateVal) { onAppStateChanged(appUiStateVal) @@ -231,7 +246,27 @@ fun App( } }, floatingActionButton = { - if (appUiStateVal.fabState.visible) { + if (appUiStateVal.expandableFabState.visible) { + ExpandableFab( + state = appUiStateVal.expandableFabState, + onToggle = { + appUiStateVal = appUiStateVal.copy( + expandableFabState = appUiStateVal.expandableFabState.copy( + expanded = !appUiStateVal.expandableFabState.expanded + ) + ) + }, + onItemClick = { item -> + item.onClick() + appUiStateVal = appUiStateVal.copy( + expandableFabState = appUiStateVal.expandableFabState.copy( + expanded = false + ) + ) + } + ) + } + else if (appUiStateVal.fabState.visible) { ExtendedFloatingActionButton( modifier = Modifier.testTag("floating_action_button"), onClick = appUiStateVal.fabState.onClick, @@ -265,6 +300,7 @@ fun App( ) { innerPadding -> AppNavHost( navController = navController, + respectNavController = respectNavController, onSetAppUiState = { appUiStateVal = it }, @@ -273,4 +309,4 @@ fun App( } } -} +} \ No newline at end of file diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt index 2217d29eb..f64f7a53d 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/app/AppNavHost.kt @@ -22,12 +22,12 @@ import world.respect.app.view.enrollment.list.EnrollmentListScreen import world.respect.app.view.learningunit.detail.LearningUnitDetailScreen import world.respect.app.view.learningunit.list.LearningUnitListScreen import world.respect.app.view.manageuser.accountlist.AccountListScreen -import world.respect.app.view.manageuser.confirmation.ConfirmationScreen +import world.respect.app.view.manageuser.acceptinvite.AcceptInviteScreen import world.respect.app.view.manageuser.createaccount.CreateAccountScreen import world.respect.app.view.manageuser.enterpasswordsignup.EnterPasswordSignupScreen import world.respect.app.view.manageuser.getstarted.GetStartedScreen import world.respect.app.view.manageuser.howpasskeywork.HowPasskeyWorksScreen -import world.respect.app.view.manageuser.joinclazzwithcode.JoinClazzWithCodeScreen +import world.respect.app.view.manageuser.enterinvitecode.EnterInviteCodeScreen import world.respect.app.view.manageuser.login.LoginScreen import world.respect.app.view.manageuser.otheroption.OtherOptionsScreen import world.respect.app.view.manageuser.otheroptionsignup.OtherOptionsSignupScreen @@ -36,13 +36,18 @@ import world.respect.app.view.manageuser.termsandcondition.TermsAndConditionScre import world.respect.app.view.manageuser.waitingforapproval.WaitingForApprovalScreen import world.respect.app.view.onboarding.OnboardingScreen import world.respect.app.view.person.changepassword.ChangePasswordScreen +import world.respect.app.view.person.copycode.CopyInviteCodeScreen import world.respect.app.view.person.detail.PersonDetailScreen import world.respect.app.view.person.edit.PersonEditScreen +import world.respect.app.view.person.inviteperson.InvitePersonScreen import world.respect.app.view.person.list.PersonListScreen import world.respect.app.view.person.manageaccount.ManageAccountScreen import world.respect.app.view.person.passkeyList.PasskeyListScreen +import world.respect.app.view.person.qrcode.InviteQrScreen import world.respect.app.view.person.setusernameandpassword.CreateAccountSetPasswordScreen import world.respect.app.view.person.setusernameandpassword.CreateAccountSetUsernameScreen +import world.respect.app.view.playlists.mapping.edit.PlaylistEditScreenForViewModel +import world.respect.app.view.playlists.mapping.share.PlaylistShareScreen import world.respect.app.view.report.detail.ReportDetailScreen import world.respect.app.view.report.edit.ReportEditScreen import world.respect.app.view.report.filteredit.ReportFilterEditScreen @@ -51,13 +56,14 @@ import world.respect.app.view.report.indicator.edit.IndictorEditScreen import world.respect.app.view.report.indicator.list.IndicatorListScreen import world.respect.app.view.report.list.ReportListScreen import world.respect.app.view.report.list.ReportTemplateListScreen +import world.respect.app.view.scanqrcode.ScanQRCodeScreen import world.respect.app.view.schooldirectory.edit.SchoolDirectoryEditScreen import world.respect.app.view.schooldirectory.list.SchoolDirectoryListScreen import world.respect.app.view.settings.SettingsScreenForViewModel -import world.respect.app.view.scanqrcode.ScanQRCodeScreen import world.respect.app.viewmodel.respectViewModel import world.respect.shared.navigation.AccountList import world.respect.shared.navigation.Acknowledgement +import world.respect.shared.navigation.AcceptInvite import world.respect.shared.navigation.AppsDetail import world.respect.shared.navigation.AssignmentDetail import world.respect.shared.navigation.AssignmentEdit @@ -66,17 +72,21 @@ import world.respect.shared.navigation.ChangePassword import world.respect.shared.navigation.ClazzDetail import world.respect.shared.navigation.ClazzEdit import world.respect.shared.navigation.ClazzList -import world.respect.shared.navigation.ConfirmationScreen +import world.respect.shared.navigation.CopyCode import world.respect.shared.navigation.CreateAccount +import world.respect.shared.navigation.CreateAccountSetPassword +import world.respect.shared.navigation.CreateAccountSetUsername import world.respect.shared.navigation.EnrollmentEdit import world.respect.shared.navigation.EnrollmentList import world.respect.shared.navigation.EnterLink +import world.respect.shared.navigation.EnterInviteCode import world.respect.shared.navigation.EnterPasswordSignup import world.respect.shared.navigation.GetStartedScreen import world.respect.shared.navigation.HowPasskeyWorks import world.respect.shared.navigation.IndicatorDetail import world.respect.shared.navigation.IndicatorList import world.respect.shared.navigation.IndictorEdit +import world.respect.shared.navigation.InvitePerson import world.respect.shared.navigation.JoinClazzWithCode import world.respect.shared.navigation.LearningUnitDetail import world.respect.shared.navigation.LearningUnitList @@ -89,6 +99,9 @@ import world.respect.shared.navigation.PasskeyList import world.respect.shared.navigation.PersonDetail import world.respect.shared.navigation.PersonEdit import world.respect.shared.navigation.PersonList +import world.respect.shared.navigation.PlaylistEdit +import world.respect.shared.navigation.PlaylistShare +import world.respect.shared.navigation.QrCode import world.respect.shared.navigation.Report import world.respect.shared.navigation.ReportDetail import world.respect.shared.navigation.ReportEdit @@ -100,8 +113,7 @@ import world.respect.shared.navigation.RespectComposeNavController import world.respect.shared.navigation.ScanQRCode import world.respect.shared.navigation.SchoolDirectoryEdit import world.respect.shared.navigation.SchoolDirectoryList -import world.respect.shared.navigation.CreateAccountSetPassword -import world.respect.shared.navigation.CreateAccountSetUsername +import world.respect.shared.navigation.Settings import world.respect.shared.navigation.SignupScreen import world.respect.shared.navigation.TermsAndCondition import world.respect.shared.navigation.WaitingForApproval @@ -118,11 +130,11 @@ import world.respect.shared.viewmodel.enrollment.edit.EnrollmentEditViewModel import world.respect.shared.viewmodel.enrollment.list.EnrollmentListViewModel import world.respect.shared.viewmodel.learningunit.detail.LearningUnitDetailViewModel import world.respect.shared.viewmodel.learningunit.list.LearningUnitListViewModel -import world.respect.shared.viewmodel.manageuser.confirmation.ConfirmationViewModel +import world.respect.shared.viewmodel.manageuser.acceptinvite.AcceptInviteViewModel import world.respect.shared.viewmodel.manageuser.enterpasswordsignup.EnterPasswordSignupViewModel import world.respect.shared.viewmodel.manageuser.getstarted.GetStartedViewModel import world.respect.shared.viewmodel.manageuser.howpasskeywork.HowPasskeyWorksViewModel -import world.respect.shared.viewmodel.manageuser.joinclazzwithcode.JoinClazzWithCodeViewModel +import world.respect.shared.viewmodel.manageuser.enterinvitecode.EnterInviteCodeViewModel import world.respect.shared.viewmodel.manageuser.login.LoginViewModel import world.respect.shared.viewmodel.manageuser.otheroption.OtherOptionsViewModel import world.respect.shared.viewmodel.manageuser.otheroptionsignup.OtherOptionsSignupViewModel @@ -131,6 +143,8 @@ import world.respect.shared.viewmodel.manageuser.signup.CreateAccountViewModel import world.respect.shared.viewmodel.manageuser.termsandcondition.TermsAndConditionViewModel import world.respect.shared.viewmodel.manageuser.waitingforapproval.WaitingForApprovalViewModel import world.respect.shared.viewmodel.onboarding.OnboardingViewModel +import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistEditViewModel +import world.respect.shared.viewmodel.playlists.mapping.share.PlaylistShareViewModel import world.respect.shared.viewmodel.report.detail.ReportDetailViewModel import world.respect.shared.viewmodel.report.edit.ReportEditViewModel import world.respect.shared.viewmodel.report.filteredit.ReportFilterEditViewModel @@ -139,36 +153,24 @@ import world.respect.shared.viewmodel.report.indictor.edit.IndicatorEditViewMode import world.respect.shared.viewmodel.report.indictor.list.IndicatorListViewModel import world.respect.shared.viewmodel.report.list.ReportListViewModel import world.respect.shared.viewmodel.report.list.ReportTemplateListViewModel -import world.respect.app.view.playlists.mapping.edit.PlaylistEditScreenForViewModel -import world.respect.app.view.playlists.mapping.share.PlaylistShareScreen -import world.respect.shared.viewmodel.settings.SettingsViewModel - -import world.respect.shared.viewmodel.playlists.mapping.edit.PlaylistEditViewModel -import world.respect.shared.navigation.Settings - -import world.respect.shared.navigation.PlaylistEdit -import world.respect.shared.navigation.PlaylistShare -import world.respect.shared.viewmodel.playlists.mapping.share.PlaylistShareViewModel import world.respect.shared.viewmodel.schooldirectory.edit.SchoolDirectoryEditViewModel import world.respect.shared.viewmodel.schooldirectory.list.SchoolDirectoryListViewModel - +import world.respect.shared.viewmodel.settings.SettingsViewModel @Composable fun AppNavHost( navController: NavHostController, + respectNavController: RespectComposeNavController = remember(Unit) { + RespectComposeNavController(navController) + }, onSetAppUiState: (AppUiState) -> Unit, modifier: Modifier, ) { - val respectNavController = remember { - RespectComposeNavController(navController) - } - NavHost( navController = navController, - startDestination = Acknowledgement, + startDestination = Acknowledgement(), modifier = modifier, ) { - composable { val viewModel: AcknowledgementViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, @@ -177,14 +179,14 @@ fun AppNavHost( AcknowledgementScreen(viewModel) } - composable{ + composable { val viewModel: OnboardingViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, navController = respectNavController ) - OnboardingScreen(viewModel) } + composable { val viewModel: LoginViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, @@ -193,12 +195,12 @@ fun AppNavHost( LoginScreen(viewModel) } - composable { - val viewModel: JoinClazzWithCodeViewModel = respectViewModel( + composable { + val viewModel: EnterInviteCodeViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, navController = respectNavController ) - JoinClazzWithCodeScreen(viewModel) + EnterInviteCodeScreen(viewModel) } composable { @@ -305,6 +307,7 @@ fun AppNavHost( ) ReportDetailScreen(navController = navController, viewModel = viewModel) } + composable { val viewModel: ReportEditViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, @@ -312,6 +315,7 @@ fun AppNavHost( ) ReportEditScreen(viewModel = viewModel) } + composable { val viewModel: ReportListViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, @@ -319,6 +323,7 @@ fun AppNavHost( ) ReportListScreen(viewModel = viewModel) } + composable { val viewModel: ReportTemplateListViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, @@ -326,6 +331,7 @@ fun AppNavHost( ) ReportTemplateListScreen(viewModel = viewModel) } + composable { val viewModel: IndicatorEditViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, @@ -333,6 +339,7 @@ fun AppNavHost( ) IndictorEditScreen(viewModel = viewModel) } + composable { val viewModel: ReportFilterEditViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, @@ -340,6 +347,7 @@ fun AppNavHost( ) ReportFilterEditScreen(navController = navController, viewModel = viewModel) } + composable { val viewModel: IndicatorListViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, @@ -347,6 +355,7 @@ fun AppNavHost( ) IndicatorListScreen(viewModel = viewModel) } + composable { val viewModel: IndicatorDetailViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, @@ -362,6 +371,7 @@ fun AppNavHost( ) HowPasskeyWorksScreen(viewModel = viewModel) } + composable { val viewModel: AppListViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, @@ -371,6 +381,7 @@ fun AppNavHost( viewModel = viewModel ) } + composable { val viewModel: EnterLinkViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, @@ -443,12 +454,12 @@ fun AppNavHost( ) } - composable { - val viewModel: ConfirmationViewModel = respectViewModel( + composable { + val viewModel: AcceptInviteViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, navController = respectNavController ) - ConfirmationScreen( + AcceptInviteScreen( viewModel = viewModel ) } @@ -518,6 +529,7 @@ fun AppNavHost( ) ) } + composable { PasskeyListScreen( viewModel = respectViewModel( @@ -535,6 +547,7 @@ fun AppNavHost( ) ) } + composable { val viewModel: SettingsViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, @@ -544,6 +557,7 @@ fun AppNavHost( viewModel = viewModel ) } + composable { ScanQRCodeScreen( viewModel = respectViewModel( @@ -562,6 +576,7 @@ fun AppNavHost( viewModel = viewModel ) } + composable { val viewModel: PlaylistShareViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, @@ -571,21 +586,20 @@ fun AppNavHost( viewModel = viewModel ) } - composable{ + + composable { val viewModel: SchoolDirectoryListViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, navController = respectNavController ) - SchoolDirectoryListScreen(viewModel) } - composable{ + composable { val viewModel: SchoolDirectoryEditViewModel = respectViewModel( onSetAppUiState = onSetAppUiState, navController = respectNavController ) - SchoolDirectoryEditScreen(viewModel) } @@ -616,8 +630,31 @@ fun AppNavHost( ) } - } -} - + composable { + InvitePersonScreen( + viewModel = respectViewModel( + onSetAppUiState = onSetAppUiState, + navController = respectNavController, + ) + ) + } + composable { + CopyInviteCodeScreen( + viewModel = respectViewModel( + onSetAppUiState = onSetAppUiState, + navController = respectNavController, + ) + ) + } + composable { + InviteQrScreen( + viewModel = respectViewModel( + onSetAppUiState = onSetAppUiState, + navController = respectNavController, + ) + ) + } + } +} \ No newline at end of file diff --git a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt index f5226a67e..b4106455b 100644 --- a/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt +++ b/respect-app-compose/src/commonMain/kotlin/world/respect/app/view/learningunit/detail/LearningUnitDetailScreen.kt @@ -372,7 +372,7 @@ private fun PlaylistDetailScreen( ) ActionButton( icon = Icons.Default.ContentCopy, - label = stringResource(Res.string.copy), + label = stringResource(Res.string.copy_list), onClick = onClickCopy, modifier = Modifier.testTag("copy_btn") ) diff --git a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml index 8d6afd5ab..a10d8f953 100644 --- a/respect-lib-shared/src/commonMain/composeResources/values/strings.xml +++ b/respect-lib-shared/src/commonMain/composeResources/values/strings.xml @@ -87,8 +87,10 @@ Enter your password Invitation Invitation for: - I’m a Student - I’m a Parent + Congratulations + You have successfully registered the school + I'm a Student + I'm a Parent Accept Terms and conditions Your profile @@ -143,6 +145,7 @@ Accept Invite"> Dismiss invite Pending requests"> + Pending request to join"> Copy invite code : "> Invite with link or QR code"> Share QR code"> @@ -175,6 +178,7 @@ School Directory Let's get started School name + School name Type school name here... I have an invite code Invalid school name @@ -222,7 +226,6 @@ Create passkey Passkey - Add account Logout Profile @@ -314,7 +317,6 @@ Indicator Indicator detail - = Equals != Not Equals @@ -354,7 +356,6 @@ Grade Level Assessment Type (Self/Assignment) - Age Group Region @@ -418,7 +419,6 @@ Expand students list Collapse students list - Add person Select person Edit person @@ -518,8 +518,48 @@ Pending teacher Edit enrollment + Regenerate invite code + Invite settings + Invite multiple people + Approval required + Allow multiple people to use this invite + Send link via SMS + Send link via email + Share link + QR code + Code + + This code is private. If it is shared with someone, they can enter this code here to join. + + Class name or school name + + This QR code is private. If it is shared with someone, they can scan to join. + + School server URL + + Approval not required until: + + Reset code + + Invite students directly + Students register themselves + + Invite via parents + Parents register and student uses parents' device + + Something went wrong: please check invite code and connection + + Developed by UstadMobile FZ-LLC + + RESPECT App. Copyright 2025-2026 UstadMobile FZ-LLC. + + The RESPECT App is open-source software licensed under the AGPLv3 license. Contains open source libraries copyright respective contributors. + + Supported by Spix Foundation. + + Select Host - Section title + Section name Playlists School Playlists My Playlists @@ -535,7 +575,8 @@ Created by: Edit playlist Home - copy playlist + Copy + copy playlist Make a copy %1$d sections • %2$d items Task @@ -557,4 +598,4 @@ Not set - + \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt index cbe621801..732f3772f 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/navigation/AppRoutes.kt @@ -4,18 +4,21 @@ package world.respect.shared.navigation import io.ktor.http.Url +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.json.Json -import world.respect.shared.domain.account.invite.RespectRedeemInviteRequest import world.respect.datalayer.school.model.EnrollmentRoleEnum +import world.respect.datalayer.school.model.Person import world.respect.datalayer.school.model.PersonRoleEnum import world.respect.datalayer.school.model.report.ReportFilter -import world.respect.shared.viewmodel.playlists.mapping.model.Playlists +import world.respect.shared.domain.account.invite.RespectRedeemInviteRequest import world.respect.shared.ext.NextAfterScan import world.respect.shared.viewmodel.learningunit.LearningUnitSelection -import world.respect.shared.viewmodel.manageuser.profile.ProfileType +import world.respect.shared.viewmodel.manageuser.signup.SignupScreenModeEnum +import world.respect.shared.viewmodel.playlists.mapping.model.Playlists +import world.respect.shared.viewmodel.schooldirectory.list.SchoolDirectoryMode /** * Mostly TypeSafe navigation for the RESPECT app. All serialized properties must be primitives or @@ -30,7 +33,19 @@ import world.respect.shared.viewmodel.manageuser.profile.ProfileType sealed interface RespectAppRoute @Serializable -object Acknowledgement : RespectAppRoute +data class Acknowledgement( + val schoolUrlStr: String? = null, + val inviteCode: String? = null +) : RespectAppRoute { + + @Transient + val schoolUrl = schoolUrlStr?.let { Url(it) } + + companion object { + fun create(schoolUrl: Url? = null, inviteCode: String? = null) = + Acknowledgement(schoolUrl.toString(), inviteCode) + } +} @Serializable data class JoinClazzWithCode( @@ -43,14 +58,38 @@ data class JoinClazzWithCode( companion object { fun create(schoolUrl: Url) = JoinClazzWithCode(schoolUrl.toString()) } +} + +@Serializable +data class EnterInviteCode( + val schoolUrlStr: String +) : RespectAppRoute { + @Transient + val schoolUrl = Url(schoolUrlStr) + + companion object { + fun create(schoolUrl: Url) = EnterInviteCode(schoolUrl.toString()) + } } @Serializable object Onboarding : RespectAppRoute @Serializable -object SchoolDirectoryList : RespectAppRoute +data class SchoolDirectoryList( + val modeStr: String = SchoolDirectoryMode.MANAGE.value +) : RespectAppRoute { + + @Transient + val mode: SchoolDirectoryMode = SchoolDirectoryMode.fromValue(modeStr) + + companion object { + fun create( + mode: SchoolDirectoryMode = SchoolDirectoryMode.MANAGE + ) = SchoolDirectoryList(mode.value) + } +} @Serializable object SchoolDirectoryEdit : RespectAppRoute @@ -66,13 +105,12 @@ data class LoginScreen( companion object { fun create(schoolUrl: Url) = LoginScreen(schoolUrl.toString()) } - } @Serializable data class RespectAppLauncher( val resultDestStr: String? = null, -) : RespectAppRoute, RouteWithResultDest{ +) : RespectAppRoute, RouteWithResultDest { @Transient override val resultDest: ResultDest? = ResultDest.fromStringOrNull(resultDestStr) @@ -94,7 +132,6 @@ data class AssignmentDetail( val uid: String, ) : RespectAppRoute - @Serializable data class AssignmentEdit( val guid: String? = null, @@ -168,6 +205,7 @@ data class AssignmentEdit( } } } + @Serializable object ClazzList : RespectAppRoute @@ -186,12 +224,12 @@ data class EnrollmentList( @Transient val role = EnrollmentRoleEnum.fromValue(roleStr) - companion object { + companion object { fun create( filterByPersonUid: String, role: EnrollmentRoleEnum, filterByClassUid: String - ) : EnrollmentList { + ): EnrollmentList { return EnrollmentList( filterByPersonUid = filterByPersonUid, roleStr = role.value, @@ -199,7 +237,6 @@ data class EnrollmentList( ) } } - } @Serializable @@ -230,7 +267,6 @@ class AddPersonToClazz( } } - @Serializable data class ClazzEdit( val guid: String? @@ -331,11 +367,9 @@ class AppsDetail private constructor( resultDestStr = resultDest?.encodeToJsonStringOrNull() ) } - } } - /** * @property opdsFeedUrl the URL for an OPDS feed containing a list of learning units and/or links * to other feeds @@ -369,10 +403,9 @@ class LearningUnitList( resultDestStr = resultDest.encodeToJsonStringOrNull() ) } - } - } + @Serializable class EnterPasswordSignup private constructor( private val schoolUrlStr: String, @@ -380,7 +413,7 @@ class EnterPasswordSignup private constructor( ) : RespectAppRoute { @Transient - val respectRedeemInviteRequest : RespectRedeemInviteRequest = + val respectRedeemInviteRequest: RespectRedeemInviteRequest = Json.decodeFromString(inviteRedeemRequestStr) @Transient @@ -396,7 +429,6 @@ class EnterPasswordSignup private constructor( Json.encodeToString(inviteRequest) ) } - } } @@ -407,7 +439,7 @@ class OtherOptionsSignup private constructor( ) : RespectAppRoute { @Transient - val respectRedeemInviteRequest : RespectRedeemInviteRequest = + val respectRedeemInviteRequest: RespectRedeemInviteRequest = Json.decodeFromString(inviteRedeemRequestStr) @Transient @@ -425,14 +457,14 @@ class OtherOptionsSignup private constructor( respectRedeemInviteRequest, schoolUrl.toString() ) } - } } @Serializable -class ConfirmationScreen( +class AcceptInvite( val schoolUrlStr: String, val code: String, + val canGoBack: Boolean = true, ) : RespectAppRoute { @Transient @@ -442,9 +474,11 @@ class ConfirmationScreen( fun create( schoolUrl: Url, code: String, - ) = ConfirmationScreen( + canGoBack: Boolean = true, + ) = AcceptInvite( schoolUrlStr = schoolUrl.toString(), code = code, + canGoBack = canGoBack, ) } } @@ -455,29 +489,36 @@ class WaitingForApproval : RespectAppRoute @Serializable class SignupScreen( private val schoolUrlStr: String, - private val profileTypeStr: String, private val inviteRedeemRequestStr: String, + private val signupModeStr: String, + private val parentPersonStr: String?, ) : RespectAppRoute { @Transient - val type: ProfileType = ProfileType.fromValue(profileTypeStr) - @Transient - val respectRedeemInviteRequest : RespectRedeemInviteRequest = + val respectRedeemInviteRequest: RespectRedeemInviteRequest = Json.decodeFromString(inviteRedeemRequestStr) + @Transient + val signupMode: SignupScreenModeEnum = SignupScreenModeEnum.fromValue(signupModeStr) + @Transient val schoolUrl = Url(schoolUrlStr) + @Transient + val parentPerson: Person? = parentPersonStr?.let { Json.decodeFromString(it) } + companion object { fun create( schoolUrl: Url, - profileType: ProfileType, inviteRequest: RespectRedeemInviteRequest, + signupMode: SignupScreenModeEnum = SignupScreenModeEnum.STANDARD, + parentPerson: Person? = null, ): SignupScreen { return SignupScreen( schoolUrlStr = schoolUrl.toString(), - profileTypeStr = profileType.value, - inviteRedeemRequestStr = Json.encodeToString(inviteRequest) + inviteRedeemRequestStr = Json.encodeToString(inviteRequest), + signupModeStr = signupMode.value, + parentPersonStr = parentPerson?.let { Json.encodeToString(it) } ) } } @@ -486,15 +527,11 @@ class SignupScreen( @Serializable class TermsAndCondition( private val schoolUrlStr: String, - private val profileTypeStr: String, private val inviteRedeemRequestStr: String, ) : RespectAppRoute { @Transient - val type: ProfileType = ProfileType.fromValue(profileTypeStr) - - @Transient - val respectRedeemInviteRequest : RespectRedeemInviteRequest = + val respectRedeemInviteRequest: RespectRedeemInviteRequest = Json.decodeFromString(inviteRedeemRequestStr) @Transient @@ -503,30 +540,30 @@ class TermsAndCondition( companion object { fun create( schoolUrl: Url, - profileType: ProfileType, - inviteRequest: RespectRedeemInviteRequest, + inviteRequest: RespectRedeemInviteRequest ): TermsAndCondition { return TermsAndCondition( schoolUrlStr = schoolUrl.toString(), - profileTypeStr = profileType.value, inviteRedeemRequestStr = Json.encodeToString(inviteRequest) ) } } } +@Serializable +data class SchoolRegistrationComplete( + val schoolUrl: String = "", + val authToken: String? = null +) : RespectAppRoute + @Serializable class CreateAccount( private val schoolUrlStr: String, - private val profileTypeStr: String, private val inviteRedeemRequestStr: String, ) : RespectAppRoute { @Transient - val type = ProfileType.fromValue(profileTypeStr) - - @Transient - val respectRedeemInviteRequest : RespectRedeemInviteRequest = Json.decodeFromString( + val respectRedeemInviteRequest: RespectRedeemInviteRequest = Json.decodeFromString( inviteRedeemRequestStr ) @@ -536,12 +573,10 @@ class CreateAccount( companion object { fun create( schoolUrl: Url, - profileType: ProfileType, - inviteRequest: RespectRedeemInviteRequest, + inviteRequest: RespectRedeemInviteRequest ): CreateAccount { return CreateAccount( schoolUrlStr = schoolUrl.toString(), - profileTypeStr = profileType.value, inviteRedeemRequestStr = Json.encodeToString(inviteRequest) ) } @@ -559,9 +594,8 @@ class CreateAccount( * @property expectedIdentifier (optional), where a refererUrl is provided, to use cached feed * metadata as above, the identifier of the publication within the feed. */ - @Serializable -class LearningUnitDetail ( +class LearningUnitDetail( private val learningUnitManifestUrlStr: String, private val appManifestUrlStr: String, private val refererUrlStr: String? = null, @@ -626,8 +660,6 @@ class LearningUnitDetail ( } } - - @Serializable class LearningUnitViewer( private val learningUnitIdStr: String, @@ -643,19 +675,28 @@ class LearningUnitViewer( ) } } - } @Serializable object AccountList : RespectAppRoute - +/** + * @property addToClassUid if the PersonList screen has been navigated when the user clicks + * add student or add teacher on the ClassDetail screen, then the classUid. + * @property addToClassRoleStr if the PersonList screen has been navigated when the user clicks + * add student or add teacher on the ClassDetail screen, then the role + */ @Serializable data class PersonList( private val filterByRoleStr: String? = null, val isTopLevel: Boolean = false, private val resultDestStr: String? = null, - val showInviteCode: String? = null, + val inviteUid: String? = null, + val classNameStr: String? = null, + val addToClassUid: String? = null, + val addToClassRoleStr: String? = null, + val personGuidStr: String? = null, + val hideInvite: Boolean = false, ) : RespectAppRoute, RouteWithResultDest { @Transient @@ -663,6 +704,11 @@ data class PersonList( PersonRoleEnum.fromValue(it) } + @Transient + val role: EnrollmentRoleEnum? = addToClassRoleStr?.let { + EnrollmentRoleEnum.fromValue(it) + } + @Transient override val resultDest: ResultDest? = ResultDest.fromStringOrNull(resultDestStr) @@ -672,14 +718,23 @@ data class PersonList( filterByRole: PersonRoleEnum? = null, isTopLevel: Boolean = false, resultDest: ResultDest? = null, - showInviteCode: String? = null, + inviteUid: String? = null, + className: String? = null, + classUid: String? = null, + personGuid: String? = null, + role: EnrollmentRoleEnum? = null, + hideInvite: Boolean = false, ) = PersonList( filterByRoleStr = filterByRole?.value, isTopLevel = isTopLevel, resultDestStr = resultDest.encodeToJsonStringOrNull(), - showInviteCode = showInviteCode, + inviteUid = inviteUid, + addToClassUid = classUid, + classNameStr = className, + addToClassRoleStr = role?.value, + personGuidStr = personGuid, + hideInvite = hideInvite, ) - } } @@ -693,7 +748,6 @@ data class PasskeyList( val guid: String, ) : RespectAppRoute - /** * @param guid the Uid of the Person account to manage as person Person.guid * @param setPersonQrBadgeUrlStr see setPersonQrBadgeUrl @@ -715,7 +769,6 @@ data class ManageAccount( @Transient val setPersonQrBadgeUrl: Url? = setPersonQrBadgeUrlStr?.let { Url(it) } - companion object { fun create( guid: String, @@ -755,8 +808,6 @@ data class PersonEdit( presetRoleStr = presetRole?.value, ) } - - } @Serializable @@ -833,10 +884,11 @@ data class PlaylistEdit( ) } } + @Serializable data class CreateAccountSetUsername( val guid: String -): RespectAppRoute +) : RespectAppRoute @Serializable data class CreateAccountSetPassword( @@ -844,12 +896,10 @@ data class CreateAccountSetPassword( val username: String? = null, ) : RespectAppRoute - - @Serializable data class ChangePassword( val guid: String, -): RespectAppRoute +) : RespectAppRoute @Serializable data class PlaylistShare( @@ -860,3 +910,54 @@ data class PlaylistShare( } } +@Serializable +data class InvitePerson( + val invitePersonOptionsStr: String, +) : RespectAppRoute { + + /** + * As there are three types of invitations, so there are three different types of invite options + */ + @Serializable + sealed interface InvitePersonOptions + + /** + * @property presetRole if set, the role dropdown will not be displayed. + */ + @Serializable + @SerialName("newuser") + data class NewUserInviteOptions( + val presetRole: PersonRoleEnum? + ) : InvitePersonOptions + + @Serializable + @SerialName("class") + data class ClassInviteOptions( + val inviteUid: String, + ) : InvitePersonOptions + + @Transient + val invitePersonOptions: InvitePersonOptions = Json.decodeFromString( + invitePersonOptionsStr + ) + + companion object { + + fun create( + invitePersonOptions: InvitePersonOptions + ) = InvitePerson( + invitePersonOptionsStr = Json.encodeToString(invitePersonOptions) + ) + } +} + +@Serializable +data class QrCode( + val inviteLink: String? = null, + val schoolOrClass: String? = null +) : RespectAppRoute + +@Serializable +data class CopyCode( + val inviteCode: String? = null +) : RespectAppRoute \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/manageuser/acceptinvite/AcceptInviteViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/manageuser/acceptinvite/AcceptInviteViewModel.kt index b032e6d10..8baf7f808 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/manageuser/acceptinvite/AcceptInviteViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/manageuser/acceptinvite/AcceptInviteViewModel.kt @@ -141,4 +141,4 @@ class AcceptInviteViewModel( ) } -} +} \ No newline at end of file diff --git a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/manageuser/profile/SignupViewModel.kt b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/manageuser/profile/SignupViewModel.kt index fe1017888..d10943ebf 100644 --- a/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/manageuser/profile/SignupViewModel.kt +++ b/respect-lib-shared/src/commonMain/kotlin/world/respect/shared/viewmodel/manageuser/profile/SignupViewModel.kt @@ -242,10 +242,9 @@ class SignupViewModel( ) ) ) - } + } } } } } } -