diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4b5e05db..52eefaa9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -15,7 +15,7 @@ android { minSdk = 26 targetSdk = 34 versionCode = 1 - versionName = "0.9.0" + versionName = "0.9.2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -139,4 +139,6 @@ dependencies { implementation("androidx.appcompat:appcompat:latest.release") implementation("androidx.appcompat:appcompat-resources:latest.release") + + implementation("io.coil-kt:coil-compose:2.6.0") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7d1dab8b..7c9c53e6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -35,7 +35,6 @@ diff --git a/app/src/main/java/ua/acclorite/book_story/Activity.kt b/app/src/main/java/ua/acclorite/book_story/Activity.kt index b8ccfa15..a44d066a 100644 --- a/app/src/main/java/ua/acclorite/book_story/Activity.kt +++ b/app/src/main/java/ua/acclorite/book_story/Activity.kt @@ -1,15 +1,13 @@ package ua.acclorite.book_story import android.annotation.SuppressLint -import android.content.res.Configuration import android.database.CursorWindow import android.os.Bundle -import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -28,7 +26,6 @@ import dagger.hilt.android.AndroidEntryPoint import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.presentation.components.bottom_navigation_bar.BottomNavigationBar import ua.acclorite.book_story.presentation.components.custom_navigation_rail.CustomNavigationRail -import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainViewModel import ua.acclorite.book_story.presentation.data.NavigationHost import ua.acclorite.book_story.presentation.data.Screen @@ -55,7 +52,7 @@ import java.lang.reflect.Field @SuppressLint("DiscouragedPrivateApi") @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) @AndroidEntryPoint -class Activity : ComponentActivity() { +class Activity : AppCompatActivity() { private val mainViewModel: MainViewModel by viewModels() private val libraryViewModel: LibraryViewModel by viewModels() private val historyViewModel: HistoryViewModel by viewModels() @@ -75,10 +72,7 @@ class Activity : ComponentActivity() { } // Initializing all variables - mainViewModel.init( - activity = this, - libraryViewModel, - ) + mainViewModel.init(libraryViewModel) // Splash screen installSplashScreen().apply { @@ -93,7 +87,6 @@ class Activity : ComponentActivity() { setContent { val windowClass = calculateWindowSizeClass(activity = this) - val updating = mainViewModel.updating.collectAsState().value val theme = mainViewModel.theme.collectAsState().value ?: Theme.BLUE val darkTheme = @@ -104,157 +97,142 @@ class Activity : ComponentActivity() { theme = theme, isDark = darkTheme.isDark() ) { - if (!updating) { - NavigationHost(startScreen = Screen.LIBRARY) { - val currentScreen by this.getCurrentScreen().collectAsState() + NavigationHost(startScreen = Screen.LIBRARY) { + val currentScreen by this.getCurrentScreen().collectAsState() - AnimatedVisibility( - visible = currentScreen == Screen.LIBRARY || - currentScreen == Screen.HISTORY || - currentScreen == Screen.BROWSE, - enter = Transitions.BackSlidingTransitionIn, - exit = Transitions.SlidingTransitionOut + AnimatedVisibility( + visible = currentScreen == Screen.LIBRARY || + currentScreen == Screen.HISTORY || + currentScreen == Screen.BROWSE, + enter = Transitions.BackSlidingTransitionIn, + exit = Transitions.SlidingTransitionOut + ) { + Scaffold( + bottomBar = { + if (!tabletUI) { + BottomNavigationBar(navigator = this@NavigationHost) + } + }, + containerColor = MaterialTheme.colorScheme.surface ) { - Scaffold( - bottomBar = { - if (!tabletUI) { - BottomNavigationBar(navigator = this@NavigationHost) - } - }, - containerColor = MaterialTheme.colorScheme.surface + Box( + modifier = Modifier + .fillMaxSize() + .padding( + start = if (tabletUI) 80.dp else 0.dp, + bottom = it.calculateBottomPadding() + ) ) { - Box( - modifier = Modifier - .fillMaxSize() - .padding( - start = if (tabletUI) 80.dp else 0.dp, - bottom = it.calculateBottomPadding() - ) - ) { - composable(screen = Screen.LIBRARY) { - @Suppress("UNCHECKED_CAST") - LibraryScreen( - viewModel = libraryViewModel, - historyViewModel = historyViewModel, - browseViewModel = browseViewModel, - navigator = this@NavigationHost, - addedBooks = retrieveArgument("added_books") as? List - ?: emptyList() - ) - } - - composable(screen = Screen.HISTORY) { - HistoryScreen( - viewModel = historyViewModel, - libraryViewModel = libraryViewModel, - navigator = this@NavigationHost - ) - } - - composable(screen = Screen.BROWSE) { - BrowseScreen( - viewModel = browseViewModel, - libraryViewModel = libraryViewModel, - navigator = this@NavigationHost - ) - } + composable(screen = Screen.LIBRARY) { + @Suppress("UNCHECKED_CAST") + LibraryScreen( + viewModel = libraryViewModel, + historyViewModel = historyViewModel, + browseViewModel = browseViewModel, + navigator = this@NavigationHost, + addedBooks = retrieveArgument("added_books") as? List + ?: emptyList() + ) } - if (tabletUI) { - CustomNavigationRail(navigator = this@NavigationHost) + composable(screen = Screen.HISTORY) { + HistoryScreen( + viewModel = historyViewModel, + libraryViewModel = libraryViewModel, + navigator = this@NavigationHost + ) + } + + composable(screen = Screen.BROWSE) { + BrowseScreen( + viewModel = browseViewModel, + libraryViewModel = libraryViewModel, + navigator = this@NavigationHost + ) } } - } - // Book Info - composable( - screen = Screen.BOOK_INFO, - enterAnim = Transitions.SlidingTransitionIn, - exitAnim = Transitions.SlidingTransitionOut - ) { - BookInfoScreen( - libraryViewModel = libraryViewModel, - browseViewModel = browseViewModel, - historyViewModel = historyViewModel, - navigator = this@NavigationHost - ) - } - composable( - screen = Screen.READER, - enterAnim = Transitions.SlidingTransitionIn, - exitAnim = Transitions.SlidingTransitionOut - ) { - ReaderScreen( - mainViewModel = mainViewModel, - libraryViewModel = libraryViewModel, - historyViewModel = historyViewModel, - navigator = this@NavigationHost - ) + if (tabletUI) { + CustomNavigationRail(navigator = this@NavigationHost) + } } + } - // Settings - composable( - screen = Screen.SETTINGS, - enterAnim = Transitions.SlidingTransitionIn, - exitAnim = Transitions.SlidingTransitionOut - ) { - SettingsScreen( - navigator = this@NavigationHost - ) - } + // Book Info + composable( + screen = Screen.BOOK_INFO, + enterAnim = Transitions.SlidingTransitionIn, + exitAnim = Transitions.SlidingTransitionOut + ) { + BookInfoScreen( + libraryViewModel = libraryViewModel, + browseViewModel = browseViewModel, + historyViewModel = historyViewModel, + navigator = this@NavigationHost + ) + } + composable( + screen = Screen.READER, + enterAnim = Transitions.SlidingTransitionIn, + exitAnim = Transitions.SlidingTransitionOut + ) { + ReaderScreen( + mainViewModel = mainViewModel, + libraryViewModel = libraryViewModel, + historyViewModel = historyViewModel, + navigator = this@NavigationHost + ) + } - // Nested categories - composable( - screen = Screen.GENERAL_SETTINGS, - enterAnim = Transitions.SlidingTransitionIn, - exitAnim = Transitions.SlidingTransitionOut - ) { - GeneralSettings( - mainViewModel = mainViewModel, - navigator = this@NavigationHost - ) - } - composable( - screen = Screen.APPEARANCE_SETTINGS, - enterAnim = Transitions.SlidingTransitionIn, - exitAnim = Transitions.SlidingTransitionOut - ) { - AppearanceSettings( - mainViewModel = mainViewModel, - navigator = this@NavigationHost - ) - } - composable( - screen = Screen.READER_SETTINGS, - enterAnim = Transitions.SlidingTransitionIn, - exitAnim = Transitions.SlidingTransitionOut - ) { - ReaderSettings( - mainViewModel = mainViewModel, - navigator = this@NavigationHost - ) - } + // Settings + composable( + screen = Screen.SETTINGS, + enterAnim = Transitions.SlidingTransitionIn, + exitAnim = Transitions.SlidingTransitionOut + ) { + SettingsScreen( + navigator = this@NavigationHost + ) + } + + // Nested categories + composable( + screen = Screen.GENERAL_SETTINGS, + enterAnim = Transitions.SlidingTransitionIn, + exitAnim = Transitions.SlidingTransitionOut + ) { + GeneralSettings( + mainViewModel = mainViewModel, + navigator = this@NavigationHost + ) + } + composable( + screen = Screen.APPEARANCE_SETTINGS, + enterAnim = Transitions.SlidingTransitionIn, + exitAnim = Transitions.SlidingTransitionOut + ) { + AppearanceSettings( + mainViewModel = mainViewModel, + navigator = this@NavigationHost + ) + } + composable( + screen = Screen.READER_SETTINGS, + enterAnim = Transitions.SlidingTransitionIn, + exitAnim = Transitions.SlidingTransitionOut + ) { + ReaderSettings( + mainViewModel = mainViewModel, + navigator = this@NavigationHost + ) + } // Start screen (later) // composable(screen = Screen.START) { // StartScreen() // } - } - } else { - Box( - modifier = Modifier - .fillMaxSize() - .background( - MaterialTheme.colorScheme.surface - ) - ) } } } } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - mainViewModel.onEvent(MainEvent.OnLocaleUpdate(this)) - } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapperImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapperImpl.kt index f314e27d..5d54ec6f 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapperImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/book/BookMapperImpl.kt @@ -9,7 +9,6 @@ import androidx.compose.ui.graphics.asImageBitmap import ua.acclorite.book_story.R import ua.acclorite.book_story.data.local.dto.BookEntity import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.domain.model.StringWithId import ua.acclorite.book_story.util.UIText import java.io.ByteArrayOutputStream import java.io.File @@ -22,12 +21,12 @@ class BookMapperImpl @Inject constructor() : BookMapper { val stream = ByteArrayOutputStream() book.coverImage?.asAndroidBitmap()?.compress( if (legacyAPI) Bitmap.CompressFormat.WEBP - else Bitmap.CompressFormat.WEBP_LOSSLESS, - if (legacyAPI) 30 else 100, + else Bitmap.CompressFormat.WEBP_LOSSY, + 0, stream ) - val text = book.text.joinToString("\n") { + val textAsString = book.text.joinToString("\n") { it.line.trim() } @@ -37,7 +36,7 @@ class BookMapperImpl @Inject constructor() : BookMapper { filePath = book.filePath, progress = book.progress, author = book.author.string, - text = text, + text = textAsString, description = book.description, image = stream.toByteArray(), category = book.category @@ -64,7 +63,7 @@ class BookMapperImpl @Inject constructor() : BookMapper { description = bookEntity.description, progress = bookEntity.progress, file = if (file.exists()) file else null, - text = bookEntity.text.split("\n").map { StringWithId(it) }, + text = emptyList(), filePath = bookEntity.filePath, lastOpened = null, category = bookEntity.category, diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt index 482745c7..1d2493ee 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubFileParser.kt @@ -37,11 +37,13 @@ class EpubFileParser @Inject constructor() : FileParser { } if (description != null) { - if (description.isEmpty()) { + if (description.isBlank()) { description = null } } + + return Book( id = null, title = title, diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt index 35688701..dfbdb7af 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/epub/EpubTextParser.kt @@ -32,15 +32,27 @@ class EpubTextParser @Inject constructor() : TextParser { for (spineReference in book.spine.spineReferences) { val resource = spineReference.resource val inputStream = resource.inputStream - val reader = - BufferedReader(InputStreamReader(inputStream, Charset.forName("UTF-8"))) + val reader = BufferedReader( + InputStreamReader( + inputStream, + Charset.forName("UTF-8") + ) + ) var line: String? + withContext(Dispatchers.IO) { + inputStream.close() + } + while (withContext(Dispatchers.IO) { reader.readLine() }.also { line = it } != null) { unformattedText.append(line).append("\n") } + + withContext(Dispatchers.IO) { + reader.close() + } } val stringWithIds = mutableListOf() diff --git a/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt index 915605c7..faa10015 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt @@ -90,6 +90,11 @@ class BookRepositoryImpl @Inject constructor( return database.findBooksById(ids).map { bookMapper.toBook(it) } } + override suspend fun getBookTextById(bookId: Int): String { + val book = database.findBookById(bookId) + return book.text + } + override suspend fun findBook( id: Int ): BookEntity { @@ -101,11 +106,39 @@ class BookRepositoryImpl @Inject constructor( } override suspend fun updateBooks(books: List) { + // without text + database.updateBooks( + books.map { + val book = database.findBookById(it.id ?: return) + bookMapper.toBookEntity( + it.copy( + text = book.text + .split("\n") + .map { line -> StringWithId(line.trim()) } + ) + ) + } + ) + } + + override suspend fun updateBooksWithText(books: List) { database.updateBooks(books.map { bookMapper.toBookEntity(it) }) } override suspend fun deleteBooks(books: List) { - database.deleteBooks(books.map { bookMapper.toBookEntity(it) }) + // without text + database.deleteBooks( + books.map { + val book = database.findBookById(it.id ?: return) + bookMapper.toBookEntity( + it.copy( + text = book.text + .split("\n") + .map { line -> StringWithId(line.trim()) } + ) + ) + } + ) } override suspend fun retrieveDataFromDataStore( @@ -119,7 +152,7 @@ class BookRepositoryImpl @Inject constructor( dataStore.putData(key, value) } - override suspend fun getFilesFromDownloads(query: String): Flow>> { + override suspend fun getFilesFromDevice(query: String): Flow>> { fun getAllFilesInDirectory(directory: File): List { val filesList = mutableListOf() @@ -140,20 +173,15 @@ class BookRepositoryImpl @Inject constructor( } return flow { - emit(Resource.Loading(true)) - val existingBooks = database .searchBooks("") .map { bookMapper.toBook(it) } - .filter { it.file != null } - val allFiles = getAllFilesInDirectory( - Environment - .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) - ) + val primaryDirectory = Environment.getExternalStorageDirectory() + val allFiles = getAllFilesInDirectory(primaryDirectory) if (allFiles.isEmpty()) { - emit(Resource.Loading(false)) + emit(Resource.Success(null)) return@flow } @@ -168,7 +196,8 @@ class BookRepositoryImpl @Inject constructor( ) } val isFileAlreadyAdded = existingBooks.all { - it.file != file + it.filePath.substringAfterLast("/") != + file.path.substringAfterLast("/") } val isQuery = if (query.isEmpty()) true else file.name.lowercase() .contains(query.trim().lowercase()) @@ -193,7 +222,6 @@ class BookRepositoryImpl @Inject constructor( }.toMutableList() } - emit(Resource.Loading(false)) emit( Resource.Success( data = filteredFiles diff --git a/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt b/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt index edf27f7d..30b09407 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt @@ -24,6 +24,10 @@ interface BookRepository { ids: List ): List + suspend fun getBookTextById( + bookId: Int + ): String + suspend fun findBook( id: Int ): BookEntity @@ -36,6 +40,10 @@ interface BookRepository { books: List ) + suspend fun updateBooksWithText( + books: List + ) + suspend fun deleteBooks( books: List ) @@ -50,7 +58,7 @@ interface BookRepository { value: T ) - suspend fun getFilesFromDownloads(query: String = ""): Flow>> + suspend fun getFilesFromDevice(query: String = ""): Flow>> suspend fun getBookTextFromFile(file: File): Flow>> diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/ChangeLanguage.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/ChangeLanguage.kt index 99341fcc..f9c4ef1e 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/ChangeLanguage.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/ChangeLanguage.kt @@ -1,27 +1,17 @@ package ua.acclorite.book_story.domain.use_case import android.annotation.SuppressLint -import androidx.activity.ComponentActivity +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.os.LocaleListCompat import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.util.DataStoreConstants -import java.util.Locale import javax.inject.Inject class ChangeLanguage @Inject constructor(private val repository: BookRepository) { @SuppressLint("AppBundleLocaleChanges") - suspend fun execute(language: String, activity: ComponentActivity) { - val config = activity.resources.configuration - val resources = activity.resources - val locale = Locale(language) - - Locale.setDefault(locale) - config.setLocale(locale) - - activity.createConfigurationContext(config) - resources.updateConfiguration( - config, - resources.displayMetrics - ) + suspend fun execute(language: String) { + val appLocale = LocaleListCompat.forLanguageTags(language) + AppCompatDelegate.setApplicationLocales(appLocale) repository.putDataToDataStore( DataStoreConstants.LANGUAGE, diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetFilesFromDownloads.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetFilesFromDevice.kt similarity index 77% rename from app/src/main/java/ua/acclorite/book_story/domain/use_case/GetFilesFromDownloads.kt rename to app/src/main/java/ua/acclorite/book_story/domain/use_case/GetFilesFromDevice.kt index b2d15ef7..2afd126e 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetFilesFromDownloads.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetFilesFromDevice.kt @@ -6,11 +6,11 @@ import ua.acclorite.book_story.util.Resource import java.io.File import javax.inject.Inject -class GetFilesFromDownloads @Inject constructor( +class GetFilesFromDevice @Inject constructor( private val repository: BookRepository ) { suspend fun execute(query: String): Flow>> { - return repository.getFilesFromDownloads(query) + return repository.getFilesFromDevice(query) } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetText.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetText.kt index 7bfab049..3b84f33f 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetText.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetText.kt @@ -1,15 +1,11 @@ package ua.acclorite.book_story.domain.use_case -import kotlinx.coroutines.flow.Flow -import ua.acclorite.book_story.domain.model.StringWithId import ua.acclorite.book_story.domain.repository.BookRepository -import ua.acclorite.book_story.util.Resource -import java.io.File import javax.inject.Inject class GetText @Inject constructor(private val repository: BookRepository) { - suspend fun execute(file: File): Flow>> { - return repository.getBookTextFromFile(file) + suspend fun execute(id: Int): String { + return repository.getBookTextById(bookId = id) } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/UpdateBooksWithText.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/UpdateBooksWithText.kt new file mode 100644 index 00000000..6aad3df0 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/UpdateBooksWithText.kt @@ -0,0 +1,12 @@ +package ua.acclorite.book_story.domain.use_case + +import ua.acclorite.book_story.domain.model.Book +import ua.acclorite.book_story.domain.repository.BookRepository +import javax.inject.Inject + +class UpdateBooksWithText @Inject constructor(private val repository: BookRepository) { + + suspend fun execute(books: List) { + repository.updateBooksWithText(books) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/AnimatedTopAppBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/AnimatedTopAppBar.kt index f68d490c..b1322265 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/AnimatedTopAppBar.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/AnimatedTopAppBar.kt @@ -1,8 +1,11 @@ package ua.acclorite.book_story.presentation.components +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.RowScope @@ -22,8 +25,9 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import ua.acclorite.book_story.ui.DefaultTransition +import ua.acclorite.book_story.R import ua.acclorite.book_story.ui.elevation /** @@ -38,7 +42,7 @@ import ua.acclorite.book_story.ui.elevation @Composable fun AnimatedTopAppBar( containerColor: Color = MaterialTheme.colorScheme.surface, - scrolledContainerColor: Color? = MaterialTheme.elevation(), + scrolledContainerColor: Color = MaterialTheme.elevation(), scrollBehavior: TopAppBarScrollBehavior?, isTopBarScrolled: Boolean?, @@ -58,79 +62,90 @@ fun AnimatedTopAppBar( content3Title: @Composable () -> Unit = {}, content3Actions: @Composable RowScope.() -> Unit = {} ) { - //todo fix lags. Box(modifier = Modifier.fillMaxWidth()) { - if ((scrollBehavior != null || isTopBarScrolled != null) && scrolledContainerColor != null) { - - val fraction by remember { - derivedStateOf { - scrollBehavior?.state?.overlappedFraction ?: 0f - } - } - - val isScrolled = if (scrollBehavior != null) { - fraction > 0.01f - } else isTopBarScrolled ?: false - - val color = if (isScrolled) scrolledContainerColor else containerColor - val animatedColor by animateColorAsState( - targetValue = color, - animationSpec = spring(stiffness = Spring.StiffnessMediumLow), - label = "TopAppBar color animation" - ) - - val statusBarPadding = with(LocalDensity.current) { - WindowInsets.statusBars.getTop(LocalDensity.current).toDp() - } + val density = LocalDensity.current + val statusBarPadding = with(density) { + WindowInsets.statusBars.getTop(density).toDp() + } + if (containerColor != Color.Transparent) { Box( modifier = Modifier .fillMaxWidth() .height(64.dp + statusBarPadding) .background( - animatedColor + if (isTopBarScrolled == true) scrolledContainerColor + else containerColor ) ) } - DefaultTransition(visible = content1Visibility ?: true) { + val scrollableContainerColor by remember(isTopBarScrolled) { + derivedStateOf { + if (isTopBarScrolled == true) { + scrolledContainerColor + } else { + containerColor + } + } + } + + val animatedContainerColor by animateColorAsState( + targetValue = scrollableContainerColor, + animationSpec = spring(stiffness = Spring.StiffnessMediumLow), + label = stringResource(id = R.string.top_app_bar_anim_content_desc) + ) + + AnimatedVisibility( + visible = content1Visibility ?: true, + enter = fadeIn(spring(stiffness = Spring.StiffnessMediumLow)), + exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow)) + ) { TopAppBar( navigationIcon = content1NavigationIcon, title = content1Title, actions = content1Actions, scrollBehavior = scrollBehavior, colors = TopAppBarDefaults.topAppBarColors( - containerColor = Color.Transparent, - scrolledContainerColor = Color.Transparent + containerColor = animatedContainerColor, + scrolledContainerColor = scrolledContainerColor ) ) } if (content2Visibility != null) { - DefaultTransition(visible = content2Visibility) { + AnimatedVisibility( + visible = content2Visibility, + enter = fadeIn(spring(stiffness = Spring.StiffnessMediumLow)), + exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow)) + ) { TopAppBar( navigationIcon = content2NavigationIcon, title = content2Title, actions = content2Actions, scrollBehavior = scrollBehavior, colors = TopAppBarDefaults.topAppBarColors( - containerColor = Color.Transparent, - scrolledContainerColor = Color.Transparent + containerColor = animatedContainerColor, + scrolledContainerColor = scrolledContainerColor ) ) } } if (content3Visibility != null) { - DefaultTransition(visible = content3Visibility) { + AnimatedVisibility( + visible = content3Visibility, + enter = fadeIn(spring(stiffness = Spring.StiffnessMediumLow)), + exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow)) + ) { TopAppBar( navigationIcon = content3NavigationIcon, title = content3Title, actions = content3Actions, scrollBehavior = scrollBehavior, colors = TopAppBarDefaults.topAppBarColors( - containerColor = Color.Transparent, - scrolledContainerColor = Color.Transparent + containerColor = animatedContainerColor, + scrolledContainerColor = scrolledContainerColor ) ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomCheckbox.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomCheckbox.kt index b63df96b..3c2cc67a 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomCheckbox.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomCheckbox.kt @@ -13,14 +13,17 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R import ua.acclorite.book_story.ui.elevation /** * Custom Checkbox. Has a Circle shape. */ @Composable -fun CustomCheckbox(selected: Boolean) { +fun CustomCheckbox(selected: Boolean, size: Dp = 22.dp) { Icon( imageVector = Icons.Default.Check, tint = if (selected) MaterialTheme.elevation(elevation = 2.dp) else Color.Transparent, @@ -36,7 +39,7 @@ fun CustomCheckbox(selected: Boolean) { shape = CircleShape ) .padding(4.dp) - .size(22.dp), - contentDescription = "checkbox" + .size(size), + contentDescription = stringResource(id = R.string.checkbox_content_desc) ) } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomIconButton.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomIconButton.kt new file mode 100644 index 00000000..69604482 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomIconButton.kt @@ -0,0 +1,45 @@ +package ua.acclorite.book_story.presentation.components + +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector + +/** + * Custom Icon Button. Can be disabled after click, so user can't press button fast 2 or 3 times. + */ +@Composable +fun CustomIconButton( + modifier: Modifier = Modifier, + icon: ImageVector, + contentDescription: String, + disableOnClick: Boolean, + enabled: Boolean = true, + color: Color = LocalContentColor.current, + onClick: () -> Unit +) { + var isClicked by remember { mutableStateOf(false) } + IconButton( + enabled = enabled && !isClicked, + onClick = { + if (disableOnClick) { + isClicked = true + } + onClick() + } + ) { + Icon( + imageVector = icon, + modifier = modifier, + contentDescription = contentDescription, + tint = color + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSelectionContainer.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSelectionContainer.kt index e67b4435..39577ccc 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSelectionContainer.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomSelectionContainer.kt @@ -127,7 +127,6 @@ private class CustomSelectionToolbar( context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager override var status: TextToolbarStatus by mutableStateOf(TextToolbarStatus.Hidden) - private set override fun showMenu( rect: Rect, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/GoBackButton.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/GoBackButton.kt index 70afc399..c11e8360 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/GoBackButton.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/GoBackButton.kt @@ -2,14 +2,7 @@ package ua.acclorite.book_story.presentation.components import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.data.Navigator @@ -19,20 +12,12 @@ import ua.acclorite.book_story.presentation.data.Navigator */ @Composable fun GoBackButton(navigator: Navigator, customOnClick: () -> Unit = {}) { - var isClicked by remember { mutableStateOf(false) } - - IconButton( - enabled = !isClicked, - onClick = { - isClicked = true - navigator.navigateBack() - customOnClick() - } + CustomIconButton( + icon = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(id = R.string.go_back_content_desc), + disableOnClick = true ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(id = R.string.go_back_content_desc), - tint = MaterialTheme.colorScheme.onSurface - ) + navigator.navigateBack() + customOnClick() } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/MoreDropDown.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/MoreDropDown.kt index f71eb974..877ff562 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/MoreDropDown.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/MoreDropDown.kt @@ -2,13 +2,10 @@ package ua.acclorite.book_story.presentation.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -16,7 +13,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp @@ -31,19 +27,17 @@ import ua.acclorite.book_story.presentation.data.Screen fun MoreDropDown(navigator: Navigator) { var showDropDown by remember { mutableStateOf(false) } - val startPadding = 12.dp - val endPadding = 36.dp + val startPadding = remember { 12.dp } + val endPadding = remember { 36.dp } Box { - IconButton(onClick = { + CustomIconButton( + icon = Icons.Default.MoreVert, + contentDescription = stringResource(id = R.string.show_dropdown_content_desc), + disableOnClick = false, + enabled = !showDropDown + ) { showDropDown = true - }) { - Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = "Show drop down", - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) } DropdownMenu( @@ -51,7 +45,6 @@ fun MoreDropDown(navigator: Navigator) { onDismissRequest = { showDropDown = false }, offset = DpOffset(10.dp, 0.dp) ) { - DropdownMenuItem( text = { Text( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationBar.kt index e2cb079d..f154b5d1 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationBar.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationBar.kt @@ -18,6 +18,8 @@ import ua.acclorite.book_story.ui.elevation /** * Bottom navigation bar, uses default [NavigationBar]. + * + * @param navigator Navigator. */ @Composable fun BottomNavigationBar( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationItem.kt index eda6f49f..4ab1c43c 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationItem.kt @@ -8,6 +8,7 @@ import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -23,6 +24,11 @@ fun RowScope.BottomNavigationBarItem( isSelected: Boolean, onClick: () -> Unit ) { + val icon = remember(isSelected) { + if (isSelected) item.selectedIcon + else item.unselectedIcon + } + NavigationBarItem( label = { Text( @@ -37,9 +43,7 @@ fun RowScope.BottomNavigationBarItem( onClick = { onClick() }, icon = { Icon( - painter = - if (isSelected) item.selectedIcon - else item.unselectedIcon, + painter = icon, contentDescription = item.title, modifier = Modifier.size(24.dp) ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithContent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithContent.kt index 0fc92a81..cba44a90 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithContent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithContent.kt @@ -20,6 +20,10 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +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.draw.clip @@ -69,6 +73,7 @@ fun CustomDialogWithContent( withDivider: Boolean, customContent: @Composable (ColumnScope.() -> Unit) = {} ) { + var actionClicked by remember { mutableStateOf(false) } AlertDialog( onDismissRequest = { onDismiss() }, properties = properties @@ -151,7 +156,13 @@ fun CustomDialogWithContent( .align(Alignment.End) .padding(horizontal = 24.dp) ) { - TextButton(onClick = { onDismiss() }) { + TextButton( + onClick = { + actionClicked = true + onDismiss() + }, + enabled = !actionClicked + ) { Text( text = stringResource(id = R.string.cancel), style = MaterialTheme.typography.labelLarge, @@ -161,8 +172,11 @@ fun CustomDialogWithContent( if (actionText != null) { Spacer(modifier = Modifier.width(4.dp)) TextButton( - onClick = { onAction() }, - enabled = isActionEnabled == true + onClick = { + actionClicked = true + onAction() + }, + enabled = isActionEnabled == true && !actionClicked ) { Text( text = actionText, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithLazyColumn.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithLazyColumn.kt index 0e7ea033..cce7512f 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithLazyColumn.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_dialog/CustomDialogWithLazyColumn.kt @@ -21,6 +21,10 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +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.draw.clip @@ -71,6 +75,7 @@ fun CustomDialogWithLazyColumn( withDivider: Boolean, items: (LazyListScope.() -> Unit) = {} ) { + var actionClicked by remember { mutableStateOf(false) } AlertDialog( onDismissRequest = { onDismiss() }, properties = properties @@ -156,7 +161,13 @@ fun CustomDialogWithLazyColumn( .align(Alignment.End) .padding(horizontal = 24.dp) ) { - TextButton(onClick = { onDismiss() }) { + TextButton( + onClick = { + actionClicked = true + onDismiss() + }, + enabled = !actionClicked + ) { Text( text = stringResource(id = R.string.cancel), style = MaterialTheme.typography.labelLarge, @@ -166,8 +177,11 @@ fun CustomDialogWithLazyColumn( if (actionText != null) { Spacer(modifier = Modifier.width(4.dp)) TextButton( - onClick = { onAction() }, - enabled = isActionEnabled == true + onClick = { + actionClicked = true + onAction() + }, + enabled = isActionEnabled == true && !actionClicked ) { Text( text = actionText, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRailItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRailItem.kt index 80ed8e28..3339ec92 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRailItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRailItem.kt @@ -7,6 +7,7 @@ import androidx.compose.material3.NavigationRailItem import androidx.compose.material3.NavigationRailItemDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -22,6 +23,11 @@ fun CustomNavigationRailItem( isSelected: Boolean, onClick: () -> Unit ) { + val icon = remember(isSelected) { + if (isSelected) item.selectedIcon + else item.unselectedIcon + } + NavigationRailItem( label = { Text( @@ -36,9 +42,7 @@ fun CustomNavigationRailItem( onClick = { onClick() }, icon = { Icon( - painter = - if (isSelected) item.selectedIcon - else item.unselectedIcon, + painter = icon, contentDescription = item.title, modifier = Modifier.size(24.dp) ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainEvent.kt index aa25f590..dfd3ba13 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainEvent.kt @@ -1,10 +1,7 @@ package ua.acclorite.book_story.presentation.data -import androidx.activity.ComponentActivity - sealed class MainEvent { - data class OnLocaleUpdate(val activity: ComponentActivity) : MainEvent() - data class OnChangeLanguage(val lang: String, val activity: ComponentActivity) : MainEvent() + data class OnChangeLanguage(val lang: String) : MainEvent() data class OnChangeTheme(val theme: String) : MainEvent() data class OnChangeDarkTheme(val darkTheme: String) : MainEvent() data class OnChangeFontFamily(val fontFamily: String) : MainEvent() diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt index 08020779..87831515 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt @@ -1,14 +1,12 @@ package ua.acclorite.book_story.presentation.data import android.os.Build -import androidx.activity.ComponentActivity import androidx.compose.ui.graphics.Color import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -44,12 +42,16 @@ class MainViewModel @Inject constructor( private val _isReady = MutableStateFlow(false) val isReady = _isReady.asStateFlow() - private val _updating = MutableStateFlow(false) - val updating = _updating.asStateFlow() - /* -- Language ----------------------------------------------------- */ private var _language: String = - stateHandle[Constants.LANGUAGE] ?: Locale.getDefault().language + stateHandle[Constants.LANGUAGE] + ?: if ( + Constants.LANGUAGES.any { Locale.getDefault().language.take(2) == it.first } + ) { + Locale.getDefault().language.take(2) + } else { + "en" + } set(value) { field = value stateHandle[Constants.LANGUAGE] = value @@ -172,23 +174,9 @@ class MainViewModel @Inject constructor( fun onEvent(event: MainEvent) { when (event) { is MainEvent.OnChangeLanguage -> { - viewModelScope.launch(Dispatchers.IO) { - _updating.update { true } - - changeLanguage.execute(event.lang, event.activity) + viewModelScope.launch(Dispatchers.Main) { + changeLanguage.execute(event.lang) _language = event.lang - delay(100) - - _updating.update { false } - } - } - - is MainEvent.OnLocaleUpdate -> { - viewModelScope.launch(Dispatchers.IO) { - _updating.update { true } - changeLanguage.execute(_language, event.activity) - delay(10) - _updating.update { false } } } @@ -266,7 +254,6 @@ class MainViewModel @Inject constructor( } fun init( - activity: ComponentActivity, libraryViewModel: LibraryViewModel, ) { val isViewModelsReady = combine( @@ -311,11 +298,11 @@ class MainViewModel @Inject constructor( } // Language - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(Dispatchers.Main) { getDatastore .execute(DataStoreConstants.LANGUAGE, _language) .first { - onEvent(MainEvent.OnChangeLanguage(it, activity)) + onEvent(MainEvent.OnChangeLanguage(it)) it.isNotBlank() } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt index 9e417482..43c31bea 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/BookInfoScreen.kt @@ -26,7 +26,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material.pullrefresh.PullRefreshDefaults import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState @@ -34,7 +33,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api 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.Scaffold import androidx.compose.material3.SnackbarHostState @@ -58,6 +56,7 @@ import dagger.hilt.android.lifecycle.withCreationCallback import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar +import ua.acclorite.book_story.presentation.components.CustomIconButton import ua.acclorite.book_story.presentation.components.CustomSnackbar import ua.acclorite.book_story.presentation.components.GoBackButton import ua.acclorite.book_story.presentation.data.Argument @@ -114,8 +113,6 @@ fun BookInfoScreen( val snackbarState = remember { SnackbarHostState() } val refreshState = rememberPullRefreshState( refreshing = state.isRefreshing, - refreshThreshold = PullRefreshDefaults.RefreshThreshold + 32.dp, - refreshingOffset = PullRefreshDefaults.RefreshingOffset + 64.dp, onRefresh = { viewModel.onEvent( BookInfoEvent.OnUpdateBook( @@ -193,26 +190,21 @@ fun BookInfoScreen( } }, content1Actions = { - IconButton( - enabled = !state.isRefreshing, - onClick = { - viewModel.onEvent( - BookInfoEvent.OnUpdateBook( - refreshList = { - libraryViewModel.onEvent(LibraryEvent.OnLoadList) - historyViewModel.onEvent(HistoryEvent.OnLoadList) - }, - snackbarState, - context - ) - ) - } + CustomIconButton( + icon = Icons.Default.Refresh, + contentDescription = stringResource(id = R.string.refresh_book_content_desc), + disableOnClick = false, + enabled = !state.isRefreshing ) { - Icon( - imageVector = Icons.Default.Refresh, - contentDescription = stringResource(id = R.string.refresh_book_content_desc), - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant + viewModel.onEvent( + BookInfoEvent.OnUpdateBook( + refreshList = { + libraryViewModel.onEvent(LibraryEvent.OnLoadList) + historyViewModel.onEvent(HistoryEvent.OnLoadList) + }, + snackbarState, + context + ) ) } @@ -230,34 +222,30 @@ fun BookInfoScreen( enter = Transitions.DefaultTransitionIn, exit = fadeOut(tween(200)) ) { - IconButton( + CustomIconButton( + icon = Icons.Default.Done, + contentDescription = stringResource(id = R.string.apply_changes_content_desc), + disableOnClick = true, enabled = state.titleValue.isNotBlank() && - state.titleValue.trim() != state.book.title.trim(), - onClick = { - viewModel.onEvent(BookInfoEvent.OnUpdateTitle( - refreshList = { - libraryViewModel.onEvent( - LibraryEvent.OnLoadList - ) - } - )) - Toast.makeText( - context, - context.getString(R.string.title_changed), - Toast.LENGTH_LONG - ).show() - } + state.titleValue.trim() != + state.book.title.trim(), + color = if (state.titleValue.isNotBlank() + && state.titleValue != state.book.title + ) MaterialTheme.colorScheme.onSurface + else MaterialTheme.colorScheme.onSurfaceVariant ) { - Icon( - imageVector = Icons.Default.Done, - contentDescription = "Apply changes", - tint = - if (state.titleValue.isNotBlank() - && state.titleValue != state.book.title - ) - MaterialTheme.colorScheme.onSurface - else MaterialTheme.colorScheme.onSurfaceVariant - ) + viewModel.onEvent(BookInfoEvent.OnUpdateTitle( + refreshList = { + libraryViewModel.onEvent( + LibraryEvent.OnLoadList + ) + } + )) + Toast.makeText( + context, + context.getString(R.string.title_changed), + Toast.LENGTH_LONG + ).show() } } } @@ -354,7 +342,9 @@ fun BookInfoScreen( PullRefreshIndicator( state.isRefreshing, refreshState, - Modifier.align(Alignment.TopCenter), + Modifier + .align(Alignment.TopCenter) + .padding(top = paddingValues.calculateTopPadding()), backgroundColor = MaterialTheme.colorScheme.inverseSurface, contentColor = MaterialTheme.colorScheme.inverseOnSurface ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoInfoSection.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoInfoSection.kt index 06af60bf..6f81b150 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoInfoSection.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoInfoSection.kt @@ -36,10 +36,12 @@ import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel @@ -76,7 +78,7 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) { ) { Icon( imageVector = Icons.Default.Image, - contentDescription = "Cover Image not found", + contentDescription = stringResource(id = R.string.cover_image_not_found_content_desc), modifier = Modifier .align(Alignment.Center) .fillMaxWidth(0.7f) @@ -87,7 +89,7 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) { if (book.coverImage != null) { Image( bitmap = book.coverImage, - contentDescription = "Cover", + contentDescription = stringResource(id = R.string.cover_image_content_desc), modifier = Modifier .fillMaxSize() .clip(MaterialTheme.shapes.large), diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoreDropDown.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoreDropDown.kt index 61f1ef0a..6610347d 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoreDropDown.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoMoreDropDown.kt @@ -1,26 +1,22 @@ package ua.acclorite.book_story.presentation.screens.book_info.components import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.DriveFileMove import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Info import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.components.CustomDropDownMenuItem +import ua.acclorite.book_story.presentation.components.CustomIconButton import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel @@ -32,15 +28,13 @@ fun BookInfoMoreDropDown(viewModel: BookInfoViewModel, snackbarState: SnackbarHo val state by viewModel.state.collectAsState() Box { - IconButton(onClick = { + CustomIconButton( + icon = Icons.Default.MoreVert, + contentDescription = stringResource(id = R.string.show_dropdown_content_desc), + disableOnClick = false, + enabled = !state.showMoreDropDown + ) { viewModel.onEvent(BookInfoEvent.OnShowHideMoreDropDown) - }) { - Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = "Show drop down", - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) } DropdownMenu( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoStatisticSection.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoStatisticSection.kt index 4d0410ba..e22b9f4d 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoStatisticSection.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/BookInfoStatisticSection.kt @@ -9,6 +9,7 @@ import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.res.stringResource @@ -24,18 +25,29 @@ import ua.acclorite.book_story.presentation.data.removeTrailingZero */ @Composable fun BookInfoStatisticSection(book: Book) { + val progress = remember(book) { + "${ + (book.progress * 100) + .toDouble() + .removeDigits(1) + .removeTrailingZero() + }%" + } + val description = if (book.progress < 1f) stringResource( + id = R.string.you_read_query, + progress + ) + " " + stringResource( + if (book.progress > 0.2f) R.string.read_keep + else R.string.read_more + ) else stringResource(id = R.string.read_done) + Column( Modifier .fillMaxWidth() .padding(horizontal = 24.dp) ) { Text( - text = "${ - (book.progress * 100) - .toDouble() - .removeDigits(1) - .removeTrailingZero() - }%", + text = progress, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.bodyLarge ) @@ -53,16 +65,7 @@ fun BookInfoStatisticSection(book: Book) { Spacer(modifier = Modifier.height(6.dp)) Text( - text = if (book.progress < 1f) stringResource( - id = R.string.you_read_query, - (book.progress * 100) - .toDouble() - .removeDigits(1) - .removeTrailingZero() + "%" - ) + " " + stringResource( - if (book.progress > 0.2f) R.string.read_keep - else R.string.read_more - ) else stringResource(id = R.string.read_done), + text = description, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodySmall, maxLines = 1, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheet.kt index 072a1d45..95b0d1ff 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheet.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/change_cover_bottom_sheet/BookInfoChangeCoverBottomSheet.kt @@ -6,8 +6,10 @@ import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.HideImage import androidx.compose.material.icons.filled.ImageSearch @@ -44,7 +46,8 @@ fun BookInfoChangeCoverBottomSheet( ) { val state by viewModel.state.collectAsState() val context = LocalContext.current - val book = state.book + val navigationBarPadding = + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() val photoPicker = rememberLauncherForActivityResult( contract = ActivityResultContracts.PickVisualMedia(), @@ -89,7 +92,7 @@ fun BookInfoChangeCoverBottomSheet( ) } - if (book.coverImage != null) { + if (state.book.coverImage != null) { BookInfoChangeCoverBottomSheetItem( icon = Icons.Default.HideImage, text = stringResource(id = R.string.delete_cover), @@ -109,6 +112,10 @@ fun BookInfoChangeCoverBottomSheet( } } - Spacer(modifier = Modifier.height(48.dp)) + Spacer( + modifier = Modifier.height( + 8.dp + navigationBarPadding + ) + ) } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheet.kt index 7797eab8..d5a952df 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheet.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheet.kt @@ -3,8 +3,10 @@ package ua.acclorite.book_story.presentation.screens.book_info.components.detail import android.widget.Toast import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet @@ -12,6 +14,7 @@ import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -35,20 +38,32 @@ fun BookInfoDetailsBottomSheet( ) { val state by viewModel.state.collectAsState() val context = LocalContext.current + val navigationBarPadding = + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() - val pattern = SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault()) - val lastOpened = pattern.format(Date(state.book.lastOpened ?: 0)) + val pattern = remember { + SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault()) + } + val lastOpened = remember { + pattern.format(Date(state.book.lastOpened ?: 0)) + } - val sizeBytes = state.book.file?.length() ?: 0 + val sizeBytes = remember { + state.book.file?.length() ?: 0 + } - val fileSizeKB = if (sizeBytes > 0) sizeBytes.toDouble() / 1024.0 else 0.0 - val fileSizeMB = if (sizeBytes > 0) fileSizeKB / 1024.0 else 0.0 + val fileSizeKB = remember { + if (sizeBytes > 0) sizeBytes.toDouble() / 1024.0 else 0.0 + } + val fileSizeMB = remember { + if (sizeBytes > 0) fileSizeKB / 1024.0 else 0.0 + } - val fileSize = + val fileSize = remember { if (fileSizeMB >= 1.0) "%.2f MB".format(fileSizeMB) else if (fileSizeMB > 0.0) "%.2f KB".format(fileSizeKB) else "" - + } ModalBottomSheet( modifier = Modifier.fillMaxWidth(), @@ -129,7 +144,10 @@ fun BookInfoDetailsBottomSheet( } } - - Spacer(modifier = Modifier.height(48.dp)) + Spacer( + modifier = Modifier.height( + 8.dp + navigationBarPadding + ) + ) } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheetItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheetItem.kt index 22cf689f..6bf9a574 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheetItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/details_bottom_sheet/BookInfoDetailsBottomSheetItem.kt @@ -34,9 +34,7 @@ fun BookInfoDetailsBottomSheetItem( modifier .fillMaxWidth() .combinedClickable( - onClick = { - - }, + onClick = {}, onLongClick = { onClick() } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoViewModel.kt index 48511fe1..9d3b08a2 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoViewModel.kt @@ -27,6 +27,7 @@ import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.use_case.DeleteBooks import ua.acclorite.book_story.domain.use_case.GetBookFromFile import ua.acclorite.book_story.domain.use_case.UpdateBooks +import ua.acclorite.book_story.domain.use_case.UpdateBooksWithText import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Screen @@ -35,6 +36,7 @@ import ua.acclorite.book_story.presentation.data.Screen class BookInfoViewModel @AssistedInject constructor( @Assisted book: Book, private val updateBooks: UpdateBooks, + private val updateBooksWithText: UpdateBooksWithText, private val deleteBooks: DeleteBooks, private val getBookFromFile: GetBookFromFile ) : ViewModel() { @@ -42,7 +44,7 @@ class BookInfoViewModel @AssistedInject constructor( private val _state = MutableStateFlow(BookInfoState(book)) val state = _state.asStateFlow() - var job: Job? = null + private var job: Job? = null fun onEvent(event: BookInfoEvent) { when (event) { @@ -286,7 +288,8 @@ class BookInfoViewModel @AssistedInject constructor( viewModelScope.launch(Dispatchers.IO) { _state.update { it.copy( - isRefreshing = true + isRefreshing = true, + editTitle = false ) } @@ -358,13 +361,20 @@ class BookInfoViewModel @AssistedInject constructor( _state.update { it.copy( book = it.book.copy( + author = updatedBook.author, + description = updatedBook.description + ) + ) + } + updateBooksWithText.execute( + listOf( + _state.value.book.copy( author = updatedBook.author, description = updatedBook.description, text = updatedBook.text ) ) - } - updateBooks.execute(listOf(_state.value.book)) + ) event.refreshList() onEvent( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/BrowseScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/BrowseScreen.kt index 36f6a1fa..e0e3c143 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/BrowseScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/BrowseScreen.kt @@ -11,14 +11,10 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -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.grid.rememberLazyGridState +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -29,8 +25,6 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -38,14 +32,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.snapshotFlow +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -55,11 +49,9 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberPermissionState -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.debounce import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar +import ua.acclorite.book_story.presentation.components.CustomIconButton import ua.acclorite.book_story.presentation.components.MoreDropDown import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty import ua.acclorite.book_story.presentation.components.is_messages.IsError @@ -79,7 +71,7 @@ import ua.acclorite.book_story.ui.elevation ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class, ExperimentalFoundationApi::class, - ExperimentalMaterialApi::class, FlowPreview::class + ExperimentalMaterialApi::class ) @Composable fun BrowseScreen( @@ -98,34 +90,21 @@ fun BrowseScreen( } ) val focusRequester = remember { FocusRequester() } - val listState = rememberLazyGridState(state.scrollIndex, state.scrollOffset) + var showErrorMessage by remember { mutableStateOf(false) } - LaunchedEffect(listState) { - snapshotFlow { - listState.firstVisibleItemIndex - } - .debounce(10L) - .collectLatest { - viewModel.onEvent(BrowseEvent.OnUpdateScrollIndex(it)) - } - } - LaunchedEffect(listState) { - snapshotFlow { - listState.firstVisibleItemScrollOffset - } - .debounce(10L) - .collectLatest { - viewModel.onEvent( - BrowseEvent.OnUpdateScrollOffset(it) - ) - } - } LaunchedEffect(Unit) { - viewModel.onEvent(BrowseEvent.OnPermissionCheck(permissionState)) + viewModel.onEvent( + BrowseEvent.OnPermissionCheck( + permissionState, + hideErrorMessage = { showErrorMessage = false } + ) + ) } if (state.requestPermissionDialog) { - BrowseStoragePermissionDialog(viewModel, permissionState) + BrowseStoragePermissionDialog(viewModel, permissionState) { + showErrorMessage = it + } } if (state.showAddingDialog) { BrowseAddingDialog( @@ -146,7 +125,7 @@ fun BrowseScreen( scrolledContainerColor = MaterialTheme.elevation(), scrollBehavior = null, - isTopBarScrolled = state.scrollIndex > 0 || state.scrollOffset > 0 || state.hasSelectedItems, + isTopBarScrolled = state.hasSelectedItems || state.listState.canScrollBackward, content1Visibility = !state.hasSelectedItems && !state.showSearch, content1NavigationIcon = {}, @@ -160,26 +139,26 @@ fun BrowseScreen( ) }, content1Actions = { - IconButton(onClick = { viewModel.onEvent(BrowseEvent.OnSearchShowHide) }) { - Icon( - imageVector = Icons.Default.Search, - contentDescription = "Search files", - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) + CustomIconButton( + icon = Icons.Default.Search, + contentDescription = stringResource(id = R.string.search_content_desc), + disableOnClick = false, + enabled = !state.showSearch + ) { + viewModel.onEvent(BrowseEvent.OnSearchShowHide) } MoreDropDown(navigator = navigator) }, content2Visibility = state.hasSelectedItems, content2NavigationIcon = { - IconButton(onClick = { viewModel.onEvent(BrowseEvent.OnClearSelectedFiles) }) { - Icon( - imageVector = Icons.Default.Clear, - contentDescription = "Clear selected items", - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurface - ) + CustomIconButton( + icon = Icons.Default.Clear, + contentDescription = + stringResource(id = R.string.clear_selected_items_content_desc), + disableOnClick = true + ) { + viewModel.onEvent(BrowseEvent.OnClearSelectedFiles) } }, content2Title = { @@ -195,25 +174,27 @@ fun BrowseScreen( ) }, content2Actions = { - IconButton(onClick = { viewModel.onEvent(BrowseEvent.OnAddingDialogRequest) }) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = "Add files to library", - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) + CustomIconButton( + icon = Icons.Default.Check, + contentDescription = + stringResource(id = R.string.add_files_content_desc), + disableOnClick = false, + enabled = !state.showAddingDialog + ) { + viewModel.onEvent(BrowseEvent.OnAddingDialogRequest) } }, content3Visibility = state.showSearch && !state.hasSelectedItems, content3NavigationIcon = { - IconButton(onClick = { viewModel.onEvent(BrowseEvent.OnSearchShowHide) }) { - Icon( - imageVector = Icons.AutoMirrored.Default.ArrowBack, - contentDescription = "Exit search mode", - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurface - ) + CustomIconButton( + icon = Icons.AutoMirrored.Default.ArrowBack, + contentDescription = stringResource( + id = R.string.exit_search_content_desc + ), + disableOnClick = true + ) { + viewModel.onEvent(BrowseEvent.OnSearchShowHide) } }, content3Title = { @@ -268,52 +249,54 @@ fun BrowseScreen( .fillMaxSize() .padding(top = padding.calculateTopPadding()) ) { - LazyVerticalGrid( - state = listState, - modifier = Modifier - .fillMaxSize(), - columns = GridCells.Adaptive(170.dp), - contentPadding = PaddingValues(12.dp) - ) { - items( - state.selectableFiles, - key = { it.first.path } - ) { selectableFile -> - BrowseFileItem( - file = selectableFile, - modifier = Modifier - .animateItemPlacement(), - onClick = { - viewModel.onEvent(BrowseEvent.OnSelectFile(selectableFile)) - } - ) + DefaultTransition(visible = !state.isLoading) { + LazyColumn( + state = state.listState, + modifier = Modifier + .fillMaxSize(), + contentPadding = PaddingValues(vertical = 8.dp) + ) { + items( + state.selectableFiles, + key = { it.first.path } + ) { selectableFile -> + BrowseFileItem( + file = selectableFile, + modifier = Modifier + .animateItemPlacement(), + hasSelectedFiles = state.selectableFiles.any { it.second }, + onClick = { + viewModel.onEvent(BrowseEvent.OnSelectFile(selectableFile)) + } + ) + } } } - if (state.isLoading && !state.isRefreshing && state.selectableFiles.isEmpty()) { - CircularProgressIndicator( - color = MaterialTheme.colorScheme.primary, - strokeCap = StrokeCap.Round, - modifier = Modifier - .align(Alignment.Center) - .size(36.dp) - ) - } - - DefaultTransition(visible = state.showErrorMessage, Modifier.align(Alignment.Center)) { + AnimatedVisibility( + visible = showErrorMessage, + modifier = Modifier.align(Alignment.Center), + enter = Transitions.DefaultTransitionIn, + exit = fadeOut(tween(0)) + ) { IsError( modifier = Modifier.align(Alignment.Center), errorMessage = stringResource(id = R.string.error_permission), icon = painterResource(id = R.drawable.error), actionTitle = stringResource(id = R.string.grant_permission) ) { - viewModel.onEvent(BrowseEvent.OnPermissionCheck(permissionState)) + viewModel.onEvent( + BrowseEvent.OnPermissionCheck( + permissionState, + hideErrorMessage = { showErrorMessage = false } + ) + ) } } AnimatedVisibility( visible = !state.isLoading && state.selectableFiles.isEmpty() - && !state.showErrorMessage && !state.requestPermissionDialog + && !showErrorMessage && !state.requestPermissionDialog && !state.isRefreshing, modifier = Modifier.align(Alignment.Center), enter = Transitions.DefaultTransitionIn, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseFIleItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseFIleItem.kt index af035842..d81c78a3 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseFIleItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseFIleItem.kt @@ -15,22 +15,25 @@ 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.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.automirrored.filled.InsertDriveFile import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.components.CustomCheckbox import ua.acclorite.book_story.ui.DefaultTransition import java.io.File import java.text.SimpleDateFormat @@ -41,19 +44,24 @@ import java.util.Locale * Browse list element item. Can be selected. */ @Composable -fun BrowseFileItem(file: Pair, modifier: Modifier, onClick: () -> Unit) { - val fileExtension: String = file.first.name.substringAfterLast(".", "") +fun BrowseFileItem( + file: Pair, + hasSelectedFiles: Boolean, + modifier: Modifier, onClick: () -> Unit +) { + val pattern = remember { SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault()) } + val lastModified = remember { pattern.format(Date(file.first.lastModified())) } + val sizeBytes = remember { file.first.length() } - val pattern = SimpleDateFormat("HH:mm dd MMM", Locale.getDefault()) - - val lastModified = pattern.format(Date(file.first.lastModified())) - val icon = when (fileExtension) { - "txt" -> painterResource(id = R.drawable.txt) - "epub" -> painterResource(id = R.drawable.epub) - "pdf" -> painterResource(id = R.drawable.pdf) - else -> painterResource(id = R.drawable.file) + val fileSizeKB = remember { if (sizeBytes > 0) sizeBytes.toDouble() / 1024.0 else 0.0 } + val fileSizeMB = remember { if (sizeBytes > 0) fileSizeKB / 1024.0 else 0.0 } + val fileSize = remember { + if (fileSizeMB >= 1.0) "%.2f MB".format(fileSizeMB) + else if (fileSizeMB > 0.0) "%.2f KB".format(fileSizeKB) + else "0 KB" } - val outlineColor = if (file.second) MaterialTheme.colorScheme.primary + + val outlineColor = if (file.second) MaterialTheme.colorScheme.outline else MaterialTheme.colorScheme.outlineVariant val backgroundColor = if (file.second) MaterialTheme.colorScheme.secondaryContainer else Color.Transparent @@ -61,90 +69,82 @@ fun BrowseFileItem(file: Pair, modifier: Modifier, onClick: () -> val animatedOutlineColor by animateColorAsState( targetValue = outlineColor, tween(300), - label = "Outline animation" + label = stringResource(id = R.string.outline_anim_content_desc) ) val animatedBackgroundColor by animateColorAsState( targetValue = backgroundColor, tween(300), - label = "Background animation" + label = stringResource(id = R.string.background_anim_content_desc) ) - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, + Row( + verticalAlignment = Alignment.CenterVertically, modifier = modifier - .padding(6.dp) - .clip(MaterialTheme.shapes.medium) - .border( - width = 1.dp, - color = animatedOutlineColor, - shape = MaterialTheme.shapes.medium - ) - .padding(1.dp) + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 3.dp) + .clip(RoundedCornerShape(10.dp)) .background(animatedBackgroundColor) .clickable { onClick() } + .padding(horizontal = 8.dp, vertical = 7.dp) ) { - Icon( - painter = icon, - contentDescription = "File icon", - modifier = Modifier - .padding(vertical = 60.dp) - .size(70.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Start, - modifier = Modifier - .background(MaterialTheme.colorScheme.surface) - .padding(horizontal = 10.dp, vertical = 12.dp) + modifier = Modifier.weight(0.88f) ) { - Box { - DefaultTransition(visible = !file.second) { - Icon( - painter = painterResource(R.drawable.file), - contentDescription = "File", - modifier = Modifier - .size(28.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant + Box( + modifier = Modifier + .border( + 1.dp, + animatedOutlineColor, + RoundedCornerShape(6.dp) ) - } - DefaultTransition(visible = file.second) { - Icon( - imageVector = Icons.Filled.CheckCircle, - contentDescription = "Checked", - modifier = Modifier - .size(28.dp), - tint = MaterialTheme.colorScheme.primary - ) - } - } - Spacer(modifier = Modifier.width(8.dp)) - Column( - verticalArrangement = Arrangement.Center + .padding(14.dp), + contentAlignment = Alignment.Center ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.InsertDriveFile, + contentDescription = stringResource(id = R.string.file_icon_content_desc), + modifier = Modifier + .size(22.dp), + tint = MaterialTheme.colorScheme.primary + ) + } + Spacer(modifier = Modifier.width(12.dp)) + Column(verticalArrangement = Arrangement.Center) { Text( file.first.name, color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.labelMedium, - maxLines = 1, + style = MaterialTheme.typography.bodyLarge, + maxLines = 2, + lineHeight = 18.sp, overflow = TextOverflow.Ellipsis, modifier = Modifier.fillMaxWidth() ) - Spacer(modifier = Modifier.height(2.dp)) + Spacer(modifier = Modifier.height(4.dp)) Text( - lastModified, + "$fileSize, $lastModified", color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodySmall, - fontSize = 11.sp, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.fillMaxWidth() ) } + } + Box( + modifier = Modifier + .weight(0.12f) + .padding(end = 6.dp), + contentAlignment = Alignment.CenterEnd + ) { + DefaultTransition( + visible = hasSelectedFiles + ) { + CustomCheckbox(selected = file.second, size = 18.dp) + } } } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseStoragePermissionDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseStoragePermissionDialog.kt index 887ac8cb..1db2f06e 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseStoragePermissionDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/BrowseStoragePermissionDialog.kt @@ -19,7 +19,11 @@ import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel */ @OptIn(ExperimentalPermissionsApi::class) @Composable -fun BrowseStoragePermissionDialog(viewModel: BrowseViewModel, permissionState: PermissionState) { +fun BrowseStoragePermissionDialog( + viewModel: BrowseViewModel, + permissionState: PermissionState, + showErrorMessage: (Boolean) -> Unit +) { val activity = LocalContext.current as ComponentActivity CustomDialogWithContent( @@ -27,15 +31,28 @@ fun BrowseStoragePermissionDialog(viewModel: BrowseViewModel, permissionState: P description = stringResource(id = R.string.storage_permission_description), actionText = stringResource(id = R.string.grant), imageVectorIcon = Icons.Default.SdStorage, - onDismiss = { viewModel.onEvent(BrowseEvent.OnStoragePermissionDismiss(permissionState)) }, + onDismiss = { + viewModel.onEvent( + BrowseEvent.OnStoragePermissionDismiss( + permissionState, + showErrorMessage = { showErrorMessage(true) } + ) + ) + }, isActionEnabled = true, withDivider = false, onAction = { viewModel.onEvent( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - BrowseEvent.OnStoragePermissionRequest(activity) + BrowseEvent.OnStoragePermissionRequest( + activity, + hideErrorMessage = { showErrorMessage(false) } + ) } else { - BrowseEvent.OnLegacyStoragePermissionRequest(permissionState) + BrowseEvent.OnLegacyStoragePermissionRequest( + permissionState, + hideErrorMessage = { showErrorMessage(false) } + ) } ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialogItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialogItem.kt index 9bfd271c..307eb37f 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialogItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialogItem.kt @@ -81,7 +81,7 @@ fun BrowseAddingDialogItem(result: NullableBook, onClick: (Boolean) -> Unit) { ) { Icon( imageVector = icon, - contentDescription = "Error", + contentDescription = stringResource(id = R.string.error_content_desc), modifier = Modifier.size(26.dp), tint = MaterialTheme.colorScheme.error ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseEvent.kt index 9992bd75..17e6b1bb 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseEvent.kt @@ -11,13 +11,24 @@ import ua.acclorite.book_story.presentation.data.Navigator import java.io.File sealed class BrowseEvent { - data class OnStoragePermissionRequest(val activity: ComponentActivity) : BrowseEvent() - data class OnLegacyStoragePermissionRequest(val permissionState: PermissionState) : - BrowseEvent() + data class OnStoragePermissionRequest( + val activity: ComponentActivity, val hideErrorMessage: () -> Unit + ) : BrowseEvent() + + data class OnLegacyStoragePermissionRequest( + val permissionState: PermissionState, val hideErrorMessage: () -> Unit + ) : BrowseEvent() + + data class OnStoragePermissionDismiss( + val permissionState: PermissionState, val showErrorMessage: () -> Unit + ) : BrowseEvent() - data class OnStoragePermissionDismiss(val permissionState: PermissionState) : BrowseEvent() data object OnRefreshList : BrowseEvent() - data class OnPermissionCheck(val permissionState: PermissionState) : BrowseEvent() + data class OnPermissionCheck( + val permissionState: PermissionState, + val hideErrorMessage: () -> Unit + ) : BrowseEvent() + data class OnSelectFile(val file: Pair) : BrowseEvent() data class OnSelectBook(val book: NullableBook) : BrowseEvent() data object OnSearchShowHide : BrowseEvent() @@ -29,8 +40,5 @@ sealed class BrowseEvent { data object OnGetBooksFromFiles : BrowseEvent() data class OnAddBooks(val navigator: Navigator, val resetScroll: () -> Unit) : BrowseEvent() data object OnLoadList : BrowseEvent() - data class OnUpdateScrollIndex(val index: Int) : BrowseEvent() - data class OnUpdateScrollOffset(val offset: Int) : BrowseEvent() - } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseState.kt index 12ad5af7..e6a412f4 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseState.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseState.kt @@ -1,5 +1,6 @@ package ua.acclorite.book_story.presentation.screens.browse.data +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Immutable import ua.acclorite.book_story.domain.model.NullableBook import java.io.File @@ -7,10 +8,11 @@ import java.io.File @Immutable data class BrowseState( val selectableFiles: List> = emptyList(), + val listState: LazyListState = LazyListState(0, 0), + val isLoading: Boolean = true, val isRefreshing: Boolean = false, val requestPermissionDialog: Boolean = false, - val showErrorMessage: Boolean = false, val selectedItemsCount: Int = 0, val hasSelectedItems: Boolean = false, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseViewModel.kt index bd4e0df8..f72c146c 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/data/BrowseViewModel.kt @@ -5,6 +5,7 @@ import android.net.Uri import android.os.Build import android.os.Environment import android.provider.Settings +import androidx.compose.foundation.lazy.LazyListState import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.accompanist.permissions.ExperimentalPermissionsApi @@ -20,7 +21,7 @@ import kotlinx.coroutines.launch import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.use_case.FastGetBooks import ua.acclorite.book_story.domain.use_case.GetBooksFromFiles -import ua.acclorite.book_story.domain.use_case.GetFilesFromDownloads +import ua.acclorite.book_story.domain.use_case.GetFilesFromDevice import ua.acclorite.book_story.domain.use_case.InsertBooks import ua.acclorite.book_story.presentation.data.Argument import ua.acclorite.book_story.presentation.data.Screen @@ -31,7 +32,7 @@ import javax.inject.Inject @HiltViewModel class BrowseViewModel @Inject constructor( private val getBooksFromFiles: GetBooksFromFiles, - private val getFilesFromDownloads: GetFilesFromDownloads, + private val getFilesFromDevice: GetFilesFromDevice, private val insertBooks: InsertBooks, private val fastGetBooks: FastGetBooks ) : ViewModel() { @@ -65,10 +66,10 @@ class BrowseViewModel @Inject constructor( } _state.update { it.copy( - requestPermissionDialog = false, - showErrorMessage = false + requestPermissionDialog = false ) } + event.hideErrorMessage() onEvent(BrowseEvent.OnRefreshList) break } @@ -93,10 +94,10 @@ class BrowseViewModel @Inject constructor( } _state.update { it.copy( - requestPermissionDialog = false, - showErrorMessage = false + requestPermissionDialog = false ) } + event.hideErrorMessage() onEvent(BrowseEvent.OnRefreshList) break } @@ -112,8 +113,7 @@ class BrowseViewModel @Inject constructor( _state.update { it.copy( - requestPermissionDialog = false, - showErrorMessage = !isPermissionGranted + requestPermissionDialog = false ) } @@ -121,8 +121,9 @@ class BrowseViewModel @Inject constructor( viewModelScope.launch(Dispatchers.IO) { getFilesFromDownloads() } + } else { + event.showErrorMessage() } - } is BrowseEvent.OnRefreshList -> { @@ -131,7 +132,8 @@ class BrowseViewModel @Inject constructor( it.copy( isRefreshing = true, hasSelectedItems = false, - showSearch = false + showSearch = false, + listState = LazyListState(0, 0) ) } @@ -146,55 +148,72 @@ class BrowseViewModel @Inject constructor( } is BrowseEvent.OnPermissionCheck -> { - val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R - val isPermissionGranted = - if (!legacyPermission) Environment.isExternalStorageManager() - else event.permissionState.status.isGranted + viewModelScope.launch(Dispatchers.IO) { + val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R + val isPermissionGranted = + if (!legacyPermission) Environment.isExternalStorageManager() + else event.permissionState.status.isGranted - if (isPermissionGranted) { - return - } - _state.update { - it.copy( - requestPermissionDialog = true, - showErrorMessage = false - ) + if (isPermissionGranted) { + return@launch + } + + _state.update { + it.copy( + requestPermissionDialog = true + ) + } + event.hideErrorMessage() } } is BrowseEvent.OnSelectFile -> { - val indexOfFile = _state.value.selectableFiles.indexOf(event.file) - val editedList = _state.value.selectableFiles.toMutableList() - editedList[indexOfFile] = editedList[indexOfFile].copy( - second = !editedList[indexOfFile].second - ) + viewModelScope.launch(Dispatchers.IO) { + val indexOfFile = _state.value.selectableFiles.indexOf(event.file) - _state.update { - it.copy( - selectableFiles = editedList.toList(), - selectedItemsCount = editedList.filter { file -> file.second }.size, - hasSelectedItems = editedList.any { file -> file.second } + if (indexOfFile == -1) { + return@launch + } + + val editedList = _state.value.selectableFiles.toMutableList() + editedList[indexOfFile] = editedList[indexOfFile].copy( + second = !editedList[indexOfFile].second ) + + _state.update { + it.copy( + selectableFiles = editedList.toList(), + selectedItemsCount = editedList.filter { file -> file.second }.size, + hasSelectedItems = editedList.any { file -> file.second } + ) + } } } is BrowseEvent.OnSelectBook -> { - val indexOfFile = _state.value.selectedBooks.indexOf(event.book) - val editedList = _state.value.selectedBooks.toMutableList() - editedList[indexOfFile] = NullableBook.NotNull( - editedList[indexOfFile].book!!.copy( - second = !editedList[indexOfFile].book!!.second - ) - ) + viewModelScope.launch(Dispatchers.IO) { + val indexOfFile = _state.value.selectedBooks.indexOf(event.book) - if (!editedList.any { it.book?.second == true }) { - return - } + if (indexOfFile == -1) { + return@launch + } - _state.update { - it.copy( - selectedBooks = editedList + val editedList = _state.value.selectedBooks.toMutableList() + editedList[indexOfFile] = NullableBook.NotNull( + editedList[indexOfFile].book!!.copy( + second = !editedList[indexOfFile].book!!.second + ) ) + + if (!editedList.any { it.book?.second == true }) { + return@launch + } + + _state.update { + it.copy( + selectedBooks = editedList + ) + } } } @@ -331,35 +350,20 @@ class BrowseViewModel @Inject constructor( viewModelScope.launch(Dispatchers.IO) { _state.update { it.copy( - isLoading = true + isLoading = true, + listState = LazyListState(0, 0) ) } getFilesFromDownloads() } } - - is BrowseEvent.OnUpdateScrollIndex -> { - _state.update { - it.copy( - scrollIndex = event.index - ) - } - } - - is BrowseEvent.OnUpdateScrollOffset -> { - _state.update { - it.copy( - scrollOffset = event.offset - ) - } - } } } private suspend fun getFilesFromDownloads( query: String = if (_state.value.showSearch) _state.value.searchQuery else "" ) { - getFilesFromDownloads.execute(query).collect { result -> + getFilesFromDevice.execute(query).collect { result -> when (result) { is Resource.Success -> { _state.update { @@ -371,13 +375,7 @@ class BrowseViewModel @Inject constructor( } } - is Resource.Loading -> { - _state.update { - it.copy( - isLoading = result.isLoading - ) - } - } + is Resource.Loading -> Unit is Resource.Error -> Unit } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt index c8973853..4d269075 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/HistoryScreen.kt @@ -12,13 +12,10 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize 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.items -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -28,24 +25,19 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -54,12 +46,10 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.debounce import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar import ua.acclorite.book_story.presentation.components.CategoryTitle +import ua.acclorite.book_story.presentation.components.CustomIconButton import ua.acclorite.book_story.presentation.components.CustomSnackbar import ua.acclorite.book_story.presentation.components.MoreDropDown import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty @@ -72,13 +62,14 @@ import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel +import ua.acclorite.book_story.ui.DefaultTransition import ua.acclorite.book_story.ui.Transitions import ua.acclorite.book_story.ui.elevation import java.util.UUID @OptIn( ExperimentalMaterial3Api::class, - ExperimentalMaterialApi::class, FlowPreview::class, ExperimentalFoundationApi::class + ExperimentalMaterialApi::class, ExperimentalFoundationApi::class ) @Composable fun HistoryScreen( @@ -89,7 +80,6 @@ fun HistoryScreen( val context = LocalContext.current val state by viewModel.state.collectAsState() val books = libraryViewModel.state.collectAsState().value.books.map { it.first } - val refreshState = rememberPullRefreshState( refreshing = state.isRefreshing, onRefresh = { @@ -98,28 +88,6 @@ fun HistoryScreen( ) val focusRequester = remember { FocusRequester() } val snackbarState = remember { SnackbarHostState() } - val listState = rememberLazyListState(state.scrollIndex, state.scrollOffset) - - LaunchedEffect(listState) { - snapshotFlow { - listState.firstVisibleItemIndex - } - .debounce(10L) - .collectLatest { - viewModel.onEvent(HistoryEvent.OnUpdateScrollIndex(it)) - } - } - LaunchedEffect(listState) { - snapshotFlow { - listState.firstVisibleItemScrollOffset - } - .debounce(10L) - .collectLatest { - viewModel.onEvent( - HistoryEvent.OnUpdateScrollOffset(it) - ) - } - } if (state.showDeleteWholeHistoryDialog) { HistoryDeleteWholeHistoryDialog(viewModel = viewModel, libraryViewModel = libraryViewModel) @@ -134,7 +102,7 @@ fun HistoryScreen( AnimatedTopAppBar( scrolledContainerColor = MaterialTheme.elevation(), scrollBehavior = null, - isTopBarScrolled = (state.scrollIndex > 0 || state.scrollOffset > 0) && !state.isLoading, + isTopBarScrolled = state.listState.canScrollBackward, content1Visibility = !state.showSearch, content1NavigationIcon = {}, @@ -148,43 +116,39 @@ fun HistoryScreen( ) }, content1Actions = { - IconButton( - enabled = !state.isRefreshing, - onClick = { viewModel.onEvent(HistoryEvent.OnSearchShowHide) } + CustomIconButton( + icon = Icons.Default.Search, + contentDescription = stringResource(id = R.string.search_content_desc), + disableOnClick = false, + enabled = !state.showSearch ) { - Icon( - imageVector = Icons.Default.Search, - contentDescription = "Search history", - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) + viewModel.onEvent(HistoryEvent.OnSearchShowHide) } - IconButton( + CustomIconButton( + icon = Icons.Outlined.DeleteSweep, + contentDescription = stringResource( + id = R.string.delete_whole_history_content_desc + ), + disableOnClick = false, enabled = !state.isLoading && !state.isRefreshing - && state.history.isNotEmpty(), - onClick = { - viewModel.onEvent(HistoryEvent.OnShowHideDeleteWholeHistoryDialog) - } + && state.history.isNotEmpty() ) { - Icon( - imageVector = Icons.Outlined.DeleteSweep, - contentDescription = "Delete whole history", - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) + viewModel.onEvent(HistoryEvent.OnShowHideDeleteWholeHistoryDialog) } MoreDropDown(navigator = navigator) }, content2Visibility = state.showSearch, content2NavigationIcon = { - IconButton(onClick = { viewModel.onEvent(HistoryEvent.OnSearchShowHide) }) { - Icon( - imageVector = Icons.AutoMirrored.Default.ArrowBack, - contentDescription = "Exit search mode", - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurface - ) + CustomIconButton( + icon = Icons.AutoMirrored.Default.ArrowBack, + contentDescription = stringResource( + id = R.string.exit_search_content_desc + ), + disableOnClick = true + ) { + viewModel.onEvent(HistoryEvent.OnSearchShowHide) } }, content2Title = { @@ -243,68 +207,72 @@ fun HistoryScreen( .fillMaxSize() .padding(top = paddingValues.calculateTopPadding()) ) { - LazyColumn( - Modifier - .fillMaxSize(), - state = listState, - contentPadding = PaddingValues(vertical = 12.dp) - ) { - if (!state.isLoading) { - state.history.forEachIndexed { index, groupedHistory -> - item(key = groupedHistory.title) { - if (index > 0) { + DefaultTransition(visible = !state.isLoading) { + LazyColumn( + Modifier + .fillMaxSize(), + state = state.listState, + contentPadding = PaddingValues(vertical = 12.dp) + ) { + if (!state.isLoading) { + state.history.forEachIndexed { index, groupedHistory -> + item(key = groupedHistory.title) { + if (index > 0) { + Spacer(modifier = Modifier.height(8.dp)) + } + + CategoryTitle( + modifier = Modifier.animateItemPlacement(), + title = when (groupedHistory.title) { + "today" -> stringResource(id = R.string.today) + "yesterday" -> stringResource(id = R.string.yesterday) + else -> groupedHistory.title + }, + padding = 16.dp + ) Spacer(modifier = Modifier.height(8.dp)) } - CategoryTitle( - modifier = Modifier.animateItemPlacement(), - title = when (groupedHistory.title) { - "today" -> stringResource(id = R.string.today) - "yesterday" -> stringResource(id = R.string.yesterday) - else -> groupedHistory.title - }, - padding = 16.dp - ) - Spacer(modifier = Modifier.height(8.dp)) - } + items( + groupedHistory.history, key = { it.id ?: UUID.randomUUID() } + ) { + val book = remember { + books.find { book -> book.id == it.bookId } + } ?: return@items - items( - groupedHistory.history, key = { it.id ?: UUID.randomUUID() } - ) { - val book = books.find { book -> book.id == it.bookId } ?: return@items - - HistoryItem( - modifier = Modifier.animateItemPlacement(), - history = it, - book = book, - onBodyClick = { - navigator.navigate( - Screen.BOOK_INFO, - false, - Argument("book", book) - ) - }, - onTitleClick = { - navigator.navigate( - Screen.READER, - false, - Argument("book", book) - ) - }, - isDeleteEnabled = !state.isRefreshing, - onDeleteClick = { - viewModel.onEvent( - HistoryEvent.OnDeleteHistoryElement( - historyToDelete = it, - snackbarState = snackbarState, - context = context, - refreshList = { - libraryViewModel.onEvent(LibraryEvent.OnLoadList) - } + HistoryItem( + modifier = Modifier.animateItemPlacement(), + history = it, + book = book, + onBodyClick = { + navigator.navigate( + Screen.BOOK_INFO, + false, + Argument("book", book) ) - ) - } - ) + }, + onTitleClick = { + navigator.navigate( + Screen.READER, + false, + Argument("book", book) + ) + }, + isDeleteEnabled = !state.isRefreshing, + onDeleteClick = { + viewModel.onEvent( + HistoryEvent.OnDeleteHistoryElement( + historyToDelete = it, + snackbarState = snackbarState, + context = context, + refreshList = { + libraryViewModel.onEvent(LibraryEvent.OnLoadList) + } + ) + ) + } + ) + } } } } @@ -322,15 +290,6 @@ fun HistoryScreen( icon = painterResource(id = R.drawable.empty_history) ) } - if (state.isLoading && !state.isRefreshing) { - CircularProgressIndicator( - color = MaterialTheme.colorScheme.primary, - strokeCap = StrokeCap.Round, - modifier = Modifier - .align(Alignment.Center) - .size(36.dp) - ) - } PullRefreshIndicator( state.isRefreshing, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryItem.kt index b890e7a7..efc4efd0 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/components/HistoryItem.kt @@ -23,6 +23,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -51,8 +52,10 @@ fun HistoryItem( isDeleteEnabled: Boolean, onDeleteClick: () -> Unit ) { - val date = Date(history.time) - val pattern = SimpleDateFormat("HH:mm", Locale.getDefault()) + val date = remember(history) { Date(history.time) } + val pattern = remember { + SimpleDateFormat("HH:mm", Locale.getDefault()) + } Row( verticalAlignment = Alignment.CenterVertically, @@ -65,7 +68,7 @@ fun HistoryItem( ) { Row( verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.weight(0.9f) + modifier = Modifier.weight(0.89f) ) { Box( modifier = Modifier @@ -76,7 +79,9 @@ fun HistoryItem( ) { Icon( imageVector = Icons.Default.Image, - contentDescription = "Cover Image not found", + contentDescription = stringResource( + id = R.string.cover_image_not_found_content_desc + ), modifier = Modifier .align(Alignment.Center) .fillMaxWidth(0.7f) @@ -128,7 +133,7 @@ fun HistoryItem( ) } } - Box(modifier = Modifier.weight(0.1f), contentAlignment = Alignment.CenterEnd) { + Box(modifier = Modifier.weight(0.11f), contentAlignment = Alignment.CenterEnd) { IconButton( enabled = isDeleteEnabled, onClick = { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryEvent.kt index 668978bd..5318fe90 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryEvent.kt @@ -20,6 +20,4 @@ sealed class HistoryEvent { data class OnSearchQueryChange(val query: String) : HistoryEvent() data object OnSearchShowHide : HistoryEvent() data class OnRequestFocus(val focusRequester: FocusRequester) : HistoryEvent() - data class OnUpdateScrollIndex(val index: Int) : HistoryEvent() - data class OnUpdateScrollOffset(val offset: Int) : HistoryEvent() } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryState.kt index 0f0c6158..72ba639c 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryState.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryState.kt @@ -1,9 +1,12 @@ package ua.acclorite.book_story.presentation.screens.history.data +import androidx.compose.foundation.lazy.LazyListState import ua.acclorite.book_story.domain.model.GroupedHistory data class HistoryState( val history: List = emptyList(), + val listState: LazyListState = LazyListState(0, 0), + val isRefreshing: Boolean = false, val isLoading: Boolean = true, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryViewModel.kt index 055a05d4..8c50dc17 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/history/data/HistoryViewModel.kt @@ -1,5 +1,6 @@ package ua.acclorite.book_story.presentation.screens.history.data +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material3.SnackbarResult import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -58,7 +59,8 @@ class HistoryViewModel @Inject constructor( _state.update { it.copy( isRefreshing = true, - showSearch = false + showSearch = false, + listState = LazyListState(0, 0), ) } @@ -76,12 +78,11 @@ class HistoryViewModel @Inject constructor( viewModelScope.launch(Dispatchers.IO) { _state.update { it.copy( - isLoading = true + isLoading = true, + listState = LazyListState(0, 0), ) } getHistoryFromDatabase() - onEvent(HistoryEvent.OnUpdateScrollIndex(0)) - onEvent(HistoryEvent.OnUpdateScrollOffset(0)) } } @@ -89,7 +90,8 @@ class HistoryViewModel @Inject constructor( viewModelScope.launch { _state.update { it.copy( - showDeleteWholeHistoryDialog = false + showDeleteWholeHistoryDialog = false, + isLoading = true ) } @@ -109,7 +111,7 @@ class HistoryViewModel @Inject constructor( } is HistoryEvent.OnDeleteHistoryElement -> { - viewModelScope.launch { + viewModelScope.launch(Dispatchers.IO) { deleteHistory.execute( listOf(event.historyToDelete) ) @@ -186,22 +188,6 @@ class HistoryViewModel @Inject constructor( } } } - - is HistoryEvent.OnUpdateScrollIndex -> { - _state.update { - it.copy( - scrollIndex = event.index - ) - } - } - - is HistoryEvent.OnUpdateScrollOffset -> { - _state.update { - it.copy( - scrollOffset = event.offset - ) - } - } } } @@ -224,16 +210,12 @@ class HistoryViewModel @Inject constructor( getHistory.execute().collect { result -> when (result) { is Resource.Success -> { - _state.update { - it.copy( - history = emptyList() - ) - } val history = result.data?.sortedByDescending { it.time } ?: emptyList() if (history.isEmpty()) { _state.update { it.copy( + history = emptyList(), isLoading = false ) } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt index fa843c82..088ed1ea 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/LibraryScreen.kt @@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid @@ -26,7 +25,6 @@ import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -38,8 +36,6 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -58,7 +54,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -74,6 +69,7 @@ import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar +import ua.acclorite.book_story.presentation.components.CustomIconButton import ua.acclorite.book_story.presentation.components.MoreDropDown import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty import ua.acclorite.book_story.presentation.data.Argument @@ -87,6 +83,7 @@ import ua.acclorite.book_story.presentation.screens.library.components.dialog.Li import ua.acclorite.book_story.presentation.screens.library.components.dialog.LibraryMoveDialog import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel +import ua.acclorite.book_story.ui.DefaultTransition import ua.acclorite.book_story.ui.Transitions import ua.acclorite.book_story.ui.elevation import java.util.UUID @@ -178,26 +175,26 @@ fun LibraryScreen( } }, content1Actions = { - IconButton(onClick = { viewModel.onEvent(LibraryEvent.OnSearchShowHide) }) { - Icon( - imageVector = Icons.Default.Search, - contentDescription = "Search books", - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) + CustomIconButton( + icon = Icons.Default.Search, + contentDescription = stringResource(id = R.string.search_content_desc), + disableOnClick = false, + enabled = !state.showSearch + ) { + viewModel.onEvent(LibraryEvent.OnSearchShowHide) } MoreDropDown(navigator = navigator) }, content2Visibility = state.hasSelectedItems, content2NavigationIcon = { - IconButton(onClick = { viewModel.onEvent(LibraryEvent.OnClearSelectedBooks) }) { - Icon( - imageVector = Icons.Default.Clear, - contentDescription = "Clear selected items", - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurface - ) + CustomIconButton( + icon = Icons.Default.Clear, + contentDescription = + stringResource(id = R.string.clear_selected_items_content_desc), + disableOnClick = true + ) { + viewModel.onEvent(LibraryEvent.OnClearSelectedBooks) } }, content2Title = { @@ -213,37 +210,38 @@ fun LibraryScreen( ) }, content2Actions = { - IconButton(onClick = { viewModel.onEvent(LibraryEvent.OnShowHideMoveDialog) }) { - Icon( - imageVector = Icons.AutoMirrored.Outlined.DriveFileMove, - contentDescription = "Move books to another category", - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) + CustomIconButton( + icon = Icons.AutoMirrored.Outlined.DriveFileMove, + contentDescription = stringResource( + id = R.string.move_books_content_desc + ), + disableOnClick = true + ) { + viewModel.onEvent(LibraryEvent.OnShowHideMoveDialog) } - IconButton(onClick = { viewModel.onEvent(LibraryEvent.OnShowHideDeleteDialog) }) { - Icon( - imageVector = Icons.Outlined.Delete, - contentDescription = "Delete books from database", - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) + CustomIconButton( + icon = Icons.Outlined.Delete, + contentDescription = stringResource( + id = R.string.delete_books_content_desc + ), + disableOnClick = true + ) { + viewModel.onEvent(LibraryEvent.OnShowHideDeleteDialog) } }, content3Visibility = state.showSearch && !state.hasSelectedItems, content3NavigationIcon = { - IconButton(onClick = { + CustomIconButton( + icon = Icons.AutoMirrored.Default.ArrowBack, + contentDescription = stringResource( + id = R.string.exit_search_content_desc + ), + disableOnClick = true + ) { viewModel.onEvent( LibraryEvent.OnSearchShowHide ) - }) { - Icon( - imageVector = Icons.AutoMirrored.Default.ArrowBack, - contentDescription = "Exit search mode", - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.onSurface - ) } }, content3Title = { @@ -307,7 +305,7 @@ fun LibraryScreen( HorizontalPager(state = pagerState, userScrollEnabled = !state.isRefreshing) { index -> var categoryIsLoading by remember { mutableStateOf(true) } val categorizedBooks = remember { mutableStateListOf>() } - val category = Category.entries[index] + val category = remember { Category.entries[index] } LaunchedEffect(state.books) { categorizedBooks.clear() @@ -321,13 +319,13 @@ fun LibraryScreen( } Box(modifier = Modifier.fillMaxSize()) { - LazyVerticalGrid( - columns = GridCells.Adaptive(120.dp), - modifier = Modifier - .fillMaxSize(), - contentPadding = PaddingValues(8.dp) - ) { - if (!state.isLoading) { + DefaultTransition(visible = !state.isLoading && !categoryIsLoading) { + LazyVerticalGrid( + columns = GridCells.Adaptive(120.dp), + modifier = Modifier + .fillMaxSize(), + contentPadding = PaddingValues(8.dp) + ) { items( categorizedBooks, key = { it.first.id ?: UUID.randomUUID() } @@ -363,16 +361,6 @@ fun LibraryScreen( } } - if (state.isLoading && !state.isRefreshing) { - CircularProgressIndicator( - color = MaterialTheme.colorScheme.primary, - strokeCap = StrokeCap.Round, - modifier = Modifier - .align(Alignment.Center) - .size(36.dp) - ) - } - AnimatedVisibility( visible = !state.isLoading && !state.isRefreshing && categorizedBooks.isEmpty() && !categoryIsLoading, @@ -404,7 +392,7 @@ fun LibraryScreen( val activity = LocalContext.current as ComponentActivity val scope = rememberCoroutineScope() - var shouldExit = false + var shouldExit = remember { false } BackHandler { if (state.hasSelectedItems) { viewModel.onEvent(LibraryEvent.OnClearSelectedBooks) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryBookItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryBookItem.kt index a097aafc..69db8aaf 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryBookItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryBookItem.kt @@ -25,14 +25,17 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.presentation.data.removeDigits import ua.acclorite.book_story.presentation.data.removeTrailingZero @@ -55,6 +58,15 @@ fun LibraryBookItem( val fontColor = if (book.second) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface + val progress = remember(book.first) { + "${ + (book.first.progress * 100) + .toDouble() + .removeDigits(1) + .removeTrailingZero() + }%" + } + val animatedBackgroundColor by animateColorAsState( targetValue = backgroundColor, tween(300), @@ -93,7 +105,9 @@ fun LibraryBookItem( ) { Icon( imageVector = Icons.Default.Image, - contentDescription = "Cover Image not found", + contentDescription = stringResource( + id = R.string.cover_image_not_found_content_desc + ), modifier = Modifier .align(Alignment.Center) .fillMaxWidth(0.7f) @@ -104,7 +118,7 @@ fun LibraryBookItem( if (book.first.coverImage != null) { Image( bitmap = book.first.coverImage!!, - contentDescription = "Cover", + contentDescription = stringResource(id = R.string.cover_image_content_desc), modifier = Modifier .fillMaxSize() .clip(MaterialTheme.shapes.large), @@ -113,12 +127,7 @@ fun LibraryBookItem( } Text( - "${ - (book.first.progress * 100) - .toDouble() - .removeDigits(1) - .removeTrailingZero() - }%", + progress, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onPrimary, modifier = Modifier diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryTabRow.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryTabRow.kt index 32cda42e..efbc3e97 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryTabRow.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/LibraryTabRow.kt @@ -18,11 +18,12 @@ import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.Book @@ -37,24 +38,27 @@ import ua.acclorite.book_story.ui.elevation @OptIn(ExperimentalFoundationApi::class) @Composable fun LibraryTabRow(viewModel: LibraryViewModel, books: List, pagerState: PagerState) { - val tabItems = listOf( - Pair( - stringResource(id = R.string.reading_tab), - books.filter { it.category == Category.READING }.size - ), - Pair( - stringResource(id = R.string.already_read_tab), - books.filter { it.category == Category.ALREADY_READ }.size - ), - Pair( - stringResource(id = R.string.planning_tab), - books.filter { it.category == Category.PLANNING }.size - ), - Pair( - stringResource(id = R.string.dropped_tab), - books.filter { it.category == Category.DROPPED }.size + val context = LocalContext.current + val tabItems = remember(books) { + listOf( + Pair( + context.getString(R.string.reading_tab), + books.filter { it.category == Category.READING }.size + ), + Pair( + context.getString(R.string.already_read_tab), + books.filter { it.category == Category.ALREADY_READ }.size + ), + Pair( + context.getString(R.string.planning_tab), + books.filter { it.category == Category.PLANNING }.size + ), + Pair( + context.getString(R.string.dropped_tab), + books.filter { it.category == Category.DROPPED }.size + ) ) - ) + } if (LocalConfiguration.current.screenWidthDp > 450) { TabRow( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryViewModel.kt index 4f0c27a4..65020b90 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryViewModel.kt @@ -156,18 +156,25 @@ class LibraryViewModel @Inject constructor( } is LibraryEvent.OnSelectBook -> { - val indexOfBook = _state.value.books.indexOf(event.book) - val editedList = _state.value.books.toMutableList() - editedList[indexOfBook] = editedList[indexOfBook].copy( - second = event.select ?: !editedList[indexOfBook].second - ) + viewModelScope.launch(Dispatchers.IO) { + val indexOfBook = _state.value.books.indexOf(event.book) - _state.update { - it.copy( - books = editedList.toList(), - selectedItemsCount = editedList.filter { book -> book.second }.size, - hasSelectedItems = editedList.any { book -> book.second } + if (indexOfBook == -1) { + return@launch + } + + val editedList = _state.value.books.toMutableList() + editedList[indexOfBook] = editedList[indexOfBook].copy( + second = event.select ?: !editedList[indexOfBook].second ) + + _state.update { + it.copy( + books = editedList.toList(), + selectedItemsCount = editedList.filter { book -> book.second }.size, + hasSelectedItems = editedList.any { book -> book.second } + ) + } } } @@ -252,7 +259,8 @@ class LibraryViewModel @Inject constructor( viewModelScope.launch { _state.update { it.copy( - showDeleteDialog = false + showDeleteDialog = false, + isLoading = true ) } @@ -269,17 +277,20 @@ class LibraryViewModel @Inject constructor( } is LibraryEvent.OnUpdateBook -> { - val books = _state.value.books.toMutableList() + viewModelScope.launch(Dispatchers.IO) { + val books = _state.value.books.toMutableList() + val index = books.indexOfFirst { it.first.id == event.book.id } + + if (index == -1) { + return@launch + } - val index = books.indexOfFirst { it.first.id == event.book.id } - if (index != -1) { books[index] = Pair(event.book, books[index].second) - } - - _state.update { - it.copy( - books = books - ) + _state.update { + it.copy( + books = books + ) + } } } } @@ -300,14 +311,7 @@ class LibraryViewModel @Inject constructor( } } - is Resource.Loading -> { - _state.update { - it.copy( - isLoading = result.isLoading - ) - } - } - + is Resource.Loading -> Unit is Resource.Error -> Unit } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt index 5459a8f9..aed7ebdb 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt @@ -150,16 +150,19 @@ fun ReaderScreen( } ) } + LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemIndex - }.debounce(50).collectLatest { + }.debounce(1000).collectLatest { if (!loading) { + val lastVisibleItemIndex = listState.layoutInfo.visibleItemsInfo.last().index + val progress = if (it > 0) { - if ((it + listState.layoutInfo.visibleItemsInfo.size) >= state.book.text.lastIndex) { + if (lastVisibleItemIndex >= (listState.layoutInfo.totalItemsCount - 1)) { 1f } else { - (it.toFloat() / (state.book.text.size - 1).toFloat()) + (it.toFloat() / (state.book.text.lastIndex).toFloat()) } } else { 0f @@ -264,10 +267,10 @@ fun ReaderScreen( LazyColumn( state = listState, modifier = Modifier + .fillMaxSize() .then( if (!loading && toolbarShowed) { Modifier - .fillMaxSize() .clickable( interactionSource = null, indication = null, @@ -279,7 +282,6 @@ fun ReaderScreen( ) } else { Modifier - .fillMaxSize() } ) ) { @@ -293,7 +295,11 @@ fun ReaderScreen( itemsIndexed( state.book.text, key = { _, key -> key.id } - ) { index, text -> + ) { index, line -> + val text = remember { "${if (paragraphIndentation) " " else ""}${line.line}" } + val color = remember { Color(fontColor.toULong()) } + val lineHeightSp = remember { (fontSize + lineHeight).sp } + Column( Modifier .background(Color(backgroundColor.toULong())) @@ -307,8 +313,8 @@ fun ReaderScreen( ) ) { Text( - text = "${if (paragraphIndentation) " " else ""}${text.line}", - color = Color(fontColor.toULong()), + text = text, + color = color, style = TextStyle( lineBreak = LineBreak.Paragraph @@ -316,7 +322,7 @@ fun ReaderScreen( fontFamily = fontFamily.font, fontStyle = fontStyle, fontSize = fontSize.sp, - lineHeight = (fontSize + lineHeight).sp + lineHeight = lineHeightSp ) } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderStartItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderStartItem.kt index d209e611..e99104be 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderStartItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderStartItem.kt @@ -64,7 +64,7 @@ fun ReaderStartItem(viewModel: ReaderViewModel) { ) { Icon( imageVector = Icons.Default.Image, - contentDescription = "Cover Image not found", + contentDescription = stringResource(id = R.string.cover_image_not_found_content_desc), modifier = Modifier .align(Alignment.Center) .fillMaxWidth(0.7f) @@ -75,7 +75,7 @@ fun ReaderStartItem(viewModel: ReaderViewModel) { if (state.book.coverImage != null) { Image( bitmap = state.book.coverImage!!, - contentDescription = "Cover", + contentDescription = stringResource(id = R.string.cover_image_content_desc), modifier = Modifier .fillMaxSize() .clip(MaterialTheme.shapes.medium), diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderBottomBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderBottomBar.kt index ea21c461..e20b452b 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderBottomBar.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderBottomBar.kt @@ -14,7 +14,9 @@ import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -39,7 +41,15 @@ fun ReaderBottomBar( systemBarsColor: Color ) { val state by viewModel.state.collectAsState() - val book = state.book + val progress by remember(state.book.progress) { + derivedStateOf { + (state.book.progress * 100) + .toDouble() + .removeDigits(4) + .removeTrailingZero() + .dropWhile { it == '-' } + "%" + } + } Column( Modifier @@ -57,17 +67,12 @@ fun ReaderBottomBar( verticalArrangement = Arrangement.Center ) { Text( - text = - (book.progress * 100) - .toDouble() - .removeDigits(4) - .removeTrailingZero() - .dropWhile { it == '-' } + "%", + text = progress, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.titleLarge ) Slider( - value = book.progress, + value = state.book.progress, onValueChange = { viewModel.onEvent( ReaderEvent.OnChangeProgress( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt index 678b341a..80b91f51 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/app_bar/ReaderTopBar.kt @@ -3,20 +3,18 @@ package ua.acclorite.book_story.presentation.screens.reader.components.app_bar import androidx.activity.ComponentActivity import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -27,6 +25,7 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.components.CustomIconButton import ua.acclorite.book_story.presentation.components.GoBackButton import ua.acclorite.book_story.presentation.data.Argument import ua.acclorite.book_story.presentation.data.Navigator @@ -44,7 +43,16 @@ import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColor: Color) { val context = LocalContext.current as ComponentActivity val state by viewModel.state.collectAsState() - val book = state.book + + val progress by remember(state.book.progress) { + derivedStateOf { + (state.book.progress * 100) + .toDouble() + .removeDigits(2) + .removeTrailingZero() + .dropWhile { it == '-' } + "%" + } + } TopAppBar( navigationIcon = { @@ -55,7 +63,7 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo title = { Column(verticalArrangement = Arrangement.Center) { Text( - book.title, + state.book.title, fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 20.sp, @@ -63,7 +71,7 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo maxLines = 1, modifier = Modifier .clickable( - interactionSource = remember { MutableInteractionSource() }, + interactionSource = null, indication = null, onClick = { navigator.navigateWithoutBackStack( @@ -71,7 +79,7 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo true, Argument( "book", - book + state.book ) ) } @@ -83,11 +91,7 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo Text( stringResource( id = R.string.read_query, - (book.progress * 100) - .toDouble() - .removeDigits(2) - .removeTrailingZero() - .dropWhile { it == '-' } + "%" + progress ), color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyLarge @@ -95,13 +99,12 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo } }, actions = { - IconButton(onClick = { + CustomIconButton( + icon = Icons.Default.Settings, + contentDescription = stringResource(id = R.string.open_reader_settings_content_desc), + disableOnClick = false + ) { viewModel.onEvent(ReaderEvent.OnShowHideSettingsBottomSheet) - }) { - Icon( - imageVector = Icons.Default.Settings, - contentDescription = stringResource(id = R.string.open_reader_settings_content_desc), - ) } }, colors = TopAppBarDefaults.topAppBarColors( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheet.kt index fe09675c..3abe97d1 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheet.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheet.kt @@ -74,15 +74,18 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie val backgroundColor = mainViewModel.backgroundColor.collectAsState().value!! val fontColor = mainViewModel.fontColor.collectAsState().value!! - val scrimColor = - if (currentPage == 1) Color.Transparent else BottomSheetDefaults.ScrimColor + val scrimColor = if (currentPage == 1) Color.Transparent + else BottomSheetDefaults.ScrimColor + val animatedScrimColor by animateColorAsState( targetValue = scrimColor, animationSpec = tween(300), label = "Scrim animation" ) - val height = if (currentPage == 1) 0.5f else 0.7f + val height = remember(currentPage) { + if (currentPage == 1) 0.5f else 0.7f + } val animatedHeight by animateFloatAsState( targetValue = height, animationSpec = tween(300), diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderEvent.kt index 92bcc2e9..ec1f6afe 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderEvent.kt @@ -15,7 +15,8 @@ sealed class ReaderEvent { val scrollState: LazyListState, val navigator: Navigator, val refreshList: (Book) -> Unit, - val onLoaded: () -> Unit + val onLoaded: () -> Unit, + val onTextIsEmpty: () -> Unit ) : ReaderEvent() data class OnShowHideMenu(val show: Boolean? = null, val context: ComponentActivity) : diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderState.kt index 5bca3077..318d066b 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderState.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderState.kt @@ -13,9 +13,6 @@ data class ReaderState( val errorMessage: UIText? = null, val showMenu: Boolean = false, - val showSettingsBottomSheet: Boolean = false, val currentPage: Int = 0, - - - ) \ No newline at end of file +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderViewModel.kt index c6d59ae0..042913e7 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/data/ReaderViewModel.kt @@ -26,6 +26,8 @@ import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.History +import ua.acclorite.book_story.domain.model.StringWithId +import ua.acclorite.book_story.domain.use_case.GetText import ua.acclorite.book_story.domain.use_case.InsertHistory import ua.acclorite.book_story.domain.use_case.UpdateBooks import ua.acclorite.book_story.presentation.data.Argument @@ -40,7 +42,8 @@ import kotlin.math.roundToInt class ReaderViewModel @AssistedInject constructor( @Assisted book: Book, private val updateBooks: UpdateBooks, - private val insertHistory: InsertHistory + private val insertHistory: InsertHistory, + private val getText: GetText ) : ViewModel() { private val _state = MutableStateFlow(ReaderState(book)) @@ -59,14 +62,27 @@ class ReaderViewModel @AssistedInject constructor( is ReaderEvent.OnLoadText -> { viewModelScope.launch(Dispatchers.IO) { + val text = getText.execute(_state.value.book.id!!) + + if (text.isBlank()) { + event.onTextIsEmpty() + } + val time = Date().time - val text = _state.value.book.text.joinToString(separator = " ") { it.line } - val letters = text.replace(" ", "").length - val words = text.split("\\s+".toRegex()).size + val letters = text + .replace("\n", "") + .length + val words = text + .replace("\n", " ") + .split("\\s+".toRegex()) + .size _state.update { it.copy( book = it.book.copy( + text = text + .split("\n") + .map { line -> StringWithId(line.trim()) }, lastOpened = time ), letters = letters, @@ -98,14 +114,20 @@ class ReaderViewModel @AssistedInject constructor( val scrollTo = (itemsCount * _state.value.book.progress).roundToInt() if (itemsCount >= _state.value.book.text.size) { if (scrollTo > 0) { - while (true) { + var loaded = false + for (i in 1..100) { try { event.scrollState.scrollToItem(scrollTo) + loaded = true break } catch (e: Exception) { - delay(50) + delay(100) } } + + if (!loaded) { + event.onTextIsEmpty() + } } delay(100) @@ -172,12 +194,12 @@ class ReaderViewModel @AssistedInject constructor( } updateBooks.execute( - listOf(_state.value.book) + listOf(_state.value.book.copy(progress = event.progress)) ) event.navigator.putArgument( - Argument("book", _state.value.book) + Argument("book", _state.value.book.copy(progress = event.progress)) ) - event.refreshList(_state.value.book) + event.refreshList(_state.value.book.copy(progress = event.progress)) } } @@ -290,21 +312,20 @@ class ReaderViewModel @AssistedInject constructor( ReaderState(book = book) } - if (book.text.isEmpty()) { - onEvent(ReaderEvent.OnTextIsEmpty(onLoaded = { onLoaded() })) - } else { - onEvent(ReaderEvent.OnShowHideMenu(false, context)) - onEvent( - ReaderEvent.OnLoadText( - scrollState, - navigator, - refreshList = { refreshList(it) }, - onLoaded = { - onLoaded() - } - ) + onEvent(ReaderEvent.OnShowHideMenu(false, context)) + onEvent( + ReaderEvent.OnLoadText( + scrollState, + navigator, + refreshList = { refreshList(it) }, + onLoaded = { + onLoaded() + }, + onTextIsEmpty = { + onEvent(ReaderEvent.OnTextIsEmpty(onLoaded = { onLoaded() })) + } ) - } + ) } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/SettingsScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/SettingsScreen.kt index c2f2e0bd..43f034fd 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/SettingsScreen.kt @@ -1,7 +1,9 @@ package ua.acclorite.book_story.presentation.screens.settings +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding @@ -21,6 +23,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.components.GoBackButton import ua.acclorite.book_story.presentation.data.Navigator @@ -97,6 +100,8 @@ fun SettingsScreen( navigator.navigate(Screen.READER_SETTINGS, false) } } + + item { Spacer(modifier = Modifier.height(48.dp)) } } } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ColorPickerWithTitle.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ColorPickerWithTitle.kt index 2906f581..c48b675d 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ColorPickerWithTitle.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ColorPickerWithTitle.kt @@ -119,7 +119,7 @@ private fun RevertibleSlider( ) { Icon( imageVector = Icons.Default.History, - contentDescription = "Revert to initial", + contentDescription = stringResource(id = R.string.revert_content_desc), modifier = Modifier .size(28.dp), tint = if (initialValue == value.first) MaterialTheme.colorScheme.onSurfaceVariant diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/AppearanceSettings.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/AppearanceSettings.kt index cff4c1bb..76f360f4 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/AppearanceSettings.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/AppearanceSettings.kt @@ -160,6 +160,8 @@ fun AppearanceSettings( } ) } + + item { Spacer(modifier = Modifier.height(48.dp)) } } } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcher.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcher.kt index 0a73e1b6..9274a3d1 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcher.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcher.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -31,8 +32,10 @@ fun AppearanceSettingsThemeSwitcher( val theme = mainViewModel.theme.collectAsState().value!! val darkTheme = mainViewModel.darkTheme.collectAsState().value!! - val themes = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Constants.THEMES - else Constants.THEMES.dropWhile { it.first == Theme.DYNAMIC } + val themes = remember { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Constants.THEMES + else Constants.THEMES.dropWhile { it.first == Theme.DYNAMIC } + } Column( Modifier diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcherItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcherItem.kt index e4bdd814..75df2813 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcherItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcherItem.kt @@ -27,8 +27,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R import ua.acclorite.book_story.ui.Theme import ua.acclorite.book_story.ui.colorScheme import ua.acclorite.book_story.util.UIText @@ -83,7 +85,7 @@ fun AppearanceSettingsThemeSwitcherItem( Spacer(modifier = Modifier.width(8.dp)) Icon( imageVector = Icons.Default.CheckCircle, - contentDescription = null, + contentDescription = stringResource(id = R.string.selected_content_desc), modifier = Modifier .size(26.dp), tint = diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/GeneralSettings.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/GeneralSettings.kt index 2d2d8160..24e1e168 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/GeneralSettings.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/general/GeneralSettings.kt @@ -1,8 +1,9 @@ package ua.acclorite.book_story.presentation.screens.settings.nested.general -import androidx.activity.ComponentActivity +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding @@ -18,8 +19,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.ChipItem import ua.acclorite.book_story.presentation.components.GoBackButton @@ -42,7 +43,6 @@ fun GeneralSettings( listState.canScrollForward } ) - val context = LocalContext.current as ComponentActivity val language = mainViewModel.language.collectAsState().value!! @@ -84,16 +84,17 @@ fun GeneralSettings( MaterialTheme.typography.labelLarge, it.first == language ) - } + }.sortedBy { it.title } ) { mainViewModel.onEvent( MainEvent.OnChangeLanguage( - it.id, - context + it.id ) ) } } + + item { Spacer(modifier = Modifier.height(48.dp)) } } } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/ReaderSettings.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/ReaderSettings.kt index a1b10ba3..13c36694 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/ReaderSettings.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/ReaderSettings.kt @@ -212,6 +212,8 @@ fun ReaderSettings( ) } } + + item { Spacer(modifier = Modifier.height(48.dp)) } } } } \ No newline at end of file diff --git a/app/src/main/res/drawable/app_icon_colored.png b/app/src/main/res/drawable/app_icon_colored.png index 3615361a..1cc515bb 100644 Binary files a/app/src/main/res/drawable/app_icon_colored.png and b/app/src/main/res/drawable/app_icon_colored.png differ diff --git a/app/src/main/res/drawable/app_icon_monochrome.png b/app/src/main/res/drawable/app_icon_monochrome.png index 0bc46a28..434255d9 100644 Binary files a/app/src/main/res/drawable/app_icon_monochrome.png and b/app/src/main/res/drawable/app_icon_monochrome.png differ diff --git a/app/src/main/res/drawable/browse_screen_outlined.png b/app/src/main/res/drawable/browse_screen_outlined.png index 5f6fcb62..e0616cce 100644 Binary files a/app/src/main/res/drawable/browse_screen_outlined.png and b/app/src/main/res/drawable/browse_screen_outlined.png differ diff --git a/app/src/main/res/drawable/empty_browse.png b/app/src/main/res/drawable/empty_browse.png index 1990e698..f1798e6c 100644 Binary files a/app/src/main/res/drawable/empty_browse.png and b/app/src/main/res/drawable/empty_browse.png differ diff --git a/app/src/main/res/drawable/empty_history.png b/app/src/main/res/drawable/empty_history.png index 897c6ea4..0e5ab0e8 100644 Binary files a/app/src/main/res/drawable/empty_history.png and b/app/src/main/res/drawable/empty_history.png differ diff --git a/app/src/main/res/drawable/empty_library.png b/app/src/main/res/drawable/empty_library.png index 5242d51e..dace3dc0 100644 Binary files a/app/src/main/res/drawable/empty_library.png and b/app/src/main/res/drawable/empty_library.png differ diff --git a/app/src/main/res/drawable/epub.png b/app/src/main/res/drawable/epub.png deleted file mode 100644 index 7a86421a..00000000 Binary files a/app/src/main/res/drawable/epub.png and /dev/null differ diff --git a/app/src/main/res/drawable/error.png b/app/src/main/res/drawable/error.png index 09583052..abee5e8a 100644 Binary files a/app/src/main/res/drawable/error.png and b/app/src/main/res/drawable/error.png differ diff --git a/app/src/main/res/drawable/file.png b/app/src/main/res/drawable/file.png deleted file mode 100644 index aeb33b53..00000000 Binary files a/app/src/main/res/drawable/file.png and /dev/null differ diff --git a/app/src/main/res/drawable/history_screen_filled.png b/app/src/main/res/drawable/history_screen_filled.png index 4a734c66..a2636f1e 100644 Binary files a/app/src/main/res/drawable/history_screen_filled.png and b/app/src/main/res/drawable/history_screen_filled.png differ diff --git a/app/src/main/res/drawable/history_screen_outlined.png b/app/src/main/res/drawable/history_screen_outlined.png index 81560819..0dc0413f 100644 Binary files a/app/src/main/res/drawable/history_screen_outlined.png and b/app/src/main/res/drawable/history_screen_outlined.png differ diff --git a/app/src/main/res/drawable/library_screen_filled.png b/app/src/main/res/drawable/library_screen_filled.png index cda3caea..9a2b898f 100644 Binary files a/app/src/main/res/drawable/library_screen_filled.png and b/app/src/main/res/drawable/library_screen_filled.png differ diff --git a/app/src/main/res/drawable/library_screen_outlined.png b/app/src/main/res/drawable/library_screen_outlined.png index ee576f9f..6f92b5a0 100644 Binary files a/app/src/main/res/drawable/library_screen_outlined.png and b/app/src/main/res/drawable/library_screen_outlined.png differ diff --git a/app/src/main/res/drawable/pdf.png b/app/src/main/res/drawable/pdf.png deleted file mode 100644 index 6996abd1..00000000 Binary files a/app/src/main/res/drawable/pdf.png and /dev/null differ diff --git a/app/src/main/res/drawable/txt.png b/app/src/main/res/drawable/txt.png deleted file mode 100644 index 902f7a04..00000000 Binary files a/app/src/main/res/drawable/txt.png and /dev/null differ diff --git a/app/src/main/res/mipmap-anydpi-v26/app_icon.xml b/app/src/main/res/mipmap-anydpi/app_icon.xml similarity index 100% rename from app/src/main/res/mipmap-anydpi-v26/app_icon.xml rename to app/src/main/res/mipmap-anydpi/app_icon.xml diff --git a/app/src/main/res/mipmap-anydpi-v26/app_icon_round.xml b/app/src/main/res/mipmap-anydpi/app_icon_round.xml similarity index 100% rename from app/src/main/res/mipmap-anydpi-v26/app_icon_round.xml rename to app/src/main/res/mipmap-anydpi/app_icon_round.xml diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index b1f63a7f..a60cebae 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -21,7 +21,7 @@ - Нам потрібен дозвіл на сховище, щоб просканувати вашу теку Завантажень. + Нам потрібен дозвіл на сховище, щоб просканувати Ваш Девайс на наявність книг. Без цього дозволу ви не зможете додати книгу. @@ -50,14 +50,11 @@ - Нам потрібен дозвіл на сховище, щоб просканувати вашу теку Завантажень та отримати книги + Нам потрібен дозвіл на сховище, щоб просканувати Ваш Девайс на наявність книг Немає опису. - - Файл не знайдено. Будь ласка, перевірте чи файл цієї книги все ще існує. - Щось пішло не так. Будь ласка, перевірте чи файл цієї книги не пошкоджений. @@ -186,9 +183,9 @@ Натисніть ще раз, щоб вийти - Всі книги були успішно додані. - Всі книги були успішно переміщені. - Всі книги були успішно видалені. + Всі обрані книги були успішно додані. + Всі обрані книги були успішно переміщені. + Всі обрані книги були успішно видалені. Обкладинка була успішно змінена. Обкладинка була успішно видалена. Назва була успішно змінена. @@ -236,5 +233,23 @@ Назад Обкладинка Відкрити налаштування читача + Анімація фону + Анімація контуру + Анімація верхньої панелі + Файл + Галочка + Показати випадаючий список + Обкладенка не знайдена + Застосувати зміни + Помилка + Пошук + Очистити вибрані предмети + Додати файли + Вийти з пошуку + Видалити всю історію + Перемістити книгу в іншу категорію + Видалити обрані книги + Повернути + Вибрано \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5d8568b1..84ccbde7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ - 0.9.0 + 0.9.2 Book\'s Story @@ -22,7 +22,7 @@ - We need storage permission to scan your Downloads folder. + We need storage permission to scan Your Device for books. Without this permission, you will not be able to add a book. @@ -51,14 +51,11 @@ - We need storage permission to scan your Downloads directory and get books + We need storage permission to scan Your Device for books No description. - - File was not found. Please check whether this book\'s file still exists. - Something went wrong. Please check whether this book\'s file is not corrupted. @@ -186,9 +183,9 @@ Press back again to exit - All books were successfully added. - All books were successfully moved. - All books were successfully deleted. + All selected books were successfully added. + All selected books were successfully moved. + All selected books were successfully deleted. Cover Image was successfully changed. Cover Image was successfully deleted. Title was successfully changed. @@ -236,5 +233,23 @@ Go back Cover image Open reader settings + Background animation + Outline animation + Top app bar animation + File + Checkbox + Show dropdown + Cover image not found + Apply changes + Error + Search + Clear selected items + Add files + Exit search + Delete whole history + Move books to another category + Delete selected books + Revert + Selected \ No newline at end of file