diff --git a/app/src/main/java/com/starry/myne/epub/EpubXMLFileParser.kt b/app/src/main/java/com/starry/myne/epub/EpubXMLFileParser.kt index 7304222e..85b24411 100644 --- a/app/src/main/java/com/starry/myne/epub/EpubXMLFileParser.kt +++ b/app/src/main/java/com/starry/myne/epub/EpubXMLFileParser.kt @@ -83,7 +83,7 @@ class EpubXMLFileParser( "EpubXMLFileParser", "Fragment ID: $fragmentId doesn't represent a
tag. Using the fragment and next fragment logic." ) - // If the fragment ID doesn't represent a tag, use the fragment and next fragment logic + // If the fragment ID doesn't represent a
tag, use the fragment and next fragment logic val fragmentElement = document.selectFirst("#$fragmentId") title = fragmentElement?.selectFirst("h1, h2, h3, h4, h5, h6")?.text() ?: "" val bodyBuilder = StringBuilder() diff --git a/app/src/main/java/com/starry/myne/ui/screens/home/composables/HomeScreen.kt b/app/src/main/java/com/starry/myne/ui/screens/home/composables/HomeScreen.kt index fb2938a7..23cb448f 100644 --- a/app/src/main/java/com/starry/myne/ui/screens/home/composables/HomeScreen.kt +++ b/app/src/main/java/com/starry/myne/ui/screens/home/composables/HomeScreen.kt @@ -58,6 +58,7 @@ import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -82,6 +83,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController +import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import coil.annotation.ExperimentalCoilApi import com.starry.myne.R @@ -92,8 +94,11 @@ import com.starry.myne.ui.common.BookItemCard import com.starry.myne.ui.common.BookLanguageButton import com.starry.myne.ui.common.NetworkError import com.starry.myne.ui.common.ProgressDots +import com.starry.myne.ui.navigation.BottomBarScreen import com.starry.myne.ui.navigation.Screens +import com.starry.myne.ui.screens.home.viewmodels.AllBooksState import com.starry.myne.ui.screens.home.viewmodels.HomeViewModel +import com.starry.myne.ui.screens.home.viewmodels.SearchBarState import com.starry.myne.ui.screens.home.viewmodels.UserAction import com.starry.myne.ui.theme.figeronaFont import com.starry.myne.ui.theme.pacificoFont @@ -117,8 +122,8 @@ fun HomeScreen(navController: NavController, networkStatus: NetworkObserver.Stat */ val sysBackButtonState = remember { mutableStateOf(false) } BackHandler(enabled = sysBackButtonState.value) { - if (viewModel.topBarState.isSearchBarVisible) { - if (viewModel.topBarState.searchText.isNotEmpty()) { + if (viewModel.searchBarState.isSearchBarVisible) { + if (viewModel.searchBarState.searchText.isNotEmpty()) { viewModel.onAction(UserAction.TextFieldInput("", networkStatus)) } else { viewModel.onAction(UserAction.CloseIconClicked) @@ -131,6 +136,18 @@ fun HomeScreen(navController: NavController, networkStatus: NetworkObserver.Stat val modalBottomSheetState = rememberModalBottomSheetState( initialValue = ModalBottomSheetValue.Hidden ) + + // Close search bar when navigating to other screens. + val navBackStackEntry by navController.currentBackStackEntryAsState() + val currentDestination = navBackStackEntry?.destination + LaunchedEffect(currentDestination) { + if (currentDestination?.route != BottomBarScreen.Home.route) { + viewModel.onAction(UserAction.TextFieldInput("", networkStatus)) + viewModel.onAction(UserAction.CloseIconClicked) + } + } + + ModalBottomSheetLayout( sheetState = modalBottomSheetState, sheetShape = RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp), @@ -206,7 +223,7 @@ private fun HomeScreenScaffold( ) { val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current - val topBarState = viewModel.topBarState + val topBarState = viewModel.searchBarState Scaffold( modifier = Modifier @@ -275,7 +292,7 @@ fun HomeScreenContents( navController: NavController, paddingValues: PaddingValues ) { - val topBarState = viewModel.topBarState + val topBarState = viewModel.searchBarState val allBooksState = viewModel.allBooksState @@ -288,119 +305,15 @@ fun HomeScreenContents( // If search text is empty show list of all books. if (topBarState.searchText.isBlank()) { - // show fullscreen progress indicator when loading the first page. - if (allBooksState.page == 1L && allBooksState.isLoading) { - Box( - modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center - ) { - CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) - } - } else if (!allBooksState.isLoading && allBooksState.error != null) { - NetworkError(onRetryClicked = { viewModel.reloadItems() }) - } else { - LazyVerticalGrid( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(start = 8.dp, end = 8.dp), - columns = GridCells.Adaptive(295.dp) - ) { - items(allBooksState.items.size) { i -> - val item = allBooksState.items[i] - if (networkStatus == NetworkObserver.Status.Available - && i >= allBooksState.items.size - 1 - && !allBooksState.endReached - && !allBooksState.isLoading - ) { - viewModel.loadNextItems() - } - Box( - modifier = Modifier - .padding(4.dp) - .fillMaxWidth(), - contentAlignment = Alignment.Center - ) { - BookItemCard( - title = item.title, - author = BookUtils.getAuthorsAsString(item.authors), - language = BookUtils.getLanguagesAsString(item.languages), - subjects = BookUtils.getSubjectsAsString( - item.subjects, 3 - ), - coverImageUrl = item.formats.imagejpeg - ) { - navController.navigate( - Screens.BookDetailScreen.withBookId( - item.id.toString() - ) - ) - } - } - - } - item { - if (allBooksState.isLoading) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - horizontalArrangement = Arrangement.Center - ) { - ProgressDots() - } - } - } - } - } - - // Else show the search results. + AllBooksList( + allBooksState = allBooksState, + networkStatus = networkStatus, + navController = navController, + onRetryClicked = { viewModel.reloadItems() }, + onLoadNextItems = { viewModel.loadNextItems() } + ) } else { - LazyVerticalGrid( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background) - .padding(start = 8.dp, end = 8.dp), - columns = GridCells.Adaptive(295.dp) - ) { - if (topBarState.isSearching) { - item { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - horizontalArrangement = Arrangement.Center - ) { - ProgressDots() - } - } - } - - items(topBarState.searchResults.size) { i -> - val item = topBarState.searchResults[i] - Box( - modifier = Modifier - .padding(4.dp) - .fillMaxWidth(), - contentAlignment = Alignment.Center - ) { - BookItemCard( - title = item.title, - author = BookUtils.getAuthorsAsString(item.authors), - language = BookUtils.getLanguagesAsString(item.languages), - subjects = BookUtils.getSubjectsAsString( - item.subjects, 3 - ), - coverImageUrl = item.formats.imagejpeg - ) { - navController.navigate( - Screens.BookDetailScreen.withBookId( - item.id.toString() - ) - ) - } - } - } - } + SearchBookList(searchBarState = topBarState, navController = navController) } } @@ -446,6 +359,130 @@ private fun HomeTopAppBar( } } +@Composable +private fun AllBooksList( + allBooksState: AllBooksState, + networkStatus: NetworkObserver.Status, + navController: NavController, + onRetryClicked: () -> Unit, + onLoadNextItems: () -> Unit +) { + // show fullscreen progress indicator when loading the first page. + if (allBooksState.page == 1L && allBooksState.isLoading) { + Box( + modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) + } + } else if (!allBooksState.isLoading && allBooksState.error != null) { + NetworkError(onRetryClicked = { onRetryClicked() }) + } else { + LazyVerticalGrid( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(start = 8.dp, end = 8.dp), + columns = GridCells.Adaptive(295.dp) + ) { + items(allBooksState.items.size) { i -> + val item = allBooksState.items[i] + if (networkStatus == NetworkObserver.Status.Available + && i >= allBooksState.items.size - 1 + && !allBooksState.endReached + && !allBooksState.isLoading + ) { + onLoadNextItems() + } + Box( + modifier = Modifier + .padding(4.dp) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + BookItemCard( + title = item.title, + author = BookUtils.getAuthorsAsString(item.authors), + language = BookUtils.getLanguagesAsString(item.languages), + subjects = BookUtils.getSubjectsAsString( + item.subjects, 3 + ), + coverImageUrl = item.formats.imagejpeg + ) { + navController.navigate( + Screens.BookDetailScreen.withBookId( + item.id.toString() + ) + ) + } + } + + } + item { + if (allBooksState.isLoading) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.Center + ) { + ProgressDots() + } + } + } + } + } +} + +@Composable +private fun SearchBookList(searchBarState: SearchBarState, navController: NavController) { + LazyVerticalGrid( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .padding(start = 8.dp, end = 8.dp), + columns = GridCells.Adaptive(295.dp) + ) { + if (searchBarState.isSearching) { + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalArrangement = Arrangement.Center + ) { + ProgressDots() + } + } + } + + items(searchBarState.searchResults.size) { i -> + val item = searchBarState.searchResults[i] + Box( + modifier = Modifier + .padding(4.dp) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + BookItemCard( + title = item.title, + author = BookUtils.getAuthorsAsString(item.authors), + language = BookUtils.getLanguagesAsString(item.languages), + subjects = BookUtils.getSubjectsAsString( + item.subjects, 3 + ), + coverImageUrl = item.formats.imagejpeg + ) { + navController.navigate( + Screens.BookDetailScreen.withBookId( + item.id.toString() + ) + ) + } + } + } + } +} + @ExperimentalMaterial3Api @Composable private fun SearchAppBar( diff --git a/app/src/main/java/com/starry/myne/ui/screens/home/viewmodels/HomeViewModel.kt b/app/src/main/java/com/starry/myne/ui/screens/home/viewmodels/HomeViewModel.kt index ca868748..035026a3 100644 --- a/app/src/main/java/com/starry/myne/ui/screens/home/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/com/starry/myne/ui/screens/home/viewmodels/HomeViewModel.kt @@ -45,7 +45,7 @@ data class AllBooksState( val page: Long = 1L ) -data class TopBarState( +data class SearchBarState( val searchText: String = "", val isSearchBarVisible: Boolean = false, val isSortMenuVisible: Boolean = false, @@ -70,7 +70,7 @@ class HomeViewModel @Inject constructor( private val preferenceUtil: PreferenceUtil ) : ViewModel() { var allBooksState by mutableStateOf(AllBooksState()) - var topBarState by mutableStateOf(TopBarState()) + var searchBarState by mutableStateOf(SearchBarState()) private val _language: MutableState = mutableStateOf(getPreferredLanguage()) val language: State = _language @@ -129,20 +129,20 @@ class HomeViewModel @Inject constructor( fun onAction(userAction: UserAction) { when (userAction) { UserAction.CloseIconClicked -> { - topBarState = topBarState.copy(isSearchBarVisible = false) + searchBarState = searchBarState.copy(isSearchBarVisible = false) } UserAction.SearchIconClicked -> { - topBarState = topBarState.copy(isSearchBarVisible = true) + searchBarState = searchBarState.copy(isSearchBarVisible = true) } is UserAction.TextFieldInput -> { - topBarState = topBarState.copy(searchText = userAction.text) + searchBarState = searchBarState.copy(searchText = userAction.text) if (userAction.networkStatus == NetworkObserver.Status.Available) { searchJob?.cancel() searchJob = viewModelScope.launch { if (userAction.text.isNotBlank()) { - topBarState = topBarState.copy(isSearching = true) + searchBarState = searchBarState.copy(isSearching = true) } delay(500L) searchBooks(userAction.text) @@ -159,7 +159,7 @@ class HomeViewModel @Inject constructor( private suspend fun searchBooks(query: String) { val bookSet = bookAPI.searchBooks(query) val books = bookSet.getOrNull()!!.books.filter { it.formats.applicationepubzip != null } - topBarState = topBarState.copy(searchResults = books, isSearching = false) + searchBarState = searchBarState.copy(searchResults = books, isSearching = false) } private fun changeLanguage(language: BookLanguage) { diff --git a/app/src/main/java/com/starry/myne/ui/screens/main/MainScreen.kt b/app/src/main/java/com/starry/myne/ui/screens/main/MainScreen.kt index a77788fb..b092c30b 100644 --- a/app/src/main/java/com/starry/myne/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/com/starry/myne/ui/screens/main/MainScreen.kt @@ -138,6 +138,7 @@ private fun BottomBar( ) { navController.navigate(screen.route) { popUpTo(navController.graph.findStartDestination().id) + launchSingleTop = true } } } diff --git a/app/src/main/java/com/starry/myne/ui/screens/reader/activities/ReaderActivity.kt b/app/src/main/java/com/starry/myne/ui/screens/reader/activities/ReaderActivity.kt index 919c6591..f63dbbab 100644 --- a/app/src/main/java/com/starry/myne/ui/screens/reader/activities/ReaderActivity.kt +++ b/app/src/main/java/com/starry/myne/ui/screens/reader/activities/ReaderActivity.kt @@ -52,8 +52,15 @@ object ReaderConstants { } +/** + * Data class to hold intent information for ReaderActivity. + * + * @param libraryItemId Library item id. + * @param chapterIndex Chapter index. + * @param isExternalFile Is book opened from external file. + */ data class IntentData( - val libraryItemId: Int?, val chapterIndex: Int?, val isExternalBook: Boolean + val libraryItemId: Int?, val chapterIndex: Int?, val isExternalFile: Boolean ) @AndroidEntryPoint @@ -119,7 +126,7 @@ class ReaderActivity : AppCompatActivity() { // If book was not opened from external epub file, update the // reading progress into the database. - if (!intentData.isExternalBook) { + if (!intentData.isExternalFile) { viewModel.updateReaderProgress( // Book ID is not null here since we are not opening // an external book. @@ -165,12 +172,11 @@ fun handleIntent( val chapterIndex = intent.extras?.getInt( ReaderConstants.EXTRA_CHAPTER_IDX, ReaderConstants.DEFAULT_NONE ) - val isExternalBook = intent.type == "application/epub+zip" + val isExternalFile = intent.type == "application/epub+zip" // Internal book if (libraryItemId != null && libraryItemId != ReaderConstants.DEFAULT_NONE) { - // Load epub book from given id and set chapters as items in - // reader's recycler view adapter. + // Load epub book from library. viewModel.loadEpubBook(libraryItemId = libraryItemId, onLoaded = { // if there is saved progress for this book, then scroll to // last page at exact position were used had left. @@ -184,18 +190,19 @@ fun handleIntent( scrollToPosition(chapterIndex, 0) } - // External book. - } else if (isExternalBook) { + + } else if (isExternalFile) { + // External book from file. intent.data?.let { contentResolver.openInputStream(it)?.let { ips -> viewModel.loadEpubBookExternal(ips as FileInputStream) } } } else { - onError() // If no book id is provided, then show error. + onError() // Invalid intent. } - return IntentData(libraryItemId, chapterIndex, isExternalBook) + return IntentData(libraryItemId, chapterIndex, isExternalFile) } /**