From 1689e9d7b9bcdf88f5521caf65a268664b1da91b Mon Sep 17 00:00:00 2001 From: acclorite Date: Wed, 20 Mar 2024 15:32:20 +0200 Subject: [PATCH] 0.9.4 - About Screen + New scrolling system in Reader(saving exact scroll position) --- app/build.gradle.kts | 2 + app/src/main/AndroidManifest.xml | 20 +- .../java/ua/acclorite/book_story/Activity.kt | 126 +++++--- .../BookApplication.kt => Application.kt} | 4 +- .../acclorite/book_story/data/di/AppModule.kt | 2 +- .../book_story/data/local/dto/BookEntity.kt | 2 + .../book_story/data/local/room/BookDao.kt | 3 + .../data/mapper/book/BookMapperImpl.kt | 8 +- .../data/mapper/history/HistoryMapperImpl.kt | 1 + .../book_story/data/parser/FileParser.kt | 2 +- .../book_story/data/parser/FileParserImpl.kt | 2 +- .../book_story/data/parser/TextParser.kt | 2 +- .../book_story/data/parser/TextParserImpl.kt | 4 +- .../data/parser/epub/EpubFileParser.kt | 8 +- .../data/parser/epub/EpubTextParser.kt | 4 +- .../data/parser/pdf/PdfFileParser.kt | 13 +- .../data/parser/pdf/PdfTextParser.kt | 9 +- .../data/parser/txt/TxtFileParser.kt | 12 +- .../data/parser/txt/TxtTextParser.kt | 4 +- .../data/repository/BookRepositoryImpl.kt | 154 ++++++--- .../acclorite/book_story/domain/model/Book.kt | 11 +- .../book_story/domain/model/FontWithName.kt | 2 +- .../book_story/domain/model/History.kt | 1 + .../book_story/domain/model/NullableBook.kt | 4 +- .../domain/repository/BookRepository.kt | 11 +- .../domain/use_case/ChangeLanguage.kt | 5 +- .../book_story/domain/use_case/GetBooks.kt | 2 +- .../domain/use_case/GetFilesFromDevice.kt | 2 +- .../book_story/domain/use_case/GetHistory.kt | 2 +- .../domain/use_case/GetLatestHistory.kt | 12 + .../book_story/domain/use_case/InsertBooks.kt | 2 +- .../domain/use_case/UpdateCoverImageOfBook.kt | 2 +- .../book_story/{ => domain}/util/Constants.kt | 23 +- .../{ => domain}/util/DataStoreConstants.kt | 3 +- .../book_story/{ => domain}/util/Resource.kt | 2 +- .../book_story/{ => domain}/util/Typealias.kt | 2 +- .../book_story/{ => domain}/util/UIText.kt | 2 +- .../components/AnimatedTopAppBar.kt | 8 +- .../presentation/components/CustomCheckbox.kt | 2 +- .../presentation/components/GoBackButton.kt | 7 +- .../BottomNavigationBar.kt | 2 +- .../custom_dialog/CustomDialogWithContent.kt | 11 +- .../CustomDialogWithLazyColumn.kt | 11 +- .../CustomNavigationRail.kt | 26 +- .../book_story/presentation/data/MainEvent.kt | 2 +- .../book_story/presentation/data/MainState.kt | 104 ++++++ .../presentation/data/MainViewModel.kt | 306 ++++++++---------- .../presentation/data/Navigation.kt | 6 +- .../presentation/screens/about/AboutScreen.kt | 258 +++++++++++++++ .../screens/about/components/AboutItem.kt | 58 ++++ .../screens/about/data/AboutEvent.kt | 11 + .../screens/about/data/AboutViewModel.kt | 43 +++ .../screens/book_info/BookInfoScreen.kt | 39 +-- .../components/BookInfoInfoSection.kt | 6 +- .../components/BookInfoStatisticSection.kt | 10 +- .../BookInfoChangeCoverBottomSheet.kt | 22 +- .../BookInfoDetailsBottomSheet.kt | 4 +- .../components/dialog/BookInfoMoveDialog.kt | 8 +- .../screens/book_info/data/BookInfoEvent.kt | 15 +- .../screens/book_info/data/BookInfoState.kt | 2 +- .../book_info/data/BookInfoViewModel.kt | 63 +++- .../screens/browse/BrowseScreen.kt | 32 +- .../browse/components/BrowseFIleItem.kt | 23 +- .../adding_dialog/BrowseAddingDialog.kt | 5 +- .../adding_dialog/BrowseAddingDialogItem.kt | 12 +- .../screens/browse/data/BrowseEvent.kt | 2 +- .../screens/browse/data/BrowseState.kt | 5 +- .../screens/browse/data/BrowseViewModel.kt | 28 +- .../screens/history/HistoryScreen.kt | 125 ++++--- .../screens/history/components/HistoryItem.kt | 23 +- .../screens/history/data/HistoryEvent.kt | 7 + .../screens/history/data/HistoryState.kt | 6 +- .../screens/history/data/HistoryViewModel.kt | 102 ++++-- .../screens/library/LibraryScreen.kt | 83 ++--- .../library/components/LibraryBookItem.kt | 8 +- .../library/components/LibraryTabRow.kt | 81 +---- .../components/dialog/LibraryMoveDialog.kt | 17 +- .../screens/library/data/LibraryEvent.kt | 10 +- .../screens/library/data/LibraryState.kt | 6 +- .../screens/library/data/LibraryViewModel.kt | 85 +++-- .../screens/reader/ReaderScreen.kt | 133 +++++--- .../reader/components/ReaderEndItem.kt | 44 ++- .../reader/components/ReaderStartItem.kt | 9 +- .../components/app_bar/ReaderBottomBar.kt | 16 +- .../reader/components/app_bar/ReaderTopBar.kt | 59 +++- .../ReaderSettingsBottomSheet.kt | 41 ++- .../screens/reader/data/ReaderEvent.kt | 19 +- .../screens/reader/data/ReaderState.kt | 8 +- .../screens/reader/data/ReaderViewModel.kt | 170 ++++++---- .../screens/settings/SettingsScreen.kt | 8 +- .../nested/appearance/AppearanceSettings.kt | 19 +- .../AppearanceSettingsThemeSwitcher.kt | 14 +- .../AppearanceSettingsThemeSwitcherItem.kt | 6 +- .../nested/general/GeneralSettings.kt | 14 +- .../settings/nested/reader/ReaderSettings.kt | 38 +-- .../book_story/{ => presentation}/ui/Color.kt | 27 +- .../{ => presentation}/ui/Elevation.kt | 2 +- .../book_story/{ => presentation}/ui/Theme.kt | 8 +- .../{ => presentation}/ui/Transitions.kt | 2 +- .../{ => presentation}/ui/Typography.kt | 2 +- .../{ => presentation}/ui/theme/AquaTheme.kt | 2 +- .../{ => presentation}/ui/theme/BlueTheme.kt | 2 +- .../{ => presentation}/ui/theme/GreenTheme.kt | 2 +- .../{ => presentation}/ui/theme/PinkTheme.kt | 2 +- .../ui/theme/PurpleTheme.kt | 2 +- .../{ => presentation}/ui/theme/RedTheme.kt | 2 +- .../ui/theme/YellowTheme.kt | 2 +- app/src/main/res/values-uk/strings.xml | 20 +- app/src/main/res/values/strings.xml | 37 ++- 109 files changed, 1870 insertions(+), 994 deletions(-) rename app/src/main/java/ua/acclorite/book_story/{util/BookApplication.kt => Application.kt} (55%) create mode 100644 app/src/main/java/ua/acclorite/book_story/domain/use_case/GetLatestHistory.kt rename app/src/main/java/ua/acclorite/book_story/{ => domain}/util/Constants.kt (92%) rename app/src/main/java/ua/acclorite/book_story/{ => domain}/util/DataStoreConstants.kt (95%) rename app/src/main/java/ua/acclorite/book_story/{ => domain}/util/Resource.kt (86%) rename app/src/main/java/ua/acclorite/book_story/{ => domain}/util/Typealias.kt (67%) rename app/src/main/java/ua/acclorite/book_story/{ => domain}/util/UIText.kt (94%) create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/data/MainState.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/screens/about/AboutScreen.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/screens/about/components/AboutItem.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutEvent.kt create mode 100644 app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutViewModel.kt rename app/src/main/java/ua/acclorite/book_story/{ => presentation}/ui/Color.kt (72%) rename app/src/main/java/ua/acclorite/book_story/{ => presentation}/ui/Elevation.kt (70%) rename app/src/main/java/ua/acclorite/book_story/{ => presentation}/ui/Theme.kt (93%) rename app/src/main/java/ua/acclorite/book_story/{ => presentation}/ui/Transitions.kt (96%) rename app/src/main/java/ua/acclorite/book_story/{ => presentation}/ui/Typography.kt (97%) rename app/src/main/java/ua/acclorite/book_story/{ => presentation}/ui/theme/AquaTheme.kt (99%) rename app/src/main/java/ua/acclorite/book_story/{ => presentation}/ui/theme/BlueTheme.kt (99%) rename app/src/main/java/ua/acclorite/book_story/{ => presentation}/ui/theme/GreenTheme.kt (99%) rename app/src/main/java/ua/acclorite/book_story/{ => presentation}/ui/theme/PinkTheme.kt (99%) rename app/src/main/java/ua/acclorite/book_story/{ => presentation}/ui/theme/PurpleTheme.kt (99%) rename app/src/main/java/ua/acclorite/book_story/{ => presentation}/ui/theme/RedTheme.kt (99%) rename app/src/main/java/ua/acclorite/book_story/{ => presentation}/ui/theme/YellowTheme.kt (99%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d00a6ede..9517a289 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -137,8 +137,10 @@ dependencies { implementation("org.slf4j:slf4j-android:latest.release") implementation("org.jsoup:jsoup:latest.release") + // App Compat for Language Switcher implementation("androidx.appcompat:appcompat:latest.release") implementation("androidx.appcompat:appcompat-resources:latest.release") + // Coil for loading bitmaps 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 7c9c53e6..0f77188d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,19 +19,24 @@ + + + + + tools:targetApi="tiramisu" + tools:ignore="DataExtractionRules"> + + + + \ No newline at end of file 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 c687bce6..65b14e2e 100644 --- a/app/src/main/java/ua/acclorite/book_story/Activity.kt +++ b/app/src/main/java/ua/acclorite/book_story/Activity.kt @@ -9,6 +9,8 @@ import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme @@ -18,17 +20,20 @@ import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass 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.LocalLayoutDirection +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat 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.MainViewModel import ua.acclorite.book_story.presentation.data.NavigationHost import ua.acclorite.book_story.presentation.data.Screen +import ua.acclorite.book_story.presentation.screens.about.AboutScreen import ua.acclorite.book_story.presentation.screens.book_info.BookInfoScreen import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel import ua.acclorite.book_story.presentation.screens.browse.BrowseScreen @@ -43,11 +48,9 @@ import ua.acclorite.book_story.presentation.screens.settings.SettingsScreen import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.AppearanceSettings import ua.acclorite.book_story.presentation.screens.settings.nested.general.GeneralSettings import ua.acclorite.book_story.presentation.screens.settings.nested.reader.ReaderSettings -import ua.acclorite.book_story.ui.BooksHistoryResurrectionTheme -import ua.acclorite.book_story.ui.DarkTheme -import ua.acclorite.book_story.ui.Theme -import ua.acclorite.book_story.ui.Transitions -import ua.acclorite.book_story.ui.isDark +import ua.acclorite.book_story.presentation.ui.BooksHistoryResurrectionTheme +import ua.acclorite.book_story.presentation.ui.Transitions +import ua.acclorite.book_story.presentation.ui.isDark import java.lang.reflect.Field @@ -89,18 +92,26 @@ class Activity : AppCompatActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) setContent { - val windowClass = calculateWindowSizeClass(activity = this) val isLoaded by mainViewModel.isReady.collectAsState() + val state by mainViewModel.state.collectAsState() - val theme = mainViewModel.theme.collectAsState().value ?: Theme.BLUE - val darkTheme = - mainViewModel.darkTheme.collectAsState().value ?: DarkTheme.FOLLOW_SYSTEM - val tabletUI = windowClass.widthSizeClass != WindowWidthSizeClass.Compact + val windowClass = calculateWindowSizeClass(activity = this) + val tabletUI = remember(windowClass) { + windowClass.widthSizeClass != WindowWidthSizeClass.Compact + } + val localLayoutDirection = LocalLayoutDirection.current + val layoutDirection = remember(localLayoutDirection) { + if (localLayoutDirection == LayoutDirection.Ltr) { + LayoutDirection.Ltr + } else { + LayoutDirection.Rtl + } + } if (isLoaded) { BooksHistoryResurrectionTheme( - theme = theme, - isDark = darkTheme.isDark() + theme = state.theme!!, + isDark = state.darkTheme!!.isDark() ) { NavigationHost(startScreen = Screen.LIBRARY) { val currentScreen by this.getCurrentScreen().collectAsState() @@ -123,42 +134,58 @@ class Activity : AppCompatActivity() { 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, + Box( + modifier = Modifier + .fillMaxSize() + .padding( + end = it.calculateEndPadding( + layoutDirection + ), + bottom = it.calculateBottomPadding(), + start = if (tabletUI) { + 80.dp + it.calculateStartPadding( + layoutDirection + ) + } else { + it.calculateStartPadding( + layoutDirection + ) + }, + ) + ) { + composable(screen = Screen.LIBRARY) { + LibraryScreen( + viewModel = libraryViewModel, + historyViewModel = historyViewModel, + browseViewModel = browseViewModel, + 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 + ) + } + } + + if (tabletUI) { + CustomNavigationRail( navigator = this@NavigationHost, - addedBooks = retrieveArgument("added_books") as? List - ?: emptyList() + layoutDirection = layoutDirection ) } - - composable(screen = Screen.HISTORY) { - HistoryScreen( - viewModel = historyViewModel, - libraryViewModel = libraryViewModel, - navigator = this@NavigationHost - ) - } - - composable(screen = Screen.BROWSE) { - BrowseScreen( - viewModel = browseViewModel, - libraryViewModel = libraryViewModel, - navigator = this@NavigationHost - ) - } - } - - if (tabletUI) { - CustomNavigationRail(navigator = this@NavigationHost) } } } @@ -234,6 +261,15 @@ class Activity : AppCompatActivity() { ) } + // About screen + composable( + screen = Screen.ABOUT, + enterAnim = Transitions.SlidingTransitionIn, + exitAnim = Transitions.SlidingTransitionOut + ) { + AboutScreen(navigator = this@NavigationHost) + } + // Start screen (later) // composable(screen = Screen.START) { // StartScreen() diff --git a/app/src/main/java/ua/acclorite/book_story/util/BookApplication.kt b/app/src/main/java/ua/acclorite/book_story/Application.kt similarity index 55% rename from app/src/main/java/ua/acclorite/book_story/util/BookApplication.kt rename to app/src/main/java/ua/acclorite/book_story/Application.kt index d203292c..5c1589f7 100644 --- a/app/src/main/java/ua/acclorite/book_story/util/BookApplication.kt +++ b/app/src/main/java/ua/acclorite/book_story/Application.kt @@ -1,7 +1,7 @@ -package ua.acclorite.book_story.util +package ua.acclorite.book_story import android.app.Application import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp -class BookApplication : Application() \ No newline at end of file +class Application : Application() \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/di/AppModule.kt b/app/src/main/java/ua/acclorite/book_story/data/di/AppModule.kt index 2202be40..1afd91e0 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/di/AppModule.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/di/AppModule.kt @@ -22,6 +22,6 @@ object AppModule { app, BookDatabase::class.java, "book_db" - ).build().dao + ).allowMainThreadQueries().build().dao } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/dto/BookEntity.kt b/app/src/main/java/ua/acclorite/book_story/data/local/dto/BookEntity.kt index 85810b09..8254b5aa 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/local/dto/BookEntity.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/local/dto/BookEntity.kt @@ -12,6 +12,8 @@ data class BookEntity( val description: String?, val text: String, val filePath: String, + val scrollIndex: Int, + val scrollOffset: Int, val progress: Float, val image: String? = null, val category: Category diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDao.kt b/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDao.kt index e110fb27..50867348 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDao.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDao.kt @@ -43,6 +43,9 @@ interface BookDao { @Query("SELECT * FROM historyentity") suspend fun getHistory(): List + @Query("SELECT * FROM historyentity WHERE bookId = :bookId ORDER BY time DESC LIMIT 1") + fun getLatestHistoryForBook(bookId: Int): HistoryEntity? + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertHistory( history: List 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 95b1c9e0..d2e73a66 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 @@ -4,7 +4,7 @@ import android.net.Uri 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.util.UIText +import ua.acclorite.book_story.domain.util.UIText import java.io.File import javax.inject.Inject @@ -18,6 +18,8 @@ class BookMapperImpl @Inject constructor() : BookMapper { id = book.id, title = book.title, filePath = book.filePath, + scrollIndex = book.scrollIndex, + scrollOffset = book.scrollOffset, progress = book.progress, author = book.author.string, text = textAsString, @@ -37,9 +39,13 @@ class BookMapperImpl @Inject constructor() : BookMapper { R.string.unknown_author ), description = bookEntity.description, + scrollIndex = bookEntity.scrollIndex, + scrollOffset = bookEntity.scrollOffset, progress = bookEntity.progress, file = if (file.exists()) file else null, text = emptyList(), + letters = 0, + words = 0, filePath = bookEntity.filePath, lastOpened = null, category = bookEntity.category, diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapperImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapperImpl.kt index ff6292f8..d8cab3d1 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapperImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/history/HistoryMapperImpl.kt @@ -17,6 +17,7 @@ class HistoryMapperImpl @Inject constructor() : HistoryMapper { return History( historyEntity.id, bookId = historyEntity.bookId, + book = null, time = historyEntity.time ) } diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/FileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/FileParser.kt index 41974c7c..7d7ee00a 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/FileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/FileParser.kt @@ -1,7 +1,7 @@ package ua.acclorite.book_story.data.parser import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.util.CoverImage +import ua.acclorite.book_story.domain.util.CoverImage import java.io.File diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/FileParserImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/FileParserImpl.kt index 27b4f864..069ba80d 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/FileParserImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/FileParserImpl.kt @@ -4,7 +4,7 @@ import ua.acclorite.book_story.data.parser.epub.EpubFileParser import ua.acclorite.book_story.data.parser.pdf.PdfFileParser import ua.acclorite.book_story.data.parser.txt.TxtFileParser import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.util.CoverImage +import ua.acclorite.book_story.domain.util.CoverImage import java.io.File import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/TextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/TextParser.kt index fcee309e..870d523d 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/TextParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/TextParser.kt @@ -1,7 +1,7 @@ package ua.acclorite.book_story.data.parser import ua.acclorite.book_story.domain.model.StringWithId -import ua.acclorite.book_story.util.Resource +import ua.acclorite.book_story.domain.util.Resource import java.io.File interface TextParser { diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/TextParserImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/TextParserImpl.kt index e98b60e5..76362747 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/TextParserImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/TextParserImpl.kt @@ -5,8 +5,8 @@ import ua.acclorite.book_story.data.parser.epub.EpubTextParser import ua.acclorite.book_story.data.parser.pdf.PdfTextParser import ua.acclorite.book_story.data.parser.txt.TxtTextParser import ua.acclorite.book_story.domain.model.StringWithId -import ua.acclorite.book_story.util.Resource -import ua.acclorite.book_story.util.UIText +import ua.acclorite.book_story.domain.util.Resource +import ua.acclorite.book_story.domain.util.UIText import java.io.File import javax.inject.Inject 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 eea796a2..96971bfc 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 @@ -7,8 +7,8 @@ import nl.siegmann.epublib.epub.EpubReader import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.util.CoverImage -import ua.acclorite.book_story.util.UIText +import ua.acclorite.book_story.domain.util.CoverImage +import ua.acclorite.book_story.domain.util.UIText import java.io.File import java.io.FileInputStream import javax.inject.Inject @@ -50,6 +50,10 @@ class EpubFileParser @Inject constructor() : FileParser { author = author, description = description?.toString(), text = emptyList(), + letters = 0, + words = 0, + scrollIndex = 0, + scrollOffset = 0, progress = 0f, file = file, filePath = file.path, 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 dfbdb7af..a39ddc78 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 @@ -7,8 +7,8 @@ import org.jsoup.Jsoup import ua.acclorite.book_story.R import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.domain.model.StringWithId -import ua.acclorite.book_story.util.Resource -import ua.acclorite.book_story.util.UIText +import ua.acclorite.book_story.domain.util.Resource +import ua.acclorite.book_story.domain.util.UIText import java.io.BufferedReader import java.io.File import java.io.FileInputStream diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt index eb458ebb..fbd733fa 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfFileParser.kt @@ -7,9 +7,8 @@ import ua.acclorite.book_story.R import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.domain.model.StringWithId -import ua.acclorite.book_story.util.CoverImage -import ua.acclorite.book_story.util.UIText +import ua.acclorite.book_story.domain.util.CoverImage +import ua.acclorite.book_story.domain.util.UIText import java.io.File import javax.inject.Inject @@ -29,8 +28,6 @@ class PdfFileParser @Inject constructor(private val application: Application) : val fileAuthor = document.documentInformation.author val author = if (fileAuthor != null) UIText.StringValue(fileAuthor) else UIText.StringResource(R.string.unknown_author) - - val text = emptyList() val description = document.documentInformation.subject ?: null document.close() @@ -40,7 +37,11 @@ class PdfFileParser @Inject constructor(private val application: Application) : title = title, author = author, description = description, - text = text, + text = emptyList(), + letters = 0, + words = 0, + scrollIndex = 0, + scrollOffset = 0, progress = 0f, file = file, filePath = file.path, diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfTextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfTextParser.kt index 19759293..e9b1496e 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfTextParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/pdf/PdfTextParser.kt @@ -7,8 +7,8 @@ import com.tom_roush.pdfbox.text.PDFTextStripper import ua.acclorite.book_story.R import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.domain.model.StringWithId -import ua.acclorite.book_story.util.Resource -import ua.acclorite.book_story.util.UIText +import ua.acclorite.book_story.domain.util.Resource +import ua.acclorite.book_story.domain.util.UIText import java.io.File import javax.inject.Inject @@ -28,11 +28,6 @@ class PdfTextParser @Inject constructor(private val application: Application) : val pdfStripper = PDFTextStripper() pdfStripper.paragraphStart = "_new_line_" - if (document.isEncrypted) { - document.close() - return Resource.Error(UIText.StringResource(R.string.error_file_encrypted)) - } - val oldText = pdfStripper.getText(document) .replace("\r", "") diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt index 92cfcfd6..83d26fc8 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtFileParser.kt @@ -4,9 +4,8 @@ import ua.acclorite.book_story.R import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.domain.model.StringWithId -import ua.acclorite.book_story.util.CoverImage -import ua.acclorite.book_story.util.UIText +import ua.acclorite.book_story.domain.util.CoverImage +import ua.acclorite.book_story.domain.util.UIText import java.io.File import javax.inject.Inject @@ -20,14 +19,17 @@ class TxtFileParser @Inject constructor() : FileParser { try { val title = file.name.trim().dropLast(4) val author = UIText.StringResource(R.string.unknown_author) - val text = emptyList() return Book( id = null, title = title, author = author, description = null, - text = text, + text = emptyList(), + letters = 0, + words = 0, + scrollIndex = 0, + scrollOffset = 0, progress = 0f, file = file, filePath = file.path, diff --git a/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtTextParser.kt b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtTextParser.kt index d1756dda..303abad7 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtTextParser.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/parser/txt/TxtTextParser.kt @@ -5,8 +5,8 @@ import kotlinx.coroutines.withContext import ua.acclorite.book_story.R import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.domain.model.StringWithId -import ua.acclorite.book_story.util.Resource -import ua.acclorite.book_story.util.UIText +import ua.acclorite.book_story.domain.util.Resource +import ua.acclorite.book_story.domain.util.UIText import java.io.BufferedReader import java.io.File import java.io.FileReader 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 d8d6e28a..d6b24f9e 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 @@ -5,11 +5,12 @@ import android.graphics.Bitmap import android.net.Uri import android.os.Environment import androidx.datastore.preferences.core.Preferences +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.withContext import ua.acclorite.book_story.R import ua.acclorite.book_story.data.local.data_store.DataStore -import ua.acclorite.book_story.data.local.dto.BookEntity import ua.acclorite.book_story.data.local.room.BookDao import ua.acclorite.book_story.data.mapper.book.BookMapper import ua.acclorite.book_story.data.mapper.history.HistoryMapper @@ -20,12 +21,13 @@ import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.model.StringWithId import ua.acclorite.book_story.domain.repository.BookRepository +import ua.acclorite.book_story.domain.util.Constants +import ua.acclorite.book_story.domain.util.CoverImage +import ua.acclorite.book_story.domain.util.Resource +import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.presentation.data.calculateFamiliarity -import ua.acclorite.book_story.util.Constants -import ua.acclorite.book_story.util.CoverImage -import ua.acclorite.book_story.util.Resource -import ua.acclorite.book_story.util.UIText import java.io.File +import java.io.FileOutputStream import java.util.UUID import javax.inject.Inject import javax.inject.Singleton @@ -47,17 +49,15 @@ class BookRepositoryImpl @Inject constructor( override suspend fun getBooks(query: String): Flow>> { return flow { emit(Resource.Loading(true)) - val books = database.searchBooks(query) - val history = database.getHistory() emit( Resource.Success( data = books.map { entity -> val book = bookMapper.toBook(entity) - val lastHistory = history - .filter { it.bookId == book.id } - .maxByOrNull { it.time } + val lastHistory = database.getLatestHistoryForBook( + book.id ?: -1 + ) book.copy( lastOpened = lastHistory?.time @@ -70,12 +70,12 @@ class BookRepositoryImpl @Inject constructor( override suspend fun fastGetBooks(query: String): List { val books = database.searchBooks(query) - val history = database.getHistory() return books.map { entity -> val book = bookMapper.toBook(entity) - val lastHistory = - history.filter { it.bookId == book.id }.maxByOrNull { it.time } + val lastHistory = database.getLatestHistoryForBook( + book.id ?: -1 + ) book.copy( lastOpened = lastHistory?.time @@ -84,7 +84,18 @@ class BookRepositoryImpl @Inject constructor( } override suspend fun getBooksById(ids: List): List { - return database.findBooksById(ids).map { bookMapper.toBook(it) } + val books = database.findBooksById(ids) + + return books.map { entity -> + val book = bookMapper.toBook(entity) + val lastHistory = database.getLatestHistoryForBook( + book.id ?: -1 + ) + + book.copy( + lastOpened = lastHistory?.time + ) + } } override suspend fun getBookTextById(bookId: Int): String { @@ -92,30 +103,35 @@ class BookRepositoryImpl @Inject constructor( return book.text } - override suspend fun findBook( - id: Int - ): BookEntity { - return database.findBookById(id) - } - override suspend fun insertBooks( books: List> ) { + val filesDir = application.filesDir + val coversDir = File(filesDir, "covers") + + if (!coversDir.exists()) { + coversDir.mkdirs() + } + val booksWithCover = books.map { var uri = "" if (it.second != null) { try { uri = "${UUID.randomUUID()}.webp" - application.openFileOutput(uri, 0x0000).use { stream -> - if ( - !it.second!!.compress( - Bitmap.CompressFormat.WEBP, - 30, - stream - ) - ) { - throw Exception("Couldn't save cover image") + val cover = File(coversDir, uri) + + withContext(Dispatchers.IO) { + FileOutputStream(cover).use { stream -> + if ( + !it.second!!.compress( + Bitmap.CompressFormat.WEBP, + 30, + stream + ) + ) { + throw Exception("Couldn't save cover image") + } } } } catch (e: Exception) { @@ -126,7 +142,7 @@ class BookRepositoryImpl @Inject constructor( val book = it.first.copy( coverImage = if (uri.isNotBlank()) { - Uri.fromFile(File("${application.filesDir}/$uri")) + Uri.fromFile(File("$coversDir/$uri")) } else null ) @@ -175,18 +191,29 @@ class BookRepositoryImpl @Inject constructor( val book = database.findBookById(bookWithOldCover.id ?: return) var uri: String? = null + val filesDir = application.filesDir + val coversDir = File(filesDir, "covers") + + if (!coversDir.exists()) { + coversDir.mkdirs() + } + if (newCoverImage != null) { try { uri = "${UUID.randomUUID()}.webp" - application.openFileOutput(uri, 0x0000).use { stream -> - if ( - !newCoverImage.compress( - Bitmap.CompressFormat.WEBP, - 30, - stream - ) - ) { - throw Exception("Couldn't save cover image") + val cover = File(coversDir, uri) + + withContext(Dispatchers.IO) { + FileOutputStream(cover).use { stream -> + if ( + !newCoverImage.compress( + Bitmap.CompressFormat.WEBP, + 30, + stream + ) + ) { + throw Exception("Couldn't save cover image") + } } } } catch (e: Exception) { @@ -197,16 +224,20 @@ class BookRepositoryImpl @Inject constructor( if (book.image != null) { try { - application.deleteFile( - book.image.substringAfterLast("/") + val fileToDelete = File( + "$coversDir/${book.image.substringAfterLast("/")}" ) + + if (fileToDelete.exists()) { + fileToDelete.delete() + } } catch (e: Exception) { e.printStackTrace() } } val newCoverImageUri = if (uri != null) { - Uri.fromFile(File("${application.filesDir}/$uri")) + Uri.fromFile(File("$coversDir/$uri")) } else { null } @@ -228,15 +259,26 @@ class BookRepositoryImpl @Inject constructor( } override suspend fun deleteBooks(books: List) { + val filesDir = application.filesDir + val coversDir = File(filesDir, "covers") + + if (!coversDir.exists()) { + coversDir.mkdirs() + } + database.deleteBooks( books.map { val book = database.findBookById(it.id ?: return) if (book.image != null) { try { - application.deleteFile( - book.image.substringAfterLast("/") + val fileToDelete = File( + "$coversDir/${book.image.substringAfterLast("/")}" ) + + if (fileToDelete.exists()) { + fileToDelete.delete() + } } catch (e: Exception) { e.printStackTrace() } @@ -301,14 +343,28 @@ class BookRepositoryImpl @Inject constructor( ignoreCase = true ) } - val isFileAlreadyAdded = existingBooks.all { + + if (!isFileSupported) { + return@filter false + } + + val isFileNotAdded = existingBooks.all { it.filePath.substringAfterLast("/") != file.path.substringAfterLast("/") } + + if (!isFileNotAdded) { + return@filter false + } + val isQuery = if (query.isEmpty()) true else file.name.lowercase() .contains(query.trim().lowercase()) - isFileAlreadyAdded && isFileSupported && isQuery + if (!isQuery) { + return@filter false + } + + true }.forEach { filteredFiles.add( it @@ -414,7 +470,6 @@ class BookRepositoryImpl @Inject constructor( override suspend fun getHistory(): Flow>> { return flow { emit(Resource.Loading(true)) - val history = database.getHistory() emit( @@ -429,6 +484,11 @@ class BookRepositoryImpl @Inject constructor( } } + override suspend fun getLatestBookHistory(bookId: Int): History? { + val history = database.getLatestHistoryForBook(bookId) + return history?.let { historyMapper.toHistory(it) } + } + override suspend fun deleteWholeHistory() { database.deleteWholeHistory() } diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/Book.kt b/app/src/main/java/ua/acclorite/book_story/domain/model/Book.kt index 124612bc..20b38271 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/Book.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/model/Book.kt @@ -2,17 +2,26 @@ package ua.acclorite.book_story.domain.model import android.net.Uri import androidx.compose.runtime.Immutable -import ua.acclorite.book_story.util.UIText +import ua.acclorite.book_story.domain.util.UIText import java.io.File @Immutable data class Book( val id: Int?, + val title: String, val author: UIText, val description: String?, + +// val textPath: String, val text: List = emptyList(), + val letters: Int, + val words: Int, + + val scrollIndex: Int, + val scrollOffset: Int, val progress: Float, + val file: File?, val filePath: String, val lastOpened: Long?, diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/FontWithName.kt b/app/src/main/java/ua/acclorite/book_story/domain/model/FontWithName.kt index a6242f1b..fd1fef61 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/FontWithName.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/model/FontWithName.kt @@ -2,7 +2,7 @@ package ua.acclorite.book_story.domain.model import androidx.compose.runtime.Immutable import androidx.compose.ui.text.font.FontFamily -import ua.acclorite.book_story.util.UIText +import ua.acclorite.book_story.domain.util.UIText @Immutable data class FontWithName( diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/History.kt b/app/src/main/java/ua/acclorite/book_story/domain/model/History.kt index 4bf92477..46b82b99 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/History.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/model/History.kt @@ -6,5 +6,6 @@ import androidx.compose.runtime.Immutable data class History( val id: Int?, val bookId: Int, + val book: Book?, val time: Long ) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/NullableBook.kt b/app/src/main/java/ua/acclorite/book_story/domain/model/NullableBook.kt index 88ee5acb..39ab8b1b 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/model/NullableBook.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/model/NullableBook.kt @@ -1,8 +1,8 @@ package ua.acclorite.book_story.domain.model import androidx.compose.runtime.Immutable -import ua.acclorite.book_story.util.CoverImage -import ua.acclorite.book_story.util.UIText +import ua.acclorite.book_story.domain.util.CoverImage +import ua.acclorite.book_story.domain.util.UIText @Immutable sealed class NullableBook( 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 a7d8853e..b2f43946 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 @@ -2,12 +2,11 @@ package ua.acclorite.book_story.domain.repository import androidx.datastore.preferences.core.Preferences import kotlinx.coroutines.flow.Flow -import ua.acclorite.book_story.data.local.dto.BookEntity import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.NullableBook -import ua.acclorite.book_story.util.CoverImage -import ua.acclorite.book_story.util.Resource +import ua.acclorite.book_story.domain.util.CoverImage +import ua.acclorite.book_story.domain.util.Resource import java.io.File interface BookRepository { @@ -28,10 +27,6 @@ interface BookRepository { bookId: Int ): String - suspend fun findBook( - id: Int - ): BookEntity - suspend fun insertBooks( books: List> ) @@ -71,6 +66,8 @@ interface BookRepository { suspend fun getHistory(): Flow>> + suspend fun getLatestBookHistory(bookId: Int): History? + suspend fun deleteWholeHistory() suspend fun deleteBookHistory(bookId: Int) 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 f9c4ef1e..c7799c12 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,14 +1,13 @@ package ua.acclorite.book_story.domain.use_case -import android.annotation.SuppressLint 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 ua.acclorite.book_story.domain.util.DataStoreConstants import javax.inject.Inject class ChangeLanguage @Inject constructor(private val repository: BookRepository) { - @SuppressLint("AppBundleLocaleChanges") + suspend fun execute(language: String) { val appLocale = LocaleListCompat.forLanguageTags(language) AppCompatDelegate.setApplicationLocales(appLocale) diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBooks.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBooks.kt index c6eeb698..76af9fb6 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBooks.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetBooks.kt @@ -3,7 +3,7 @@ package ua.acclorite.book_story.domain.use_case import kotlinx.coroutines.flow.Flow import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.repository.BookRepository -import ua.acclorite.book_story.util.Resource +import ua.acclorite.book_story.domain.util.Resource import javax.inject.Inject class GetBooks @Inject constructor(private val repository: BookRepository) { diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetFilesFromDevice.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetFilesFromDevice.kt index 2afd126e..83806822 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetFilesFromDevice.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetFilesFromDevice.kt @@ -2,7 +2,7 @@ package ua.acclorite.book_story.domain.use_case import kotlinx.coroutines.flow.Flow import ua.acclorite.book_story.domain.repository.BookRepository -import ua.acclorite.book_story.util.Resource +import ua.acclorite.book_story.domain.util.Resource import java.io.File import javax.inject.Inject diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetHistory.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetHistory.kt index 2745db62..bf7b1efc 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetHistory.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetHistory.kt @@ -3,7 +3,7 @@ package ua.acclorite.book_story.domain.use_case import kotlinx.coroutines.flow.Flow import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.repository.BookRepository -import ua.acclorite.book_story.util.Resource +import ua.acclorite.book_story.domain.util.Resource import javax.inject.Inject class GetHistory @Inject constructor(private val repository: BookRepository) { diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetLatestHistory.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetLatestHistory.kt new file mode 100644 index 00000000..23bc1682 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetLatestHistory.kt @@ -0,0 +1,12 @@ +package ua.acclorite.book_story.domain.use_case + +import ua.acclorite.book_story.domain.model.History +import ua.acclorite.book_story.domain.repository.BookRepository +import javax.inject.Inject + +class GetLatestHistory @Inject constructor(private val repository: BookRepository) { + + suspend fun execute(bookId: Int): History? { + return repository.getLatestBookHistory(bookId) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/InsertBooks.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/InsertBooks.kt index 5caeabcb..028c4441 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/InsertBooks.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/InsertBooks.kt @@ -2,7 +2,7 @@ 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 ua.acclorite.book_story.util.CoverImage +import ua.acclorite.book_story.domain.util.CoverImage import javax.inject.Inject class InsertBooks @Inject constructor(private val repository: BookRepository) { diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/UpdateCoverImageOfBook.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/UpdateCoverImageOfBook.kt index 4e574d7d..db25f60b 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/use_case/UpdateCoverImageOfBook.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/UpdateCoverImageOfBook.kt @@ -2,7 +2,7 @@ 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 ua.acclorite.book_story.util.CoverImage +import ua.acclorite.book_story.domain.util.CoverImage import javax.inject.Inject class UpdateCoverImageOfBook @Inject constructor(private val repository: BookRepository) { diff --git a/app/src/main/java/ua/acclorite/book_story/util/Constants.kt b/app/src/main/java/ua/acclorite/book_story/domain/util/Constants.kt similarity index 92% rename from app/src/main/java/ua/acclorite/book_story/util/Constants.kt rename to app/src/main/java/ua/acclorite/book_story/domain/util/Constants.kt index fd69ea8e..8483c930 100644 --- a/app/src/main/java/ua/acclorite/book_story/util/Constants.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/util/Constants.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.util +package ua.acclorite.book_story.domain.util import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily @@ -8,23 +8,12 @@ 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.FontWithName -import ua.acclorite.book_story.ui.Theme +import ua.acclorite.book_story.presentation.ui.Theme object Constants { - // SavedStateHandle constants(actually just all settings params) - const val LANGUAGE = "language" - const val THEME = "theme" - const val DARK_THEME = "dark_theme" - const val SHOW_START_SCREEN = "guide" - const val BACKGROUND_COLOR = "background_color" - const val FONT_COLOR = "font_color" - const val FONT = "font" - const val IS_ITALIC = "font_style" - const val FONT_SIZE = "font_size" - const val LINE_HEIGHT = "line_height" - const val PARAGRAPH_HEIGHT = "paragraph_height" - const val PARAGRAPH_INDENTATION = "paragraph_indentation" + // Main State Constant + const val MAIN_STATE = "main_state" // Supported file extensions val EXTENSIONS = listOf(".txt", ".pdf", ".epub") @@ -182,6 +171,10 @@ object Constants { UIText.StringValue(""), null, emptyList(), + 0, + 0, + 0, + 0, 0f, null, "", diff --git a/app/src/main/java/ua/acclorite/book_story/util/DataStoreConstants.kt b/app/src/main/java/ua/acclorite/book_story/domain/util/DataStoreConstants.kt similarity index 95% rename from app/src/main/java/ua/acclorite/book_story/util/DataStoreConstants.kt rename to app/src/main/java/ua/acclorite/book_story/domain/util/DataStoreConstants.kt index d350dfac..365d26a0 100644 --- a/app/src/main/java/ua/acclorite/book_story/util/DataStoreConstants.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/util/DataStoreConstants.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.util +package ua.acclorite.book_story.domain.util import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey @@ -10,7 +10,6 @@ object DataStoreConstants { val THEME = stringPreferencesKey("theme") val DARK_THEME = stringPreferencesKey("dark_theme") val SHOW_START_SCREEN = booleanPreferencesKey("guide") - val BACKGROUND_COLOR = longPreferencesKey("background_color") val FONT_COLOR = longPreferencesKey("font_color") val FONT = stringPreferencesKey("font") diff --git a/app/src/main/java/ua/acclorite/book_story/util/Resource.kt b/app/src/main/java/ua/acclorite/book_story/domain/util/Resource.kt similarity index 86% rename from app/src/main/java/ua/acclorite/book_story/util/Resource.kt rename to app/src/main/java/ua/acclorite/book_story/domain/util/Resource.kt index 1edb7509..60f0705e 100644 --- a/app/src/main/java/ua/acclorite/book_story/util/Resource.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/util/Resource.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.util +package ua.acclorite.book_story.domain.util sealed class Resource(val data: T? = null, val message: UIText? = null) { class Success(data: T?) : Resource(data) diff --git a/app/src/main/java/ua/acclorite/book_story/util/Typealias.kt b/app/src/main/java/ua/acclorite/book_story/domain/util/Typealias.kt similarity index 67% rename from app/src/main/java/ua/acclorite/book_story/util/Typealias.kt rename to app/src/main/java/ua/acclorite/book_story/domain/util/Typealias.kt index 21734d6f..7855cb8a 100644 --- a/app/src/main/java/ua/acclorite/book_story/util/Typealias.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/util/Typealias.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.util +package ua.acclorite.book_story.domain.util import android.graphics.Bitmap diff --git a/app/src/main/java/ua/acclorite/book_story/util/UIText.kt b/app/src/main/java/ua/acclorite/book_story/domain/util/UIText.kt similarity index 94% rename from app/src/main/java/ua/acclorite/book_story/util/UIText.kt rename to app/src/main/java/ua/acclorite/book_story/domain/util/UIText.kt index 223ccf24..a95beeea 100644 --- a/app/src/main/java/ua/acclorite/book_story/util/UIText.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/util/UIText.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.util +package ua.acclorite.book_story.domain.util import android.content.Context import androidx.annotation.StringRes 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 b1322265..424e449f 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 @@ -28,7 +28,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R -import ua.acclorite.book_story.ui.elevation +import ua.acclorite.book_story.presentation.ui.elevation /** * Animated top app bar. Has up to 3 different top bars inside it. Follow [TopAppBar] for more info. @@ -102,10 +102,12 @@ fun AnimatedTopAppBar( exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow)) ) { TopAppBar( + windowInsets = WindowInsets.statusBars, navigationIcon = content1NavigationIcon, title = content1Title, actions = content1Actions, scrollBehavior = scrollBehavior, + modifier = Modifier.fillMaxWidth(), colors = TopAppBarDefaults.topAppBarColors( containerColor = animatedContainerColor, scrolledContainerColor = scrolledContainerColor @@ -120,10 +122,12 @@ fun AnimatedTopAppBar( exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow)) ) { TopAppBar( + windowInsets = WindowInsets.statusBars, navigationIcon = content2NavigationIcon, title = content2Title, actions = content2Actions, scrollBehavior = scrollBehavior, + modifier = Modifier.fillMaxWidth(), colors = TopAppBarDefaults.topAppBarColors( containerColor = animatedContainerColor, scrolledContainerColor = scrolledContainerColor @@ -139,10 +143,12 @@ fun AnimatedTopAppBar( exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow)) ) { TopAppBar( + windowInsets = WindowInsets.statusBars, navigationIcon = content3NavigationIcon, title = content3Title, actions = content3Actions, scrollBehavior = scrollBehavior, + modifier = Modifier.fillMaxWidth(), colors = TopAppBarDefaults.topAppBarColors( 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 3c2cc67a..94ddcb5d 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 @@ -17,7 +17,7 @@ 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 +import ua.acclorite.book_story.presentation.ui.elevation /** * Custom Checkbox. Has a Circle shape. 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 c11e8360..58fee9b1 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 @@ -11,13 +11,14 @@ import ua.acclorite.book_story.presentation.data.Navigator * Go back arrow button. Prevents double or triple clicking on it while going back action is performed. */ @Composable -fun GoBackButton(navigator: Navigator, customOnClick: () -> Unit = {}) { +fun GoBackButton(navigator: Navigator, enabled: Boolean = true, customOnClick: () -> Unit = {}) { CustomIconButton( icon = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(id = R.string.go_back_content_desc), - disableOnClick = true + disableOnClick = true, + enabled = enabled ) { - navigator.navigateBack() customOnClick() + navigator.navigateBack() } } \ No newline at end of file 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 f154b5d1..db283aa6 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 @@ -14,7 +14,7 @@ import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.NavigationItem import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Screen -import ua.acclorite.book_story.ui.elevation +import ua.acclorite.book_story.presentation.ui.elevation /** * Bottom navigation bar, uses default [NavigationBar]. 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 cba44a90..1a631584 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 @@ -36,14 +36,13 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogWindowProvider import ua.acclorite.book_story.R -import ua.acclorite.book_story.ui.ElevationDefaults -import ua.acclorite.book_story.ui.elevation +import ua.acclorite.book_story.presentation.ui.ElevationDefaults +import ua.acclorite.book_story.presentation.ui.elevation /** * Custom Dialog with custom content. * * @param modifier Modifier to be applied. - * @param properties Dialog properties. * @param drawableIcon Drawable icon to show(1/2). * @param imageVectorIcon Image vector icon to show(2/2). * @param backgroundTransparency The transparency of the background behind the dialog. @@ -60,7 +59,6 @@ import ua.acclorite.book_story.ui.elevation @Composable fun CustomDialogWithContent( modifier: Modifier = Modifier, - properties: DialogProperties = DialogProperties(), drawableIcon: Painter? = null, imageVectorIcon: ImageVector? = null, backgroundTransparency: Float = 0.3f, @@ -76,7 +74,10 @@ fun CustomDialogWithContent( var actionClicked by remember { mutableStateOf(false) } AlertDialog( onDismissRequest = { onDismiss() }, - properties = properties + properties = DialogProperties( + dismissOnBackPress = !actionClicked, + dismissOnClickOutside = !actionClicked + ) ) { (LocalView.current.parent as DialogWindowProvider).window.setDimAmount( backgroundTransparency 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 cce7512f..5f7c8ce0 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 @@ -37,15 +37,14 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogWindowProvider import ua.acclorite.book_story.R -import ua.acclorite.book_story.ui.ElevationDefaults -import ua.acclorite.book_story.ui.elevation +import ua.acclorite.book_story.presentation.ui.ElevationDefaults +import ua.acclorite.book_story.presentation.ui.elevation /** * Custom Dialog with lazy column. * * @param modifier Modifier to be applied. - * @param properties Dialog properties. * @param drawableIcon Drawable icon to show(1/2). * @param imageVectorIcon Image vector icon to show(2/2). * @param backgroundTransparency The transparency of the background behind the dialog. @@ -62,7 +61,6 @@ import ua.acclorite.book_story.ui.elevation @Composable fun CustomDialogWithLazyColumn( modifier: Modifier = Modifier, - properties: DialogProperties = DialogProperties(), drawableIcon: Painter? = null, imageVectorIcon: ImageVector? = null, backgroundTransparency: Float = 0.3f, @@ -78,7 +76,10 @@ fun CustomDialogWithLazyColumn( var actionClicked by remember { mutableStateOf(false) } AlertDialog( onDismissRequest = { onDismiss() }, - properties = properties + properties = DialogProperties( + dismissOnBackPress = !actionClicked, + dismissOnClickOutside = !actionClicked + ) ) { (LocalView.current.parent as DialogWindowProvider).window.setDimAmount( backgroundTransparency diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRail.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRail.kt index 939d68ea..8bc8a27a 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRail.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRail.kt @@ -2,8 +2,13 @@ package ua.acclorite.book_story.presentation.components.custom_navigation_rail import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material3.MaterialTheme @@ -18,19 +23,21 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.NavigationItem import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Screen -import ua.acclorite.book_story.ui.elevation +import ua.acclorite.book_story.presentation.ui.elevation /** * Custom Navigation Rail. It is used to be shown on Tablets. */ @Composable -fun CustomNavigationRail( - navigator: Navigator +fun BoxScope.CustomNavigationRail( + navigator: Navigator, + layoutDirection: LayoutDirection ) { var currentScreen: Screen? by remember { mutableStateOf(null) } @@ -44,10 +51,19 @@ fun CustomNavigationRail( NavigationRail( modifier = Modifier - .width(80.dp) + .align( + Alignment.CenterStart + ) .background(MaterialTheme.elevation()) + .padding( + start = WindowInsets.navigationBars + .asPaddingValues() + .calculateStartPadding(layoutDirection) + ) + .width(80.dp) .padding(horizontal = 12.dp), - containerColor = MaterialTheme.elevation() + containerColor = MaterialTheme.elevation(), + windowInsets = WindowInsets(0, 0, 0, 0) ) { Column( Modifier 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 dfd3ba13..a3c965eb 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 @@ -12,5 +12,5 @@ sealed class MainEvent { data class OnChangeParagraphIndentation(val bool: Boolean) : MainEvent() data class OnChangeBackgroundColor(val color: Long) : MainEvent() data class OnChangeFontColor(val color: Long) : MainEvent() - + data class OnChangeShowStartScreen(val bool: Boolean) : MainEvent() } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainState.kt new file mode 100644 index 00000000..0791ebfe --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainState.kt @@ -0,0 +1,104 @@ +package ua.acclorite.book_story.presentation.data + +import android.os.Parcel +import android.os.Parcelable +import androidx.compose.runtime.Immutable +import ua.acclorite.book_story.presentation.ui.DarkTheme +import ua.acclorite.book_story.presentation.ui.Theme + +/** + * Main State. All app's settings are here. Wrapped in SavedStateHandle, so it won't reset. + */ +@Immutable +data class MainState( + val language: String? = null, + val theme: Theme? = null, + val darkTheme: DarkTheme? = null, + val fontFamily: String? = null, + val isItalic: Boolean? = null, + val fontSize: Int? = null, + val lineHeight: Int? = null, + val paragraphHeight: Int? = null, + val paragraphIndentation: Boolean? = null, + val backgroundColor: Long? = null, + val fontColor: Long? = null, + val showStartScreen: Boolean? = null, +) : Parcelable { + constructor(parcel: Parcel) : this( + // String + language = parcel.readString(), + // String + theme = Theme.valueOf(parcel.readString() ?: Theme.BLUE.name), + // String + darkTheme = DarkTheme.valueOf(parcel.readString() ?: DarkTheme.FOLLOW_SYSTEM.name), + // String + fontFamily = parcel.readString(), + // Boolean + isItalic = if (parcel.readByte().toInt() != 0) parcel.readByte().toInt() == 1 else null, + // Int + fontSize = if (parcel.readByte().toInt() != 0) parcel.readInt() else null, + // Int + lineHeight = if (parcel.readByte().toInt() != 0) parcel.readInt() else null, + // Int + paragraphHeight = if (parcel.readByte().toInt() != 0) parcel.readInt() else null, + // Boolean + paragraphIndentation = if (parcel.readByte().toInt() != 0) parcel.readByte() + .toInt() == 1 else null, + // Long + backgroundColor = if (parcel.readByte().toInt() != 0) parcel.readLong() else null, + // Long + fontColor = if (parcel.readByte().toInt() != 0) parcel.readLong() else null, + // Boolean + showStartScreen = if (parcel.readByte().toInt() != 0) parcel.readByte() + .toInt() == 1 else null, + ) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + // Language + parcel.writeString(language) + // Theme + parcel.writeString(theme?.name) + // Dark Theme + parcel.writeString(darkTheme?.name) + // Font Family + parcel.writeString(fontFamily) + // Is Italic + parcel.writeByte((if (isItalic != null) 1 else 0).toByte()) + isItalic?.let { parcel.writeByte(if (it) 1 else 0) } + // Font Size + parcel.writeByte((if (fontSize != null) 1 else 0).toByte()) + fontSize?.let { parcel.writeInt(it) } + // Line Height + parcel.writeByte((if (lineHeight != null) 1 else 0).toByte()) + lineHeight?.let { parcel.writeInt(it) } + // Paragraph Height + parcel.writeByte((if (paragraphHeight != null) 1 else 0).toByte()) + paragraphHeight?.let { parcel.writeInt(it) } + // Paragraph Indentation + parcel.writeByte((if (paragraphIndentation != null) 1 else 0).toByte()) + paragraphIndentation?.let { parcel.writeByte(if (it) 1 else 0) } + // Background color + parcel.writeByte((if (backgroundColor != null) 1 else 0).toByte()) + backgroundColor?.let { parcel.writeLong(it) } + // Font color + parcel.writeByte((if (fontColor != null) 1 else 0).toByte()) + fontColor?.let { parcel.writeLong(it) } + // Show Start Screen + parcel.writeByte((if (showStartScreen != null) 1 else 0).toByte()) + showStartScreen?.let { parcel.writeByte(if (it) 1 else 0) } + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): MainState { + return MainState(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file 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 87831515..b31313c3 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 @@ -8,7 +8,6 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first @@ -17,13 +16,13 @@ import kotlinx.coroutines.launch import ua.acclorite.book_story.domain.use_case.ChangeLanguage import ua.acclorite.book_story.domain.use_case.GetDatastore import ua.acclorite.book_story.domain.use_case.SetDatastore +import ua.acclorite.book_story.domain.util.Constants +import ua.acclorite.book_story.domain.util.DataStoreConstants import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel -import ua.acclorite.book_story.ui.DarkTheme -import ua.acclorite.book_story.ui.Theme -import ua.acclorite.book_story.ui.toDarkTheme -import ua.acclorite.book_story.ui.toTheme -import ua.acclorite.book_story.util.Constants -import ua.acclorite.book_story.util.DataStoreConstants +import ua.acclorite.book_story.presentation.ui.DarkTheme +import ua.acclorite.book_story.presentation.ui.Theme +import ua.acclorite.book_story.presentation.ui.toDarkTheme +import ua.acclorite.book_story.presentation.ui.toTheme import java.util.Locale import javax.inject.Inject @@ -42,212 +41,143 @@ class MainViewModel @Inject constructor( private val _isReady = MutableStateFlow(false) val isReady = _isReady.asStateFlow() - /* -- Language ----------------------------------------------------- */ - private var _language: String = - 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 - } - val language: StateFlow - get() = stateHandle.getStateFlow(Constants.LANGUAGE, null) - - /* -- Theme -------------------------------------------------------- */ - private var _theme: Theme = - stateHandle[Constants.THEME] - ?: if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Theme.DYNAMIC else Theme.BLUE - set(value) { - field = value - stateHandle[Constants.THEME] = value - } - val theme: StateFlow - get() = stateHandle.getStateFlow(Constants.THEME, null) - - /* -- Dark Theme --------------------------------------------------- */ - private var _darkTheme: DarkTheme = - stateHandle[Constants.DARK_THEME] ?: DarkTheme.FOLLOW_SYSTEM - set(value) { - field = value - stateHandle[Constants.DARK_THEME] = value - } - val darkTheme: StateFlow - get() = stateHandle.getStateFlow(Constants.DARK_THEME, null) - - /* -- Show Start Screen -------------------------------------------- */ - private var _showStartScreen: Boolean = - stateHandle[Constants.SHOW_START_SCREEN] ?: true - set(value) { - field = value - stateHandle[Constants.SHOW_START_SCREEN] = value - } - val showStartScreen: StateFlow - get() = stateHandle.getStateFlow(Constants.SHOW_START_SCREEN, null) - - /* -- Background Color --------------------------------------------- */ - private var _backgroundColor: Long = - stateHandle[Constants.BACKGROUND_COLOR] ?: Color.DarkGray.value.toLong() - set(value) { - field = value - stateHandle[Constants.BACKGROUND_COLOR] = value - } - val backgroundColor: StateFlow - get() = stateHandle.getStateFlow(Constants.BACKGROUND_COLOR, null) - - /* -- Font Color --------------------------------------------------- */ - private var _fontColor: Long = - stateHandle[Constants.FONT_COLOR] ?: Color.LightGray.value.toLong() - set(value) { - field = value - stateHandle[Constants.FONT_COLOR] = value - } - val fontColor: StateFlow - get() = stateHandle.getStateFlow(Constants.FONT_COLOR, null) - - /* -- Font Family -------------------------------------------------- */ - private var _fontFamily: String = - stateHandle[Constants.FONT] ?: Constants.FONTS[0].id - set(value) { - field = value - stateHandle[Constants.FONT] = value - } - val fontFamily: StateFlow - get() = stateHandle.getStateFlow(Constants.FONT, null) - - /* -- Is Italic ---------------------------------------------------- */ - private var _isItalic: Boolean = - stateHandle[Constants.IS_ITALIC] ?: false - set(value) { - field = value - stateHandle[Constants.IS_ITALIC] = value - } - val isItalic: StateFlow - get() = stateHandle.getStateFlow(Constants.IS_ITALIC, null) - - /* -- Font Size ---------------------------------------------------- */ - private var _fontSize: Int = - stateHandle[Constants.FONT_SIZE] ?: 16 - set(value) { - field = value - stateHandle[Constants.FONT_SIZE] = value - } - val fontSize: StateFlow - get() = stateHandle.getStateFlow(Constants.FONT_SIZE, null) - - /* -- Line Height -------------------------------------------------- */ - private var _lineHeight: Int = - stateHandle[Constants.LINE_HEIGHT] ?: 4 - set(value) { - field = value - stateHandle[Constants.LINE_HEIGHT] = value - } - val lineHeight: StateFlow - get() = stateHandle.getStateFlow(Constants.LINE_HEIGHT, null) - - /* -- Paragraph Height --------------------------------------------- */ - private var _paragraphHeight: Int = - stateHandle[Constants.PARAGRAPH_HEIGHT] ?: 8 - set(value) { - field = value - stateHandle[Constants.PARAGRAPH_HEIGHT] = value - } - val paragraphHeight: StateFlow - get() = stateHandle.getStateFlow(Constants.PARAGRAPH_HEIGHT, null) - - /* -- Paragraph Indentation ---------------------------------------- */ - private var _paragraphIndentation: Boolean = - stateHandle[Constants.PARAGRAPH_INDENTATION] ?: false - set(value) { - field = value - stateHandle[Constants.PARAGRAPH_INDENTATION] = value - } - val paragraphIndentation: StateFlow - get() = stateHandle.getStateFlow(Constants.PARAGRAPH_INDENTATION, null) - /* --------------------------------------------------------------- */ + private val _state: MutableStateFlow = MutableStateFlow( + stateHandle[Constants.MAIN_STATE] ?: MainState() + ) + val state = _state.asStateFlow() fun onEvent(event: MainEvent) { when (event) { is MainEvent.OnChangeLanguage -> { viewModelScope.launch(Dispatchers.Main) { changeLanguage.execute(event.lang) - _language = event.lang + _state.updateWithSavedHandle { + it.copy( + language = event.lang + ) + } } } is MainEvent.OnChangeDarkTheme -> { viewModelScope.launch(Dispatchers.IO) { setDatastore.execute(DataStoreConstants.DARK_THEME, event.darkTheme) - _darkTheme = event.darkTheme.toDarkTheme() + _state.updateWithSavedHandle { + it.copy( + darkTheme = event.darkTheme.toDarkTheme() + ) + } } } is MainEvent.OnChangeTheme -> { viewModelScope.launch(Dispatchers.IO) { setDatastore.execute(DataStoreConstants.THEME, event.theme) - _theme = event.theme.toTheme() + _state.updateWithSavedHandle { + it.copy( + theme = event.theme.toTheme() + ) + } } } is MainEvent.OnChangeFontFamily -> { viewModelScope.launch(Dispatchers.IO) { setDatastore.execute(DataStoreConstants.FONT, event.fontFamily) - _fontFamily = Constants.FONTS.find { font -> font.id == event.fontFamily }?.id - ?: Constants.FONTS[0].id + _state.updateWithSavedHandle { + it.copy( + fontFamily = Constants.FONTS.find { font -> font.id == event.fontFamily }?.id + ?: Constants.FONTS[0].id + ) + } } } is MainEvent.OnChangeFontStyle -> { viewModelScope.launch(Dispatchers.IO) { setDatastore.execute(DataStoreConstants.IS_ITALIC, event.fontStyle) - _isItalic = event.fontStyle + _state.updateWithSavedHandle { + it.copy( + isItalic = event.fontStyle + ) + } } } is MainEvent.OnChangeFontSize -> { viewModelScope.launch(Dispatchers.IO) { setDatastore.execute(DataStoreConstants.FONT_SIZE, event.fontSize) - _fontSize = event.fontSize + _state.updateWithSavedHandle { + it.copy( + fontSize = event.fontSize + ) + } } } is MainEvent.OnChangeLineHeight -> { viewModelScope.launch(Dispatchers.IO) { setDatastore.execute(DataStoreConstants.LINE_HEIGHT, event.lineHeight) - _lineHeight = event.lineHeight + _state.updateWithSavedHandle { + it.copy( + lineHeight = event.lineHeight + ) + } } } is MainEvent.OnChangeParagraphHeight -> { viewModelScope.launch(Dispatchers.IO) { setDatastore.execute(DataStoreConstants.PARAGRAPH_HEIGHT, event.paragraphHeight) - _paragraphHeight = event.paragraphHeight + _state.updateWithSavedHandle { + it.copy( + paragraphHeight = event.paragraphHeight + ) + } } } is MainEvent.OnChangeParagraphIndentation -> { viewModelScope.launch(Dispatchers.IO) { setDatastore.execute(DataStoreConstants.PARAGRAPH_INDENTATION, event.bool) - _paragraphIndentation = event.bool + _state.updateWithSavedHandle { + it.copy( + paragraphIndentation = event.bool + ) + } } } is MainEvent.OnChangeBackgroundColor -> { viewModelScope.launch(Dispatchers.IO) { setDatastore.execute(DataStoreConstants.BACKGROUND_COLOR, event.color) - _backgroundColor = event.color + _state.updateWithSavedHandle { + it.copy( + backgroundColor = event.color + ) + } } } is MainEvent.OnChangeFontColor -> { viewModelScope.launch(Dispatchers.IO) { setDatastore.execute(DataStoreConstants.FONT_COLOR, event.color) - _fontColor = event.color + _state.updateWithSavedHandle { + it.copy( + fontColor = event.color + ) + } + } + } + + is MainEvent.OnChangeShowStartScreen -> { + viewModelScope.launch(Dispatchers.IO) { + setDatastore.execute(DataStoreConstants.SHOW_START_SCREEN, event.bool) + _state.updateWithSavedHandle { + it.copy( + showStartScreen = event.bool + ) + } } } } @@ -263,20 +193,22 @@ class MainViewModel @Inject constructor( } val isDataReady = combine( - language, - theme, - darkTheme, - showStartScreen, - backgroundColor, - fontColor, - fontFamily, - isItalic, - fontSize, - lineHeight, - paragraphHeight, - paragraphIndentation - ) { values -> - values.all { it != null } + _state + ) { + val value = it.first() + + return@combine value.language != null && + value.theme != null && + value.darkTheme != null && + value.fontFamily != null && + value.isItalic != null && + value.fontSize != null && + value.lineHeight != null && + value.paragraphHeight != null && + value.paragraphIndentation != null && + value.backgroundColor != null && + value.fontColor != null && + value.showStartScreen != null } val isReady = combine( @@ -300,7 +232,14 @@ class MainViewModel @Inject constructor( // Language viewModelScope.launch(Dispatchers.Main) { getDatastore - .execute(DataStoreConstants.LANGUAGE, _language) + .execute( + DataStoreConstants.LANGUAGE, + if (Constants.LANGUAGES.any { Locale.getDefault().language.take(2) == it.first }) { + Locale.getDefault().language.take(2) + } else { + "en" + } + ) .first { onEvent(MainEvent.OnChangeLanguage(it)) it.isNotBlank() @@ -310,7 +249,11 @@ class MainViewModel @Inject constructor( // Theme viewModelScope.launch(Dispatchers.IO) { getDatastore - .execute(DataStoreConstants.THEME, _theme.toString()) + .execute( + DataStoreConstants.THEME, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Theme.DYNAMIC.name + else Theme.BLUE.name + ) .first { onEvent(MainEvent.OnChangeTheme(it)) it.isNotBlank() @@ -320,7 +263,7 @@ class MainViewModel @Inject constructor( // Dark Theme viewModelScope.launch(Dispatchers.IO) { getDatastore - .execute(DataStoreConstants.DARK_THEME, _darkTheme.toString()) + .execute(DataStoreConstants.DARK_THEME, DarkTheme.FOLLOW_SYSTEM.name) .first { onEvent(MainEvent.OnChangeDarkTheme(it)) it.isNotBlank() @@ -330,11 +273,9 @@ class MainViewModel @Inject constructor( // Show Start Screen viewModelScope.launch(Dispatchers.IO) { getDatastore - .execute(DataStoreConstants.SHOW_START_SCREEN, _showStartScreen) + .execute(DataStoreConstants.SHOW_START_SCREEN, true) .first { - setDatastore.execute(DataStoreConstants.SHOW_START_SCREEN, it) - _showStartScreen = it - + onEvent(MainEvent.OnChangeShowStartScreen(it)) true } } @@ -342,7 +283,7 @@ class MainViewModel @Inject constructor( // Background Color viewModelScope.launch(Dispatchers.IO) { getDatastore - .execute(DataStoreConstants.BACKGROUND_COLOR, _backgroundColor) + .execute(DataStoreConstants.BACKGROUND_COLOR, Color.DarkGray.value.toLong()) .first { onEvent(MainEvent.OnChangeBackgroundColor(it)) true @@ -352,7 +293,7 @@ class MainViewModel @Inject constructor( // Font Color viewModelScope.launch(Dispatchers.IO) { getDatastore - .execute(DataStoreConstants.FONT_COLOR, _fontColor) + .execute(DataStoreConstants.FONT_COLOR, Color.LightGray.value.toLong()) .first { onEvent(MainEvent.OnChangeFontColor(it)) true @@ -362,7 +303,7 @@ class MainViewModel @Inject constructor( // Font Family viewModelScope.launch(Dispatchers.IO) { getDatastore - .execute(DataStoreConstants.FONT, _fontFamily) + .execute(DataStoreConstants.FONT, Constants.FONTS[0].id) .first { onEvent(MainEvent.OnChangeFontFamily(it)) true @@ -372,7 +313,7 @@ class MainViewModel @Inject constructor( // Font Style viewModelScope.launch(Dispatchers.IO) { getDatastore - .execute(DataStoreConstants.IS_ITALIC, _isItalic) + .execute(DataStoreConstants.IS_ITALIC, false) .first { onEvent(MainEvent.OnChangeFontStyle(it)) true @@ -382,7 +323,7 @@ class MainViewModel @Inject constructor( // Font Size viewModelScope.launch(Dispatchers.IO) { getDatastore - .execute(DataStoreConstants.FONT_SIZE, _fontSize) + .execute(DataStoreConstants.FONT_SIZE, 16) .first { onEvent(MainEvent.OnChangeFontSize(it)) it > 0 @@ -392,7 +333,7 @@ class MainViewModel @Inject constructor( // Line Height viewModelScope.launch(Dispatchers.IO) { getDatastore - .execute(DataStoreConstants.LINE_HEIGHT, _lineHeight) + .execute(DataStoreConstants.LINE_HEIGHT, 4) .first { onEvent(MainEvent.OnChangeLineHeight(it)) true @@ -402,7 +343,7 @@ class MainViewModel @Inject constructor( // Paragraph Height viewModelScope.launch(Dispatchers.IO) { getDatastore - .execute(DataStoreConstants.PARAGRAPH_HEIGHT, _paragraphHeight) + .execute(DataStoreConstants.PARAGRAPH_HEIGHT, 8) .first { onEvent(MainEvent.OnChangeParagraphHeight(it)) true @@ -412,7 +353,7 @@ class MainViewModel @Inject constructor( // Paragraph Indentation viewModelScope.launch(Dispatchers.IO) { getDatastore - .execute(DataStoreConstants.PARAGRAPH_INDENTATION, _paragraphIndentation) + .execute(DataStoreConstants.PARAGRAPH_INDENTATION, false) .first { onEvent(MainEvent.OnChangeParagraphIndentation(it)) true @@ -421,4 +362,17 @@ class MainViewModel @Inject constructor( } + /** + * Updates [MutableStateFlow] along with [SavedStateHandle]. + */ + private fun MutableStateFlow.updateWithSavedHandle( + const: String = Constants.MAIN_STATE, + function: (T) -> T + ) { + val nextValue = function(value) + update { + stateHandle[const] = nextValue + nextValue + } + } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/data/Navigation.kt b/app/src/main/java/ua/acclorite/book_story/presentation/data/Navigation.kt index c9cf246e..72f96578 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/data/Navigation.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/data/Navigation.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateListOf import androidx.compose.ui.Modifier @@ -28,7 +29,7 @@ import dagger.hilt.android.lifecycle.withCreationCallback import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import ua.acclorite.book_story.ui.Transitions +import ua.acclorite.book_story.presentation.ui.Transitions private const val CURRENT_SCREEN = "current_screen" @@ -56,12 +57,12 @@ enum class Screen { START } +@Immutable data class Argument( val key: String, val arg: Any? ) - /** * Navigator. Using for navigation between screens. * @@ -181,7 +182,6 @@ class Navigator @AssistedInject constructor( } } - @AssistedFactory interface Factory { fun create(startScreen: Screen): Navigator diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/AboutScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/AboutScreen.kt new file mode 100644 index 00000000..adf50af6 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/AboutScreen.kt @@ -0,0 +1,258 @@ +package ua.acclorite.book_story.presentation.screens.about + +import android.widget.Toast +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.components.GoBackButton +import ua.acclorite.book_story.presentation.data.Navigator +import ua.acclorite.book_story.presentation.screens.about.components.AboutItem +import ua.acclorite.book_story.presentation.screens.about.data.AboutEvent +import ua.acclorite.book_story.presentation.screens.about.data.AboutViewModel +import ua.acclorite.book_story.presentation.ui.elevation + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AboutScreen( + viewModel: AboutViewModel = hiltViewModel(), + navigator: Navigator +) { + val context = LocalContext.current + val listState = rememberLazyListState() + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( + canScroll = { + listState.canScrollForward || listState.canScrollBackward + } + ) + + Scaffold( + Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .windowInsetsPadding(WindowInsets.navigationBars), + containerColor = MaterialTheme.colorScheme.surface, + topBar = { + LargeTopAppBar( + title = { + Text(stringResource(id = R.string.about_screen)) + }, + navigationIcon = { + GoBackButton(navigator = navigator) + }, + scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.elevation() + ) + ) + } + ) { paddingValues -> + LazyColumn( + Modifier + .fillMaxSize() + .padding(top = paddingValues.calculateTopPadding()), + state = listState + ) { + item { + Spacer(modifier = Modifier.height(16.dp)) + } + + item { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Icon( + painterResource(id = R.drawable.app_icon_monochrome), + contentDescription = stringResource(id = R.string.app_icon_content_desc), + modifier = Modifier + .size(180.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + item { + Spacer(modifier = Modifier.height(36.dp)) + Divider( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.outlineVariant + ) + } + + item { + AboutItem( + title = stringResource(id = R.string.app_version_option), + description = stringResource( + id = R.string.app_version_option_desc, + stringResource(id = R.string.app_version) + ), + isOnClickEnabled = false + ) + } + + item { + AboutItem( + title = stringResource(id = R.string.all_releases_option), + description = stringResource( + id = R.string.all_releases_option_desc + ) + ) { + Toast.makeText( + context, + "${context.getString(R.string.app_version_option)}:" + + " ${context.getString(R.string.app_version)}", + Toast.LENGTH_LONG + ).show() + viewModel.onEvent( + AboutEvent.OnNavigateToBrowserPage( + context.getString(R.string.releases_page), + context, + noAppsFound = { + Toast.makeText( + context, + context.getString(R.string.error_no_browser), + Toast.LENGTH_SHORT + ).show() + } + ) + ) + } + } + + item { + AboutItem( + title = stringResource(id = R.string.issues_option), + description = stringResource( + id = R.string.issues_option_desc + ) + ) { + viewModel.onEvent( + AboutEvent.OnNavigateToBrowserPage( + context.getString(R.string.issues_page), + context, + noAppsFound = { + Toast.makeText( + context, + context.getString(R.string.error_no_browser), + Toast.LENGTH_SHORT + ).show() + } + ) + ) + } + } + + item { + AboutItem( + title = stringResource(id = R.string.project_page_option), + description = stringResource( + id = R.string.project_page_option_desc + ) + ) { + viewModel.onEvent( + AboutEvent.OnNavigateToBrowserPage( + context.getString(R.string.project_page), + context, + noAppsFound = { + Toast.makeText( + context, + context.getString(R.string.error_no_browser), + Toast.LENGTH_SHORT + ).show() + } + ) + ) + } + } + + item { + AboutItem( + title = stringResource(id = R.string.my_page_option), + description = stringResource( + id = R.string.my_page_option_desc + ) + ) { + viewModel.onEvent( + AboutEvent.OnNavigateToBrowserPage( + context.getString(R.string.my_page), + context, + noAppsFound = { + Toast.makeText( + context, + context.getString(R.string.error_no_browser), + Toast.LENGTH_SHORT + ).show() + } + ) + ) + } + } + + item { + AboutItem( + title = stringResource(id = R.string.support_option), + description = stringResource( + id = R.string.support_option_desc + ) + ) { + viewModel.onEvent( + AboutEvent.OnNavigateToBrowserPage( + context.getString(R.string.support_page), + context, + noAppsFound = { + Toast.makeText( + context, + context.getString(R.string.error_no_browser), + Toast.LENGTH_SHORT + ).show() + } + ) + ) + } + } + + item { + AboutItem( + title = stringResource(id = R.string.slava_ukraini_option), + description = null + ) { + Toast.makeText( + context, + context.getString(R.string.slava_ukraini_option_desc), + Toast.LENGTH_SHORT + ).show() + } + } + + 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/about/components/AboutItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/components/AboutItem.kt new file mode 100644 index 00000000..c35e2d4e --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/components/AboutItem.kt @@ -0,0 +1,58 @@ +package ua.acclorite.book_story.presentation.screens.about.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +/** + * About screen item. + */ +@Composable +fun AboutItem( + modifier: Modifier = Modifier, + title: String, + description: String?, + verticalPadding: Dp = 12.dp, + isOnClickEnabled: Boolean = true, + onClick: () -> Unit = {} +) { + Column( + modifier + .fillMaxWidth() + .clickable(enabled = isOnClickEnabled) { + onClick() + } + .padding(horizontal = 18.dp, vertical = verticalPadding), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + title, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurface, + fontSize = 18.sp + ) + Spacer(modifier = Modifier.height(2.dp)) + description?.let { + Text( + it, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutEvent.kt new file mode 100644 index 00000000..7466e796 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutEvent.kt @@ -0,0 +1,11 @@ +package ua.acclorite.book_story.presentation.screens.about.data + +import android.content.Context + +sealed class AboutEvent { + data class OnNavigateToBrowserPage( + val page: String, + val context: Context, + val noAppsFound: () -> Unit + ) : AboutEvent() +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutViewModel.kt new file mode 100644 index 00000000..7a6e7a1e --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/about/data/AboutViewModel.kt @@ -0,0 +1,43 @@ +package ua.acclorite.book_story.presentation.screens.about.data + +import android.content.Intent +import android.net.Uri +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class AboutViewModel @Inject constructor( + +) : ViewModel() { + + fun onEvent(event: AboutEvent) { + when (event) { + is AboutEvent.OnNavigateToBrowserPage -> { + viewModelScope.launch { + val intent = Intent( + Intent.ACTION_VIEW, + Uri.parse(event.page) + ) + + if (intent.resolveActivity(event.context.packageManager) != null) { + event.context.startActivity(intent) + return@launch + } + + event.noAppsFound() + } + } + } + } +} + + + + + + + + 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 6b23e85f..f36169db 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 @@ -1,6 +1,7 @@ package ua.acclorite.book_story.presentation.screens.book_info import android.widget.Toast +import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween import androidx.compose.animation.fadeOut @@ -55,9 +56,7 @@ 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 import ua.acclorite.book_story.presentation.data.Navigator -import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoBackground import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoDescriptionSection import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoInfoSection @@ -74,9 +73,9 @@ 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 ua.acclorite.book_story.presentation.ui.DefaultTransition +import ua.acclorite.book_story.presentation.ui.Transitions +import ua.acclorite.book_story.presentation.ui.elevation @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @Composable @@ -99,8 +98,8 @@ fun BookInfoScreen( viewModel.onEvent( BookInfoEvent.OnUpdateBook( refreshList = { - libraryViewModel.onEvent(LibraryEvent.OnLoadList) - historyViewModel.onEvent(HistoryEvent.OnLoadList) + libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) + historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it)) }, snackbarState, context @@ -138,6 +137,7 @@ fun BookInfoScreen( if (state.showMoveDialog) { BookInfoMoveDialog( libraryViewModel = libraryViewModel, + historyViewModel = historyViewModel, viewModel = viewModel, navigator = navigator ) @@ -158,7 +158,7 @@ fun BookInfoScreen( isTopBarScrolled = null, content1NavigationIcon = { - GoBackButton(navigator = navigator) + GoBackButton(navigator = navigator, enabled = !state.isRefreshing) }, content1Title = { DefaultTransition(visible = firstVisibleItemIndex > 0) { @@ -181,8 +181,8 @@ fun BookInfoScreen( viewModel.onEvent( BookInfoEvent.OnUpdateBook( refreshList = { - libraryViewModel.onEvent(LibraryEvent.OnLoadList) - historyViewModel.onEvent(HistoryEvent.OnLoadList) + libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) + historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it)) }, snackbarState, context @@ -218,9 +218,8 @@ fun BookInfoScreen( ) { viewModel.onEvent(BookInfoEvent.OnUpdateTitle( refreshList = { - libraryViewModel.onEvent( - LibraryEvent.OnLoadList - ) + libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) + historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it)) } )) Toast.makeText( @@ -282,11 +281,9 @@ fun BookInfoScreen( FloatingActionButton( onClick = { - navigator.navigate( - Screen.READER, false, Argument( - "book", state.book.id - ) - ) + if (!state.isRefreshing) { + viewModel.onEvent(BookInfoEvent.OnNavigateToReaderScreen(navigator)) + } }, shape = MaterialTheme.shapes.large, modifier = Modifier @@ -332,4 +329,10 @@ fun BookInfoScreen( ) } } + + BackHandler { + if (!state.isRefreshing) { + navigator.navigateBack() + } + } } \ No newline at end of file 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 89075bc0..98011d93 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 @@ -45,7 +45,7 @@ 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 -import ua.acclorite.book_story.ui.elevation +import ua.acclorite.book_story.presentation.ui.elevation /** * Info section. @@ -65,7 +65,7 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) { modifier = Modifier .height(195.dp) .width(130.dp) - .clip(MaterialTheme.shapes.large) + .clip(MaterialTheme.shapes.medium) .background(MaterialTheme.elevation()) .combinedClickable( interactionSource = remember { MutableInteractionSource() }, @@ -82,7 +82,7 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) { contentDescription = stringResource(id = R.string.cover_image_content_desc), modifier = Modifier .fillMaxSize() - .clip(MaterialTheme.shapes.large), + .clip(MaterialTheme.shapes.medium), contentScale = ContentScale.Crop ) } else { 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 e22b9f4d..f2407008 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 @@ -33,13 +33,11 @@ fun BookInfoStatisticSection(book: Book) { .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 + val description = stringResource( + if (book.progress == 1f) R.string.read_done + else if (book.progress > 0.2f) R.string.read_keep else R.string.read_more - ) else stringResource(id = R.string.read_done) + ) Column( Modifier 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 95b0d1ff..0f024e6c 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 @@ -31,8 +31,8 @@ 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.ElevationDefaults -import ua.acclorite.book_story.ui.elevation +import ua.acclorite.book_story.presentation.ui.ElevationDefaults +import ua.acclorite.book_story.presentation.ui.elevation /** * Change cover bottom sheet. Lets user select photo from the gallery and replaces old one. @@ -58,8 +58,8 @@ fun BookInfoChangeCoverBottomSheet( uri, context, refreshList = { - libraryViewModel.onEvent(LibraryEvent.OnLoadList) - historyViewModel.onEvent(HistoryEvent.OnLoadList) + libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) + historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it)) } ) ) @@ -98,12 +98,14 @@ fun BookInfoChangeCoverBottomSheet( text = stringResource(id = R.string.delete_cover), description = stringResource(id = R.string.delete_cover_desc) ) { - viewModel.onEvent(BookInfoEvent.OnDeleteCover( - refreshList = { - libraryViewModel.onEvent(LibraryEvent.OnLoadList) - historyViewModel.onEvent(HistoryEvent.OnLoadList) - } - )) + viewModel.onEvent( + BookInfoEvent.OnDeleteCover( + refreshList = { + libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) + historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it)) + } + ) + ) Toast.makeText( context, context.getString(R.string.cover_image_deleted), 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 d5a952df..256311e1 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 @@ -22,8 +22,8 @@ import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel -import ua.acclorite.book_story.ui.ElevationDefaults -import ua.acclorite.book_story.ui.elevation +import ua.acclorite.book_story.presentation.ui.ElevationDefaults +import ua.acclorite.book_story.presentation.ui.elevation import java.text.SimpleDateFormat import java.util.Date import java.util.Locale diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoMoveDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoMoveDialog.kt index 12355b58..952a0267 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoMoveDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/components/dialog/BookInfoMoveDialog.kt @@ -18,6 +18,8 @@ import ua.acclorite.book_story.presentation.components.custom_dialog.SelectableD import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel +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 @@ -27,6 +29,7 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewMode @Composable fun BookInfoMoveDialog( libraryViewModel: LibraryViewModel, + historyViewModel: HistoryViewModel, viewModel: BookInfoViewModel, navigator: Navigator ) { @@ -54,9 +57,8 @@ fun BookInfoMoveDialog( viewModel.onEvent( BookInfoEvent.OnMoveBook( refreshList = { - libraryViewModel.onEvent( - LibraryEvent.OnLoadList - ) + libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) + historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it)) }, updatePage = { libraryViewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(it)) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoEvent.kt index f30828d2..0bc42428 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoEvent.kt @@ -4,26 +4,27 @@ import android.content.Context import android.net.Uri import androidx.compose.material3.SnackbarHostState import androidx.compose.ui.focus.FocusRequester +import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.presentation.data.Navigator sealed class BookInfoEvent { data object OnShowHideChangeCoverBottomSheet : BookInfoEvent() data object OnShowHideDetailsBottomSheet : BookInfoEvent() - data class OnDeleteCover(val refreshList: () -> Unit) : BookInfoEvent() - data class OnChangeCover(val uri: Uri, val context: Context, val refreshList: () -> Unit) : + data class OnDeleteCover(val refreshList: (Book) -> Unit) : BookInfoEvent() + data class OnChangeCover(val uri: Uri, val context: Context, val refreshList: (Book) -> Unit) : BookInfoEvent() data object OnShowHideEditTitle : BookInfoEvent() data class OnRequestFocus(val focusRequester: FocusRequester) : BookInfoEvent() data class OnTitleValueChange(val value: String) : BookInfoEvent() - data class OnUpdateTitle(val refreshList: () -> Unit) : BookInfoEvent() + data class OnUpdateTitle(val refreshList: (Book) -> Unit) : BookInfoEvent() data object OnShowHideMoreDropDown : BookInfoEvent() data object OnShowHideDeleteDialog : BookInfoEvent() data object OnShowHideMoveDialog : BookInfoEvent() data class OnDeleteBook(val refreshList: () -> Unit, val navigator: Navigator) : BookInfoEvent() data class OnMoveBook( - val refreshList: () -> Unit, + val refreshList: (Book) -> Unit, val updatePage: (Int) -> Unit, val navigator: Navigator ) : BookInfoEvent() @@ -44,8 +45,12 @@ sealed class BookInfoEvent { ) : BookInfoEvent() data class OnUpdateBook( - val refreshList: () -> Unit, + val refreshList: (Book) -> Unit, val snackbarState: SnackbarHostState, val context: Context ) : BookInfoEvent() + + data class OnNavigateToReaderScreen( + val navigator: Navigator + ) : BookInfoEvent() } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoState.kt index 650e4522..9b3667bd 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoState.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/book_info/data/BookInfoState.kt @@ -3,7 +3,7 @@ package ua.acclorite.book_story.presentation.screens.book_info.data import androidx.compose.runtime.Immutable import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.util.Constants +import ua.acclorite.book_story.domain.util.Constants @Immutable data class BookInfoState( 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 f6533883..24d4310e 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 @@ -17,16 +17,21 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch 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.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.GetBooksById +import ua.acclorite.book_story.domain.use_case.InsertHistory import ua.acclorite.book_story.domain.use_case.UpdateBooks import ua.acclorite.book_story.domain.use_case.UpdateBooksWithText import ua.acclorite.book_story.domain.use_case.UpdateCoverImageOfBook +import ua.acclorite.book_story.presentation.data.Argument import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Screen +import java.util.Date import javax.inject.Inject @@ -35,6 +40,7 @@ class BookInfoViewModel @Inject constructor( private val updateBooks: UpdateBooks, private val updateBooksWithText: UpdateBooksWithText, private val updateCoverImageOfBook: UpdateCoverImageOfBook, + private val insertHistory: InsertHistory, private val deleteBooks: DeleteBooks, private val getBookFromFile: GetBookFromFile, private val getBookById: GetBooksById @@ -65,7 +71,7 @@ class BookInfoViewModel @Inject constructor( _state.value.book, image ) - event.refreshList() + val newCoverImage = getBookById.execute( listOf( _state.value.book.id ?: return@launch @@ -80,6 +86,7 @@ class BookInfoViewModel @Inject constructor( showChangeCoverBottomSheet = false ) } + event.refreshList(_state.value.book) } } @@ -93,8 +100,6 @@ class BookInfoViewModel @Inject constructor( bookWithOldCover = _state.value.book, newCoverImage = null ) - event.refreshList() - _state.update { it.copy( book = it.book.copy( @@ -103,6 +108,7 @@ class BookInfoViewModel @Inject constructor( showChangeCoverBottomSheet = false ) } + event.refreshList(_state.value.book) } } @@ -155,8 +161,6 @@ class BookInfoViewModel @Inject constructor( _state.value.book.copy(title = title) ) ) - event.refreshList() - _state.update { it.copy( book = it.book.copy( @@ -164,6 +168,8 @@ class BookInfoViewModel @Inject constructor( ) ) } + event.refreshList(_state.value.book) + onEvent(BookInfoEvent.OnShowHideEditTitle) } } @@ -223,13 +229,16 @@ class BookInfoViewModel @Inject constructor( ) } - val book = _state.value.book.copy( - category = _state.value.selectedCategory - ) - updateBooks.execute(listOf(book)) - - event.refreshList() + _state.update { + it.copy( + book = it.book.copy( + category = it.selectedCategory + ) + ) + } + updateBooks.execute(listOf(_state.value.book)) + event.refreshList(_state.value.book) event.updatePage( Category.entries.dropLastWhile { it != _state.value.selectedCategory @@ -375,7 +384,7 @@ class BookInfoViewModel @Inject constructor( ) ) ) - event.refreshList() + event.refreshList(_state.value.book) onEvent( BookInfoEvent.OnShowSnackbar( @@ -394,20 +403,42 @@ class BookInfoViewModel @Inject constructor( } } } + + is BookInfoEvent.OnNavigateToReaderScreen -> { + viewModelScope.launch { + _state.value.book.id?.let { + insertHistory.execute( + listOf( + History( + null, + it, + null, + Date().time + ) + ) + ) + } + event.navigator.navigate( + Screen.READER, + false, + Argument( + "book", _state.value.book + ) + ) + } + } } } fun init(navigator: Navigator) { viewModelScope.launch { - val bookIndex = navigator.retrieveArgument("book") as? Int + val book = navigator.retrieveArgument("book") as? Book - if (bookIndex == null || bookIndex < 0) { + if (book == null) { navigator.navigateBack() return@launch } - val book = getBookById.execute(listOf(bookIndex)).first() - _state.update { BookInfoState(book = book) } 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 e0e3c143..ed9c5dec 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 @@ -1,6 +1,7 @@ package ua.acclorite.book_story.presentation.screens.browse import android.Manifest +import android.widget.Toast import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween @@ -41,6 +42,7 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle @@ -63,9 +65,9 @@ import ua.acclorite.book_story.presentation.screens.browse.components.adding_dia import ua.acclorite.book_story.presentation.screens.browse.data.BrowseEvent import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel 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 ua.acclorite.book_story.presentation.ui.DefaultTransition +import ua.acclorite.book_story.presentation.ui.Transitions +import ua.acclorite.book_story.presentation.ui.elevation @OptIn( ExperimentalMaterial3Api::class, @@ -79,6 +81,7 @@ fun BrowseScreen( libraryViewModel: LibraryViewModel, navigator: Navigator ) { + val context = LocalContext.current val permissionState = rememberPermissionState( permission = Manifest.permission.READ_EXTERNAL_STORAGE ) @@ -130,20 +133,13 @@ fun BrowseScreen( content1Visibility = !state.hasSelectedItems && !state.showSearch, content1NavigationIcon = {}, content1Title = { - Text( - stringResource(id = R.string.browse_screen), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - overflow = TextOverflow.Ellipsis, - maxLines = 1 - ) + Text(stringResource(id = R.string.browse_screen)) }, content1Actions = { CustomIconButton( icon = Icons.Default.Search, contentDescription = stringResource(id = R.string.search_content_desc), - disableOnClick = false, - enabled = !state.showSearch + disableOnClick = true ) { viewModel.onEvent(BrowseEvent.OnSearchShowHide) } @@ -167,8 +163,6 @@ fun BrowseScreen( id = R.string.selected_items_count_query, state.selectedItemsCount.coerceAtLeast(1) ), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, overflow = TextOverflow.Ellipsis, maxLines = 1 ) @@ -265,6 +259,16 @@ fun BrowseScreen( modifier = Modifier .animateItemPlacement(), hasSelectedFiles = state.selectableFiles.any { it.second }, + onLongClick = { + Toast.makeText( + context, + context.getString( + R.string.file_path_query, + selectableFile.first.path + ), + Toast.LENGTH_LONG + ).show() + }, onClick = { viewModel.onEvent(BrowseEvent.OnSelectFile(selectableFile)) } 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 4931fa47..63a4b2df 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 @@ -2,9 +2,10 @@ package ua.acclorite.book_story.presentation.screens.browse.components import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.tween +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -33,9 +34,9 @@ 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.domain.util.Selected import ua.acclorite.book_story.presentation.components.CustomCheckbox -import ua.acclorite.book_story.ui.DefaultTransition -import ua.acclorite.book_story.util.Selected +import ua.acclorite.book_story.presentation.ui.DefaultTransition import java.io.File import java.text.SimpleDateFormat import java.util.Date @@ -44,11 +45,14 @@ import java.util.Locale /** * Browse list element item. Can be selected. */ +@OptIn(ExperimentalFoundationApi::class) @Composable fun BrowseFileItem( file: Pair, hasSelectedFiles: Boolean, - modifier: Modifier, onClick: () -> Unit + modifier: Modifier, + onClick: () -> Unit, + onLongClick: () -> Unit ) { val pattern = remember { SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault()) } val lastModified = remember { pattern.format(Date(file.first.lastModified())) } @@ -85,9 +89,14 @@ fun BrowseFileItem( .padding(horizontal = 8.dp, vertical = 3.dp) .clip(RoundedCornerShape(10.dp)) .background(animatedBackgroundColor) - .clickable { - onClick() - } + .combinedClickable( + onLongClick = { + onLongClick() + }, + onClick = { + onClick() + } + ) .padding(horizontal = 8.dp, vertical = 7.dp) ) { Row( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialog.kt index 6e822334..4079d293 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/browse/components/adding_dialog/BrowseAddingDialog.kt @@ -54,6 +54,7 @@ fun BrowseAddingDialog( navigator, resetScroll = { libraryViewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(0)) + libraryViewModel.onEvent(LibraryEvent.OnLoadList) } ) ) @@ -83,7 +84,9 @@ fun BrowseAddingDialog( } } else { items(state.selectedBooks) { book -> - BrowseAddingDialogItem(result = book) { + BrowseAddingDialogItem( + result = book + ) { if (it) { viewModel.onEvent(BrowseEvent.OnSelectBook(book)) } else { 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 1e23efb3..c1346e7a 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 @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Error -import androidx.compose.material.icons.filled.Lock import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -23,8 +22,8 @@ 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.NullableBook +import ua.acclorite.book_story.domain.util.Selected import ua.acclorite.book_story.presentation.components.CustomCheckbox -import ua.acclorite.book_story.util.Selected /** * Adding dialog item. @@ -64,13 +63,6 @@ fun BrowseAddingDialogItem(result: Pair, onClick: (Boole } } } else { - val icon = - if (result.first.message?.asString() == stringResource(R.string.error_file_encrypted)) { - Icons.Default.Lock - } else { - Icons.Default.Error - } - Row( Modifier .fillMaxWidth() @@ -81,7 +73,7 @@ fun BrowseAddingDialogItem(result: Pair, onClick: (Boole verticalAlignment = Alignment.CenterVertically ) { Icon( - imageVector = icon, + imageVector = Icons.Default.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 d641538b..c29a6708 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 @@ -7,8 +7,8 @@ import androidx.compose.ui.focus.FocusRequester import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState import ua.acclorite.book_story.domain.model.NullableBook +import ua.acclorite.book_story.domain.util.Selected import ua.acclorite.book_story.presentation.data.Navigator -import ua.acclorite.book_story.util.Selected import java.io.File sealed class BrowseEvent { 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 fb453989..b6b0a4fa 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 @@ -3,7 +3,7 @@ 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 ua.acclorite.book_story.util.Selected +import ua.acclorite.book_story.domain.util.Selected import java.io.File @Immutable @@ -18,9 +18,6 @@ data class BrowseState( val selectedItemsCount: Int = 0, val hasSelectedItems: Boolean = false, - val scrollIndex: Int = 0, - val scrollOffset: Int = 0, - val showSearch: Boolean = false, val searchQuery: String = "", val hasFocused: 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 c48e0fd1..2fe1b30d 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 @@ -23,9 +23,9 @@ 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.GetFilesFromDevice import ua.acclorite.book_story.domain.use_case.InsertBooks +import ua.acclorite.book_story.domain.util.Resource import ua.acclorite.book_story.presentation.data.Argument import ua.acclorite.book_story.presentation.data.Screen -import ua.acclorite.book_story.util.Resource import javax.inject.Inject @OptIn(ExperimentalPermissionsApi::class) @@ -41,6 +41,7 @@ class BrowseViewModel @Inject constructor( val state = _state.asStateFlow() private var job: Job? = null + private var job2: Job? = null init { viewModelScope.launch(Dispatchers.IO) { @@ -127,7 +128,8 @@ class BrowseViewModel @Inject constructor( } is BrowseEvent.OnRefreshList -> { - viewModelScope.launch(Dispatchers.IO) { + job2?.cancel() + job2 = viewModelScope.launch(Dispatchers.IO) { _state.update { it.copy( isRefreshing = true, @@ -169,20 +171,19 @@ class BrowseViewModel @Inject constructor( is BrowseEvent.OnSelectFile -> { viewModelScope.launch(Dispatchers.IO) { - val indexOfFile = _state.value.selectableFiles.indexOf(event.file) - - if (indexOfFile == -1) { - return@launch + val editedList = _state.value.selectableFiles.map { + if (event.file.first.path == it.first.path) { + it.copy( + second = !it.second + ) + } else { + it + } } - val editedList = _state.value.selectableFiles.toMutableList() - editedList[indexOfFile] = editedList[indexOfFile].copy( - second = !editedList[indexOfFile].second - ) - _state.update { it.copy( - selectableFiles = editedList.toList(), + selectableFiles = editedList, selectedItemsCount = editedList.filter { file -> file.second }.size, hasSelectedItems = editedList.any { file -> file.second } ) @@ -267,7 +268,7 @@ class BrowseViewModel @Inject constructor( ) } job?.cancel() - job = viewModelScope.launch { + job = viewModelScope.launch(Dispatchers.IO) { delay(500) getFilesFromDownloads() } @@ -352,6 +353,7 @@ class BrowseViewModel @Inject constructor( _state.update { it.copy( isLoading = true, + hasSelectedItems = false, listState = LazyListState(0, 0) ) } 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 2390b880..48b67a3f 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 @@ -62,9 +62,9 @@ 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 ua.acclorite.book_story.presentation.ui.DefaultTransition +import ua.acclorite.book_story.presentation.ui.Transitions +import ua.acclorite.book_story.presentation.ui.elevation import java.util.UUID @OptIn( @@ -79,7 +79,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 = { @@ -108,19 +107,14 @@ fun HistoryScreen( content1NavigationIcon = {}, content1Title = { Text( - stringResource(id = R.string.history_screen), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - overflow = TextOverflow.Ellipsis, - maxLines = 1 + stringResource(id = R.string.history_screen) ) }, content1Actions = { CustomIconButton( icon = Icons.Default.Search, contentDescription = stringResource(id = R.string.search_content_desc), - disableOnClick = false, - enabled = !state.showSearch + disableOnClick = true, ) { viewModel.onEvent(HistoryEvent.OnSearchShowHide) } @@ -133,6 +127,7 @@ fun HistoryScreen( enabled = !state.isLoading && !state.isRefreshing && state.history.isNotEmpty() + && !state.showDeleteWholeHistoryDialog ) { viewModel.onEvent(HistoryEvent.OnShowHideDeleteWholeHistoryDialog) } @@ -214,72 +209,72 @@ fun HistoryScreen( 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 - ) + state.history.forEachIndexed { index, groupedHistory -> + item(key = groupedHistory.title) { + if (index > 0) { 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 + 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 + ) - HistoryItem( - modifier = Modifier.animateItemPlacement(), - history = it, - book = book, - onBodyClick = { - navigator.navigate( - Screen.BOOK_INFO, - false, - Argument("book", book.id) + Spacer(modifier = Modifier.height(8.dp)) + } + + items( + groupedHistory.history, key = { it.id ?: UUID.randomUUID() } + ) { + HistoryItem( + modifier = Modifier.animateItemPlacement(), + history = it, + isOnClickEnabled = !state.isRefreshing, + onBodyClick = { + navigator.navigate( + Screen.BOOK_INFO, + false, + Argument("book", it.book) + ) + }, + onTitleClick = { + viewModel.onEvent( + HistoryEvent.OnNavigateToReaderScreen( + navigator, + it.book ) - }, - onTitleClick = { - navigator.navigate( - Screen.READER, - false, - Argument("book", book.id) + ) + }, + isDeleteEnabled = !state.isRefreshing, + onDeleteClick = { + viewModel.onEvent( + HistoryEvent.OnDeleteHistoryElement( + historyToDelete = it, + snackbarState = snackbarState, + context = context, + refreshList = { + libraryViewModel.onEvent( + LibraryEvent.OnLoadList + ) + } ) - }, - isDeleteEnabled = !state.isRefreshing, - onDeleteClick = { - viewModel.onEvent( - HistoryEvent.OnDeleteHistoryElement( - historyToDelete = it, - snackbarState = snackbarState, - context = context, - refreshList = { - libraryViewModel.onEvent(LibraryEvent.OnLoadList) - } - ) - ) - } - ) - } + ) + } + ) } } } } AnimatedVisibility( - visible = !state.isLoading && state.history.isEmpty() && !state.isRefreshing, + visible = !state.isLoading + && state.history.isEmpty() + && !state.isRefreshing, modifier = Modifier.align(Alignment.Center), enter = Transitions.DefaultTransitionIn, exit = fadeOut(tween(0)) 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 105b1562..64ba65ff 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 @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Image import androidx.compose.material.icons.outlined.Delete @@ -30,11 +31,11 @@ import androidx.compose.ui.layout.ContentScale 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 coil.compose.AsyncImage import ua.acclorite.book_story.R -import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.History -import ua.acclorite.book_story.ui.elevation +import ua.acclorite.book_story.presentation.ui.elevation import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -46,7 +47,7 @@ import java.util.Locale fun HistoryItem( modifier: Modifier = Modifier, history: History, - book: Book, + isOnClickEnabled: Boolean, onBodyClick: () -> Unit, onTitleClick: () -> Unit, isDeleteEnabled: Boolean, @@ -61,7 +62,7 @@ fun HistoryItem( verticalAlignment = Alignment.CenterVertically, modifier = modifier .fillMaxWidth() - .clickable { + .clickable(enabled = isOnClickEnabled) { onBodyClick() } .padding(top = 6.dp, bottom = 6.dp, start = 16.dp, end = 10.dp) @@ -74,16 +75,16 @@ fun HistoryItem( modifier = Modifier .height(90.dp) .width(60.dp) - .clip(MaterialTheme.shapes.medium) + .clip(RoundedCornerShape(10.dp)) .background(MaterialTheme.elevation()) ) { - if (book.coverImage != null) { + if (history.book?.coverImage != null) { AsyncImage( - model = book.coverImage, + model = history.book.coverImage, contentDescription = stringResource(id = R.string.cover_image_content_desc), modifier = Modifier .fillMaxSize() - .clip(MaterialTheme.shapes.medium), + .clip(RoundedCornerShape(10.dp)), contentScale = ContentScale.Crop ) } else { @@ -108,14 +109,16 @@ fun HistoryItem( modifier = Modifier.fillMaxHeight() ) { Text( - book.title, + history.book?.title ?: return, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.bodyLarge, + lineHeight = 21.sp, modifier = Modifier .fillMaxWidth() .clickable( interactionSource = null, indication = null, + enabled = isOnClickEnabled, onClick = onTitleClick ), maxLines = 2, @@ -135,7 +138,7 @@ fun HistoryItem( } Box(modifier = Modifier.weight(0.11f), contentAlignment = Alignment.CenterEnd) { IconButton( - enabled = isDeleteEnabled, + enabled = isDeleteEnabled && isOnClickEnabled, onClick = { onDeleteClick() } 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 5318fe90..2d51220e 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 @@ -3,7 +3,9 @@ package ua.acclorite.book_story.presentation.screens.history.data import android.content.Context import androidx.compose.material3.SnackbarHostState import androidx.compose.ui.focus.FocusRequester +import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.History +import ua.acclorite.book_story.presentation.data.Navigator sealed class HistoryEvent { data object OnRefreshList : HistoryEvent() @@ -20,4 +22,9 @@ sealed class HistoryEvent { data class OnSearchQueryChange(val query: String) : HistoryEvent() data object OnSearchShowHide : HistoryEvent() data class OnRequestFocus(val focusRequester: FocusRequester) : HistoryEvent() + data class OnUpdateBook(val book: Book) : HistoryEvent() + data class OnNavigateToReaderScreen( + val navigator: Navigator, + val book: Book? + ) : 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 72ba639c..3c02d5ab 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 @@ -15,8 +15,4 @@ data class HistoryState( val showSearch: Boolean = false, val searchQuery: String = "", val hasFocused: Boolean = false, - - val scrollIndex: Int = 0, - val scrollOffset: Int = 0, - - ) +) 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 8c50dc17..031c40c3 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 @@ -20,9 +20,12 @@ import ua.acclorite.book_story.domain.use_case.DeleteWholeHistory import ua.acclorite.book_story.domain.use_case.GetBooksById import ua.acclorite.book_story.domain.use_case.GetHistory import ua.acclorite.book_story.domain.use_case.InsertHistory -import ua.acclorite.book_story.util.Resource +import ua.acclorite.book_story.domain.util.Resource +import ua.acclorite.book_story.presentation.data.Argument +import ua.acclorite.book_story.presentation.data.Screen import java.text.SimpleDateFormat import java.util.Calendar +import java.util.Date import java.util.Locale import javax.inject.Inject @@ -40,6 +43,7 @@ class HistoryViewModel @Inject constructor( private var job: Job? = null private var job2: Job? = null + private var job3: Job? = null init { viewModelScope.launch(Dispatchers.IO) { @@ -55,7 +59,8 @@ class HistoryViewModel @Inject constructor( fun onEvent(event: HistoryEvent) { when (event) { is HistoryEvent.OnRefreshList -> { - viewModelScope.launch(Dispatchers.IO) { + job3?.cancel() + job3 = viewModelScope.launch(Dispatchers.IO) { _state.update { it.copy( isRefreshing = true, @@ -115,6 +120,7 @@ class HistoryViewModel @Inject constructor( deleteHistory.execute( listOf(event.historyToDelete) ) + onEvent(HistoryEvent.OnRefreshList) event.refreshList() @@ -172,7 +178,7 @@ class HistoryViewModel @Inject constructor( ) } job?.cancel() - job = viewModelScope.launch { + job = viewModelScope.launch(Dispatchers.IO) { delay(500) getHistoryFromDatabase() } @@ -188,6 +194,54 @@ class HistoryViewModel @Inject constructor( } } } + + is HistoryEvent.OnUpdateBook -> { + viewModelScope.launch(Dispatchers.IO) { + val history = _state.value.history.map { + it.copy( + history = it.history.map { history -> + if (history.bookId == event.book.id) { + history.copy( + book = event.book + ) + } else { + history + } + } + ) + } + + _state.update { + it.copy( + history = history + ) + } + } + } + + is HistoryEvent.OnNavigateToReaderScreen -> { + viewModelScope.launch { + event.book?.id?.let { + insertHistory.execute( + listOf( + History( + null, + it, + null, + Date().time + ) + ) + ) + } + event.navigator.navigate( + Screen.READER, + false, + Argument( + "book", event.book + ) + ) + } + } } } @@ -210,9 +264,10 @@ class HistoryViewModel @Inject constructor( getHistory.execute().collect { result -> when (result) { is Resource.Success -> { - val history = result.data?.sortedByDescending { it.time } ?: emptyList() + val historyWithoutBook = + result.data?.sortedByDescending { it.time } ?: emptyList() - if (history.isEmpty()) { + if (historyWithoutBook.isEmpty()) { _state.update { it.copy( history = emptyList(), @@ -223,8 +278,26 @@ class HistoryViewModel @Inject constructor( } val books = getBooksById.execute( - history.map { it.bookId }.distinct() + historyWithoutBook.map { it.bookId }.distinct() ) + + if (books.isEmpty()) { + _state.update { + it.copy( + history = emptyList(), + isLoading = false + ) + } + return@collect + } + + val history = historyWithoutBook.map { + val book = books.find { book -> book.id == it.bookId }!! + it.copy( + book = book + ) + } + val groupedHistory = mutableListOf() history @@ -278,20 +351,3 @@ class HistoryViewModel @Inject constructor( } } - - - - - - - - - - - - - - - - - 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 e38e38bc..e19d140b 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 @@ -68,6 +68,7 @@ import kotlinx.coroutines.launch 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.util.Selected import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar import ua.acclorite.book_story.presentation.components.CustomIconButton import ua.acclorite.book_story.presentation.components.MoreDropDown @@ -83,10 +84,9 @@ 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 ua.acclorite.book_story.util.Selected +import ua.acclorite.book_story.presentation.ui.DefaultTransition +import ua.acclorite.book_story.presentation.ui.Transitions +import ua.acclorite.book_story.presentation.ui.elevation import java.util.UUID @OptIn( @@ -98,8 +98,7 @@ fun LibraryScreen( viewModel: LibraryViewModel, browseViewModel: BrowseViewModel, historyViewModel: HistoryViewModel, - navigator: Navigator, - addedBooks: List + navigator: Navigator ) { val state by viewModel.state.collectAsState() val pagerState = rememberPagerState( @@ -114,21 +113,21 @@ fun LibraryScreen( ) val focusRequester = remember { FocusRequester() } - LaunchedEffect(Unit) { - if (addedBooks.isNotEmpty()) { - viewModel.onEvent(LibraryEvent.OnPreloadBooks(addedBooks)) - navigator.clearArgument("added_books") - } - } LaunchedEffect(Unit) { viewModel.onEvent(LibraryEvent.OnScrollToPage(state.currentPage, pagerState)) } LaunchedEffect(pagerState.currentPage) { - viewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(pagerState.currentPage)) + if (pagerState.currentPage != state.currentPage) { + viewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(pagerState.currentPage)) + } } if (state.showMoveDialog) { - LibraryMoveDialog(viewModel = viewModel, pagerState = pagerState) + LibraryMoveDialog( + viewModel = viewModel, + historyViewModel = historyViewModel, + pagerState = pagerState + ) } if (state.showDeleteDialog) { LibraryDeleteDialog( @@ -156,13 +155,7 @@ fun LibraryScreen( content1NavigationIcon = {}, content1Title = { Row(verticalAlignment = Alignment.CenterVertically) { - Text( - stringResource(id = R.string.library_screen), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, - overflow = TextOverflow.Ellipsis, - maxLines = 1 - ) + Text(stringResource(id = R.string.library_screen)) Spacer(modifier = Modifier.width(6.dp)) Text( text = state.books.size.toString(), @@ -179,8 +172,7 @@ fun LibraryScreen( CustomIconButton( icon = Icons.Default.Search, contentDescription = stringResource(id = R.string.search_content_desc), - disableOnClick = false, - enabled = !state.showSearch + disableOnClick = true, ) { viewModel.onEvent(LibraryEvent.OnSearchShowHide) } @@ -204,8 +196,6 @@ fun LibraryScreen( id = R.string.selected_items_count_query, state.selectedItemsCount.coerceAtLeast(1) ), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface, overflow = TextOverflow.Ellipsis, maxLines = 1 ) @@ -216,8 +206,12 @@ fun LibraryScreen( contentDescription = stringResource( id = R.string.move_books_content_desc ), - disableOnClick = true - ) { + enabled = !state.isLoading + && !state.isRefreshing + && !state.showMoveDialog, + disableOnClick = false, + + ) { viewModel.onEvent(LibraryEvent.OnShowHideMoveDialog) } CustomIconButton( @@ -225,7 +219,10 @@ fun LibraryScreen( contentDescription = stringResource( id = R.string.delete_books_content_desc ), - disableOnClick = true + enabled = !state.isLoading + && !state.isRefreshing + && !state.showDeleteDialog, + disableOnClick = false ) { viewModel.onEvent(LibraryEvent.OnShowHideDeleteDialog) } @@ -310,11 +307,13 @@ fun LibraryScreen( LaunchedEffect(state.books) { categorizedBooks.clear() - categorizedBooks.addAll(state.books.filter { it.first.category == category } - .sortedWith( - compareByDescending> { it.first.lastOpened } - .thenBy { it.first.title } - ) + categorizedBooks.addAll( + state.books + .filter { it.first.category == category } + .sortedWith( + compareByDescending> { it.first.lastOpened } + .thenBy { it.first.title } + ) ) categoryIsLoading = false } @@ -323,8 +322,7 @@ fun LibraryScreen( DefaultTransition(visible = !state.isLoading && !categoryIsLoading) { LazyVerticalGrid( columns = GridCells.Adaptive(120.dp), - modifier = Modifier - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(8.dp) ) { items( @@ -341,7 +339,7 @@ fun LibraryScreen( navigator.navigate( Screen.BOOK_INFO, false, - Argument("book", it.first.id) + Argument("book", it.first) ) } }, @@ -351,10 +349,11 @@ fun LibraryScreen( } }, onButtonClick = { - navigator.navigate( - Screen.READER, - false, - Argument("book", it.first.id) + viewModel.onEvent( + LibraryEvent.OnNavigateToReaderScreen( + navigator, + it.first + ) ) } ) @@ -363,7 +362,9 @@ fun LibraryScreen( } AnimatedVisibility( - visible = !state.isLoading && !state.isRefreshing && categorizedBooks.isEmpty() + visible = !state.isLoading + && !state.isRefreshing + && categorizedBooks.isEmpty() && !categoryIsLoading, modifier = Modifier.align(Alignment.Center), enter = Transitions.DefaultTransitionIn, 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 10b956a2..17e1e5c0 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 @@ -37,10 +37,10 @@ import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.Book +import ua.acclorite.book_story.domain.util.Selected import ua.acclorite.book_story.presentation.data.removeDigits import ua.acclorite.book_story.presentation.data.removeTrailingZero -import ua.acclorite.book_story.ui.elevation -import ua.acclorite.book_story.util.Selected +import ua.acclorite.book_story.presentation.ui.elevation /** * Library list element item. @@ -93,7 +93,7 @@ fun LibraryBookItem( modifier = Modifier .aspectRatio(1f / 1.5f) .fillMaxWidth() - .clip(MaterialTheme.shapes.large) + .clip(MaterialTheme.shapes.medium) .background(MaterialTheme.elevation()) .combinedClickable( onClick = { @@ -110,7 +110,7 @@ fun LibraryBookItem( contentDescription = stringResource(id = R.string.cover_image_content_desc), modifier = Modifier .fillMaxSize() - .clip(MaterialTheme.shapes.large), + .clip(MaterialTheme.shapes.medium), contentScale = ContentScale.Crop ) } else { 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 efbc3e97..3390d941 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 @@ -2,6 +2,7 @@ package ua.acclorite.book_story.presentation.screens.library.components import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -13,7 +14,6 @@ import androidx.compose.material3.Divider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.Tab -import androidx.compose.material3.TabRow import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.material3.Text @@ -22,7 +22,7 @@ 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.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R @@ -30,7 +30,7 @@ import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Category 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.elevation +import ua.acclorite.book_story.presentation.ui.elevation /** * Tab row, either scrollable or static, depends on screen width. @@ -60,83 +60,34 @@ fun LibraryTabRow(viewModel: LibraryViewModel, books: List, pagerState: Pa ) } - if (LocalConfiguration.current.screenWidthDp > 450) { - TabRow( + Box(Modifier.fillMaxWidth()) { + Divider( modifier = Modifier + .align(Alignment.BottomCenter) .fillMaxWidth(), - selectedTabIndex = pagerState.currentPage, - containerColor = MaterialTheme.colorScheme.surface, - divider = { - Divider(color = MaterialTheme.colorScheme.surfaceVariant) - }, - indicator = { tabPositions -> - TabRowDefaults.Indicator( - Modifier - .tabIndicatorOffset(tabPositions[pagerState.currentPage]) - .padding(horizontal = 16.dp) - .clip( - RoundedCornerShape( - topStartPercent = 100, - topEndPercent = 100 - ) - ), - color = MaterialTheme.colorScheme.primary - ) - } - ) { - tabItems.forEachIndexed { index, tabItem -> - Tab( - modifier = Modifier, - selected = pagerState.currentPage == index, - onClick = { - viewModel.onEvent(LibraryEvent.OnScrollToPage(index, pagerState)) - }, - selectedContentColor = MaterialTheme.colorScheme.primary, - unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant, - text = { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - tabItem.first, - style = MaterialTheme.typography.bodyLarge, - maxLines = 1, - ) - Spacer(modifier = Modifier.width(6.dp)) - Text( - text = tabItem.second.toString(), - modifier = Modifier - .clip(MaterialTheme.shapes.medium) - .background(MaterialTheme.elevation(6.dp)) - .padding(horizontal = 8.dp, vertical = 4.dp), - color = MaterialTheme.colorScheme.onSurfaceVariant, - style = MaterialTheme.typography.bodyMedium - ) - } - } - ) - } - } - } else { + color = MaterialTheme.colorScheme.surfaceVariant + ) ScrollableTabRow( modifier = Modifier - .fillMaxWidth(), + .fillMaxWidth() + .padding(bottom = 0.5.dp), selectedTabIndex = pagerState.currentPage, - containerColor = MaterialTheme.colorScheme.surface, - divider = { - Divider(color = MaterialTheme.colorScheme.surfaceVariant) - }, + containerColor = Color.Transparent, edgePadding = 0.dp, + divider = {}, indicator = { tabPositions -> TabRowDefaults.Indicator( Modifier .tabIndicatorOffset(tabPositions[pagerState.currentPage]) .padding(horizontal = 16.dp) + .padding(end = 5.5.dp) .clip( RoundedCornerShape( - topStartPercent = 100, - topEndPercent = 100 + 100 ) ), - color = MaterialTheme.colorScheme.primary + color = MaterialTheme.colorScheme.primary, + height = 3.dp ) } ) { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryMoveDialog.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryMoveDialog.kt index a904a4cb..398b1d86 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryMoveDialog.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/components/dialog/LibraryMoveDialog.kt @@ -15,6 +15,8 @@ import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithLazyColumn import ua.acclorite.book_story.presentation.components.custom_dialog.SelectableDialogItem +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 @@ -23,7 +25,11 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewMode */ @OptIn(ExperimentalFoundationApi::class) @Composable -fun LibraryMoveDialog(viewModel: LibraryViewModel, pagerState: PagerState) { +fun LibraryMoveDialog( + viewModel: LibraryViewModel, + historyViewModel: HistoryViewModel, + pagerState: PagerState +) { val state by viewModel.state.collectAsState() val context = LocalContext.current @@ -38,7 +44,14 @@ fun LibraryMoveDialog(viewModel: LibraryViewModel, pagerState: PagerState) { isActionEnabled = true, onDismiss = { viewModel.onEvent(LibraryEvent.OnShowHideMoveDialog) }, onAction = { - viewModel.onEvent(LibraryEvent.OnMoveBooks(pagerState)) + viewModel.onEvent( + LibraryEvent.OnMoveBooks( + pagerState, + refreshList = { + historyViewModel.onEvent(HistoryEvent.OnLoadList) + } + ) + ) Toast.makeText( context, context.getString(R.string.books_moved), diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryEvent.kt index ed83a084..d6f8b374 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryEvent.kt @@ -7,10 +7,10 @@ import androidx.compose.foundation.pager.PagerState import androidx.compose.ui.focus.FocusRequester import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.util.Selected +import ua.acclorite.book_story.domain.util.Selected +import ua.acclorite.book_story.presentation.data.Navigator sealed class LibraryEvent { - data class OnPreloadBooks(val books: List) : LibraryEvent() data object OnRefreshList : LibraryEvent() data object OnLoadList : LibraryEvent() data class OnScrollToPage(val index: Int, val pagerState: PagerState) : LibraryEvent() @@ -24,8 +24,12 @@ sealed class LibraryEvent { data object OnClearSelectedBooks : LibraryEvent() data object OnShowHideMoveDialog : LibraryEvent() data class OnSelectCategory(val category: Category) : LibraryEvent() - data class OnMoveBooks(val pagerState: PagerState) : LibraryEvent() + data class OnMoveBooks(val pagerState: PagerState, val refreshList: () -> Unit) : LibraryEvent() data object OnShowHideDeleteDialog : LibraryEvent() data class OnDeleteBooks(val refreshList: () -> Unit) : LibraryEvent() data class OnUpdateBook(val book: Book) : LibraryEvent() + data class OnNavigateToReaderScreen( + val navigator: Navigator, + val book: Book + ) : LibraryEvent() } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryState.kt index deca028e..402fc0d1 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryState.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/library/data/LibraryState.kt @@ -3,7 +3,7 @@ package ua.acclorite.book_story.presentation.screens.library.data import androidx.compose.runtime.Immutable import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Category -import ua.acclorite.book_story.util.Selected +import ua.acclorite.book_story.domain.util.Selected @Immutable data class LibraryState( @@ -25,6 +25,4 @@ data class LibraryState( val selectedCategory: Category = Category.READING, val showDeleteDialog: Boolean = false, - - - ) +) 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 65020b90..eabc42d5 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 @@ -12,10 +12,15 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import ua.acclorite.book_story.domain.model.Category +import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.use_case.DeleteBooks import ua.acclorite.book_story.domain.use_case.GetBooks +import ua.acclorite.book_story.domain.use_case.InsertHistory import ua.acclorite.book_story.domain.use_case.UpdateBooks -import ua.acclorite.book_story.util.Resource +import ua.acclorite.book_story.domain.util.Resource +import ua.acclorite.book_story.presentation.data.Argument +import ua.acclorite.book_story.presentation.data.Screen +import java.util.Date import javax.inject.Inject @OptIn(ExperimentalFoundationApi::class) @@ -23,7 +28,8 @@ import javax.inject.Inject class LibraryViewModel @Inject constructor( private val getBooks: GetBooks, private val updateBooks: UpdateBooks, - private val deleteBooks: DeleteBooks + private val deleteBooks: DeleteBooks, + private val insertHistory: InsertHistory ) : ViewModel() { private val _state = MutableStateFlow(LibraryState()) @@ -33,6 +39,7 @@ class LibraryViewModel @Inject constructor( val isReady = _isReady.asStateFlow() private var job: Job? = null + private var job2: Job? = null init { viewModelScope.launch(Dispatchers.IO) { @@ -69,16 +76,9 @@ class LibraryViewModel @Inject constructor( } } - is LibraryEvent.OnPreloadBooks -> { - _state.update { - it.copy( - books = event.books.map { book -> Pair(book, false) } - ) - } - } - is LibraryEvent.OnRefreshList -> { - viewModelScope.launch(Dispatchers.IO) { + job2?.cancel() + job2 = viewModelScope.launch(Dispatchers.IO) { _state.update { it.copy( isRefreshing = true, @@ -101,7 +101,8 @@ class LibraryViewModel @Inject constructor( viewModelScope.launch(Dispatchers.IO) { _state.update { it.copy( - isLoading = true + isLoading = true, + hasSelectedItems = false ) } getBooksFromDatabase() @@ -149,7 +150,7 @@ class LibraryViewModel @Inject constructor( ) } job?.cancel() - job = viewModelScope.launch { + job = viewModelScope.launch(Dispatchers.IO) { delay(500) getBooksFromDatabase() } @@ -157,20 +158,19 @@ class LibraryViewModel @Inject constructor( is LibraryEvent.OnSelectBook -> { viewModelScope.launch(Dispatchers.IO) { - val indexOfBook = _state.value.books.indexOf(event.book) - - if (indexOfBook == -1) { - return@launch + val editedList = _state.value.books.map { + if (it.first.id == event.book.first.id) { + it.copy( + second = event.select ?: !it.second + ) + } else { + it + } } - val editedList = _state.value.books.toMutableList() - editedList[indexOfBook] = editedList[indexOfBook].copy( - second = event.select ?: !editedList[indexOfBook].second - ) - _state.update { it.copy( - books = editedList.toList(), + books = editedList, selectedItemsCount = editedList.filter { book -> book.second }.size, hasSelectedItems = editedList.any { book -> book.second } ) @@ -234,6 +234,7 @@ class LibraryViewModel @Inject constructor( updateBooks.execute(books) getBooksFromDatabase() + event.refreshList() onEvent(LibraryEvent.OnClearSelectedBooks) onEvent( @@ -278,14 +279,16 @@ class LibraryViewModel @Inject constructor( is LibraryEvent.OnUpdateBook -> { 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 books = _state.value.books.map { + if (it.first.id == event.book.id) { + it.copy( + first = event.book + ) + } else { + it + } } - books[index] = Pair(event.book, books[index].second) _state.update { it.copy( books = books @@ -293,6 +296,30 @@ class LibraryViewModel @Inject constructor( } } } + + is LibraryEvent.OnNavigateToReaderScreen -> { + viewModelScope.launch { + event.book.id?.let { + insertHistory.execute( + listOf( + History( + null, + it, + null, + Date().time + ) + ) + ) + } + event.navigator.navigate( + Screen.READER, + false, + Argument( + "book", event.book + ) + ) + } + } } } 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 d963d002..466d6b0b 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 @@ -28,6 +28,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -54,6 +55,7 @@ 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.domain.util.Constants import ua.acclorite.book_story.presentation.components.CustomSelectionContainer import ua.acclorite.book_story.presentation.components.is_messages.IsError import ua.acclorite.book_story.presentation.data.MainViewModel @@ -69,8 +71,7 @@ import ua.acclorite.book_story.presentation.screens.reader.components.app_bar.Re import ua.acclorite.book_story.presentation.screens.reader.components.settings_bottom_sheet.ReaderSettingsBottomSheet import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel -import ua.acclorite.book_story.ui.elevation -import ua.acclorite.book_story.util.Constants +import ua.acclorite.book_story.presentation.ui.elevation @OptIn(FlowPreview::class) @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @@ -86,8 +87,10 @@ fun ReaderScreen( val systemBarsColor = MaterialTheme.elevation(20.dp).copy(0.85f) var loading by remember { mutableStateOf(true) } - val state by viewModel.state.collectAsState() val listState = rememberLazyListState() + + val state by viewModel.state.collectAsState() + val canScrollForward by remember { derivedStateOf { listState.canScrollForward } } val nestedScrollConnection = remember { object : NestedScrollConnection { override fun onPostScroll( @@ -106,47 +109,45 @@ fun ReaderScreen( } } - val fontFamily = Constants.FONTS.find { - it.id == mainViewModel.fontFamily.collectAsState().value!! - } ?: Constants.FONTS[0] - val fontStyle = when (mainViewModel.isItalic.collectAsState().value!!) { - false -> FontStyle.Normal - true -> FontStyle.Italic + val mainState by mainViewModel.state.collectAsState() + val fontFamily = remember(mainState.fontFamily) { + Constants.FONTS.find { + it.id == mainState.fontFamily + } ?: Constants.FONTS[0] } - val fontSize = mainViewModel.fontSize.collectAsState().value!! - val lineHeight = mainViewModel.lineHeight.collectAsState().value!! - val paragraphHeight = mainViewModel.paragraphHeight.collectAsState().value!! - val paragraphIndentation = mainViewModel.paragraphIndentation.collectAsState().value!! - val backgroundColor = mainViewModel.backgroundColor.collectAsState().value!! - val fontColor = mainViewModel.fontColor.collectAsState().value!! LaunchedEffect(Unit) { viewModel.init( navigator, context, - listState, refreshList = { libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) historyViewModel.onEvent(HistoryEvent.OnLoadList) }, + listState = listState, onLoaded = { loading = false } ) } + LaunchedEffect(canScrollForward) { + if (!canScrollForward && !state.showMenu && !loading) { + viewModel.onEvent(ReaderEvent.OnShowHideMenu(context = context)) + } + } LaunchedEffect(listState) { snapshotFlow { - listState.firstVisibleItemIndex - }.debounce(1000).collectLatest { + listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset + }.debounce(500).collectLatest { items -> if (!loading) { val lastVisibleItemIndex = listState.layoutInfo.visibleItemsInfo.last().index - val progress = if (it > 0) { + val progress = if (items.first > 0) { if (lastVisibleItemIndex >= (listState.layoutInfo.totalItemsCount - 1)) { 1f } else { - (it.toFloat() / (state.book.text.lastIndex).toFloat()) + (items.first.toFloat() / (state.book.text.lastIndex).toFloat()) } } else { 0f @@ -154,14 +155,13 @@ fun ReaderScreen( viewModel.onEvent( ReaderEvent.OnChangeProgress( - progress, - navigator, + progress = progress, + navigator = navigator, + firstVisibleItemIndex = items.first, + firstVisibleItemOffset = items.second, refreshList = { book -> - libraryViewModel.onEvent( - LibraryEvent.OnUpdateBook( - book - ) - ) + libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(book)) + historyViewModel.onEvent(HistoryEvent.OnUpdateBook(book)) } ) ) @@ -186,7 +186,10 @@ fun ReaderScreen( ) { ReaderTopBar( viewModel = viewModel, + libraryViewModel = libraryViewModel, + historyViewModel = historyViewModel, navigator = navigator, + listState = listState, containerColor = systemBarsColor ) } @@ -200,8 +203,9 @@ fun ReaderScreen( ReaderBottomBar( viewModel = viewModel, libraryViewModel = libraryViewModel, + historyViewModel = historyViewModel, + listState = listState, navigator = navigator, - scrollState = listState, systemBarsColor = systemBarsColor ) } @@ -280,36 +284,44 @@ fun ReaderScreen( itemsIndexed( state.book.text, key = { _, key -> key.id } ) { index, line -> - val text = - remember(paragraphIndentation) { "${if (paragraphIndentation) " " else ""}${line.line}" } - val fontCol = remember(fontColor) { Color(fontColor.toULong()) } - val backgroundCol = - remember(backgroundColor) { Color(backgroundColor.toULong()) } - val lineHeightSp = remember(fontSize, lineHeight) { (fontSize + lineHeight).sp } + val text = remember(mainState.paragraphIndentation) { + "${if (mainState.paragraphIndentation!!) " " else ""}${line.line}" + } + val fontColor = remember(mainState.fontColor) { + Color(mainState.fontColor!!.toULong()) + } + val backgroundColor = remember(mainState.backgroundColor) { + Color(mainState.backgroundColor!!.toULong()) + } + val lineHeight = remember(mainState.fontSize, mainState.lineHeight) { + (mainState.fontSize!! + mainState.lineHeight!!).sp + } Column( Modifier - .background(backgroundCol) + .background(backgroundColor) .fillMaxWidth() .padding( - top = if (index == 0) 36.dp else 0.dp, + top = if (index == 0) 18.dp else 0.dp, start = 18.dp, end = 18.dp, - bottom = if (index == state.book.text.lastIndex) 36.dp - else (paragraphHeight * 3).dp + bottom = if (index == state.book.text.lastIndex) 18.dp + else (mainState.paragraphHeight!! * 3).dp ) ) { Text( text = text, - color = fontCol, - + color = fontColor, style = TextStyle( lineBreak = LineBreak.Paragraph ), fontFamily = fontFamily.font, - fontStyle = fontStyle, - fontSize = fontSize.sp, - lineHeight = lineHeightSp + fontStyle = when (mainState.isItalic!!) { + true -> FontStyle.Italic + false -> FontStyle.Normal + }, + fontSize = mainState.fontSize!!.sp, + lineHeight = lineHeight ) } } @@ -319,7 +331,9 @@ fun ReaderScreen( DisableSelection { ReaderEndItem( libraryViewModel = libraryViewModel, + historyViewModel = historyViewModel, viewModel = viewModel, + listState = listState, navigator = navigator ) } @@ -343,10 +357,21 @@ fun ReaderScreen( icon = painterResource(id = R.drawable.error), actionTitle = stringResource(id = R.string.go_back), action = { - navigator.navigateBack() libraryViewModel.onEvent(LibraryEvent.OnLoadList) - historyViewModel.onEvent(HistoryEvent.OnLoadList) - viewModel.onEvent(ReaderEvent.OnShowSystemBars(context)) + viewModel.onEvent( + ReaderEvent.OnGoBack( + context, + navigator, + refreshList = { + libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) + historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it)) + }, + listState = listState, + navigate = { + it.navigateBack() + } + ) + ) } ) } else { @@ -369,7 +394,19 @@ fun ReaderScreen( } BackHandler { - navigator.navigateBack() - viewModel.onEvent(ReaderEvent.OnShowSystemBars(context)) + viewModel.onEvent( + ReaderEvent.OnGoBack( + context, + navigator, + refreshList = { + libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) + historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it)) + }, + listState = listState, + navigate = { + it.navigateBack() + } + ) + ) } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderEndItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderEndItem.kt index cc1b5c15..9ff45a5d 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderEndItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderEndItem.kt @@ -8,11 +8,12 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.PaddingValues 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.navigationBarsIgnoringVisibility import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -32,6 +33,8 @@ import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Screen +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.presentation.screens.reader.data.ReaderEvent @@ -44,7 +47,9 @@ import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel @Composable fun ReaderEndItem( libraryViewModel: LibraryViewModel, + historyViewModel: HistoryViewModel, viewModel: ReaderViewModel, + listState: LazyListState, navigator: Navigator ) { val state by viewModel.state.collectAsState() @@ -54,12 +59,11 @@ fun ReaderEndItem( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.surface) - .windowInsetsPadding(WindowInsets.navigationBarsIgnoringVisibility) .padding( - top = 24.dp, - start = 24.dp, - end = 24.dp, - bottom = 8.dp + horizontal = 36.dp, + vertical = 108.dp + WindowInsets.navigationBarsIgnoringVisibility + .asPaddingValues() + .calculateBottomPadding() ), horizontalAlignment = Alignment.CenterHorizontally ) { @@ -72,7 +76,11 @@ fun ReaderEndItem( ) Spacer(modifier = Modifier.height(4.dp)) Text( - stringResource(id = R.string.letters_and_words, state.letters, state.words), + stringResource( + id = R.string.letters_and_words, + state.book.letters, + state.book.words + ), color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyLarge, textAlign = TextAlign.Center @@ -83,15 +91,15 @@ fun ReaderEndItem( if (state.book.category != Category.ALREADY_READ) { viewModel.onEvent( ReaderEvent.OnMoveBookToAlreadyRead( - refreshList = { - libraryViewModel.onEvent( - LibraryEvent.OnLoadList - ) + onUpdateCategories = { + libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) + historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it)) }, updatePage = { libraryViewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(it)) }, navigator = navigator, + listState = listState, context = context ) ) @@ -103,11 +111,19 @@ fun ReaderEndItem( ).show() } else { viewModel.onEvent( - ReaderEvent.OnShowSystemBars( - context + ReaderEvent.OnGoBack( + context, + navigator, + refreshList = { + libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) + historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it)) + }, + listState = listState, + navigate = { + it.navigate(Screen.LIBRARY, true) + } ) ) - navigator.navigate(Screen.LIBRARY, true) } }, contentPadding = PaddingValues(horizontal = 12.dp), 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 952b183b..0539f8ec 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 @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsIgnoringVisibility import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Image import androidx.compose.material3.Icon @@ -34,7 +35,7 @@ import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel -import ua.acclorite.book_story.ui.elevation +import ua.acclorite.book_story.presentation.ui.elevation /** * Reader start item. Displays at the beginning of the book. @@ -49,7 +50,7 @@ fun ReaderStartItem(viewModel: ReaderViewModel) { .fillMaxWidth() .background(MaterialTheme.colorScheme.surface) .windowInsetsPadding(WindowInsets.statusBarsIgnoringVisibility) - .padding(24.dp) + .padding(horizontal = 24.dp, vertical = 80.dp) ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier @@ -59,7 +60,7 @@ fun ReaderStartItem(viewModel: ReaderViewModel) { modifier = Modifier .height(135.dp) .width(90.dp) - .clip(MaterialTheme.shapes.medium) + .clip(RoundedCornerShape(10.dp)) .background(MaterialTheme.elevation()) ) { if (state.book.coverImage != null) { @@ -68,7 +69,7 @@ fun ReaderStartItem(viewModel: ReaderViewModel) { contentDescription = stringResource(id = R.string.cover_image_content_desc), modifier = Modifier .fillMaxSize() - .clip(MaterialTheme.shapes.medium), + .clip(RoundedCornerShape(10.dp)), contentScale = ContentScale.Crop ) } else { 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 e20b452b..0babba89 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 @@ -24,6 +24,8 @@ import androidx.compose.ui.unit.dp import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.removeDigits import ua.acclorite.book_story.presentation.data.removeTrailingZero +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.presentation.screens.reader.data.ReaderEvent @@ -36,8 +38,9 @@ import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel fun ReaderBottomBar( viewModel: ReaderViewModel, libraryViewModel: LibraryViewModel, + historyViewModel: HistoryViewModel, + listState: LazyListState, navigator: Navigator, - scrollState: LazyListState, systemBarsColor: Color ) { val state by viewModel.state.collectAsState() @@ -74,20 +77,19 @@ fun ReaderBottomBar( Slider( value = state.book.progress, onValueChange = { + viewModel.onEvent(ReaderEvent.OnScroll(listState, it)) viewModel.onEvent( ReaderEvent.OnChangeProgress( progress = it, navigator = navigator, + firstVisibleItemIndex = listState.firstVisibleItemIndex, + firstVisibleItemOffset = 0, refreshList = { book -> - libraryViewModel.onEvent( - LibraryEvent.OnUpdateBook( - book - ) - ) + libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(book)) + historyViewModel.onEvent(HistoryEvent.OnUpdateBook(book)) } ) ) - viewModel.onEvent(ReaderEvent.OnScroll(scrollState, it)) }, colors = SliderDefaults.colors( activeTrackColor = MaterialTheme.colorScheme.primary, 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 d747c949..15c581c6 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 @@ -5,7 +5,9 @@ import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme @@ -26,12 +28,14 @@ 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 import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.data.removeDigits import ua.acclorite.book_story.presentation.data.removeTrailingZero +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.presentation.screens.reader.data.ReaderEvent import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel @@ -40,7 +44,14 @@ import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel */ @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColor: Color) { +fun ReaderTopBar( + viewModel: ReaderViewModel, + libraryViewModel: LibraryViewModel, + historyViewModel: HistoryViewModel, + listState: LazyListState, + navigator: Navigator, + containerColor: Color +) { val context = LocalContext.current as ComponentActivity val state by viewModel.state.collectAsState() @@ -56,8 +67,25 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo TopAppBar( navigationIcon = { - GoBackButton(navigator = navigator) { - viewModel.onEvent(ReaderEvent.OnShowSystemBars(context)) + CustomIconButton( + icon = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(id = R.string.go_back_content_desc), + disableOnClick = true + ) { + viewModel.onEvent( + ReaderEvent.OnGoBack( + context, + navigator, + refreshList = { + libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) + historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it)) + }, + listState = listState, + navigate = { + it.navigateBack() + } + ) + ) } }, title = { @@ -74,12 +102,21 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo interactionSource = null, indication = null, onClick = { - navigator.navigateWithoutBackStack( - Screen.BOOK_INFO, - true, - Argument( - "book", - state.book.id + viewModel.onEvent( + ReaderEvent.OnGoBack( + context, + navigator, + refreshList = { + libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it)) + historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it)) + }, + listState = listState, + navigate = { + it.navigateWithoutBackStack( + Screen.BOOK_INFO, + true + ) + } ) ) } 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 3abe97d1..b4b1d6e1 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 @@ -35,6 +35,7 @@ import androidx.compose.ui.text.font.FontStyle 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.domain.util.Constants import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainViewModel @@ -44,9 +45,8 @@ import ua.acclorite.book_story.presentation.screens.settings.components.Checkbox import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle import ua.acclorite.book_story.presentation.screens.settings.components.ColorPickerWithTitle import ua.acclorite.book_story.presentation.screens.settings.components.SliderWithTitle -import ua.acclorite.book_story.ui.ElevationDefaults -import ua.acclorite.book_story.ui.elevation -import ua.acclorite.book_story.util.Constants +import ua.acclorite.book_story.presentation.ui.ElevationDefaults +import ua.acclorite.book_story.presentation.ui.elevation /** * Settings bottom sheet. Has General and Colors categories. @@ -63,16 +63,13 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie val navigationBarPadding = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() - val fontFamily = Constants.FONTS.find { - it.id == mainViewModel.fontFamily.collectAsState().value!! - } ?: Constants.FONTS[0] - val fontStyle = mainViewModel.isItalic.collectAsState().value!! - val fontSize = mainViewModel.fontSize.collectAsState().value!! - val lineHeight = mainViewModel.lineHeight.collectAsState().value!! - val paragraphHeight = mainViewModel.paragraphHeight.collectAsState().value!! - val paragraphIndentation = mainViewModel.paragraphIndentation.collectAsState().value!! - val backgroundColor = mainViewModel.backgroundColor.collectAsState().value!! - val fontColor = mainViewModel.fontColor.collectAsState().value!! + val state by mainViewModel.state.collectAsState() + + val fontFamily = remember(state.fontFamily) { + Constants.FONTS.find { + it.id == state.fontFamily + } ?: Constants.FONTS[0] + } val scrimColor = if (currentPage == 1) Color.Transparent else BottomSheetDefaults.ScrimColor @@ -161,7 +158,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie fontFamily = fontFamily.font, fontStyle = FontStyle.Normal ), - !fontStyle + !state.isItalic!! ), ChipItem( "italic", @@ -170,7 +167,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie fontFamily = fontFamily.font, fontStyle = FontStyle.Italic ), - fontStyle + state.isItalic!! ), ), onClick = { @@ -187,7 +184,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie } item { SliderWithTitle( - value = fontSize to "pt", + value = state.fontSize!! to "pt", fromValue = 10, toValue = 35, title = stringResource(id = R.string.font_size_option), @@ -212,7 +209,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie } item { SliderWithTitle( - value = lineHeight to "pt", + value = state.lineHeight!! to "pt", fromValue = 1, toValue = 16, title = stringResource(id = R.string.line_height_option), @@ -225,7 +222,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie } item { SliderWithTitle( - value = paragraphHeight to "pt", + value = state.paragraphHeight!! to "pt", fromValue = 0, toValue = 24, title = stringResource(id = R.string.paragraph_height_option), @@ -238,11 +235,11 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie } item { CheckboxWithTitle( - selected = paragraphIndentation, + selected = state.paragraphIndentation!!, title = stringResource(id = R.string.paragraph_indentation_option) ) { mainViewModel.onEvent( - MainEvent.OnChangeParagraphIndentation(!paragraphIndentation) + MainEvent.OnChangeParagraphIndentation(!state.paragraphIndentation!!) ) } } @@ -254,7 +251,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie } item { ColorPickerWithTitle( - value = Color(backgroundColor.toULong()), + value = Color(state.backgroundColor!!.toULong()), title = stringResource(id = R.string.background_color_option), onValueChange = { mainViewModel.onEvent( @@ -267,7 +264,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie } item { ColorPickerWithTitle( - value = Color(fontColor.toULong()), + value = Color(state.fontColor!!.toULong()), title = stringResource(id = R.string.font_color_option), onValueChange = { mainViewModel.onEvent( 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 ec1f6afe..3fa17f9a 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 @@ -12,8 +12,7 @@ import ua.acclorite.book_story.presentation.data.Navigator sealed class ReaderEvent { data class OnTextIsEmpty(val onLoaded: () -> Unit) : ReaderEvent() data class OnLoadText( - val scrollState: LazyListState, - val navigator: Navigator, + val listState: LazyListState, val refreshList: (Book) -> Unit, val onLoaded: () -> Unit, val onTextIsEmpty: () -> Unit @@ -22,19 +21,29 @@ sealed class ReaderEvent { data class OnShowHideMenu(val show: Boolean? = null, val context: ComponentActivity) : ReaderEvent() - data class OnShowSystemBars(val context: ComponentActivity) : ReaderEvent() + data class OnGoBack( + val context: ComponentActivity, + val navigator: Navigator, + val listState: LazyListState, + val refreshList: (Book) -> Unit, + val navigate: (Navigator) -> Unit + ) : ReaderEvent() + data class OnChangeProgress( val progress: Float, val navigator: Navigator, + val firstVisibleItemIndex: Int, + val firstVisibleItemOffset: Int, val refreshList: (Book) -> Unit ) : ReaderEvent() - data class OnScroll(val scrollState: LazyListState, val progress: Float) : ReaderEvent() + data class OnScroll(val listState: LazyListState, val progress: Float) : ReaderEvent() data object OnShowHideSettingsBottomSheet : ReaderEvent() data class OnScrollToSettingsPage(val page: Int, val pagerState: PagerState) : ReaderEvent() data class OnMoveBookToAlreadyRead( val context: ComponentActivity, - val refreshList: () -> Unit, + val listState: LazyListState, + val onUpdateCategories: (Book) -> Unit, val updatePage: (Int) -> Unit, val navigator: Navigator ) : ReaderEvent() 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 62a91d8a..5614fb99 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 @@ -2,17 +2,13 @@ package ua.acclorite.book_story.presentation.screens.reader.data import androidx.compose.runtime.Immutable import ua.acclorite.book_story.domain.model.Book -import ua.acclorite.book_story.util.Constants -import ua.acclorite.book_story.util.UIText +import ua.acclorite.book_story.domain.util.Constants +import ua.acclorite.book_story.domain.util.UIText @Immutable data class ReaderState( val book: Book = Constants.EMPTY_BOOK, - val letters: Int = 0, - val words: Int = 0, - val errorMessage: UIText? = null, - val showMenu: Boolean = false, val showSettingsBottomSheet: Boolean = false, val currentPage: Int = 0, 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 c77f51f1..53de02bf 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 @@ -22,17 +22,14 @@ import kotlinx.coroutines.launch 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.GetBooksById +import ua.acclorite.book_story.domain.use_case.GetLatestHistory 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.domain.util.UIText import ua.acclorite.book_story.presentation.data.Argument import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Screen -import ua.acclorite.book_story.util.UIText -import java.util.Date import javax.inject.Inject import kotlin.math.roundToInt @@ -40,9 +37,8 @@ import kotlin.math.roundToInt @HiltViewModel class ReaderViewModel @Inject constructor( private val updateBooks: UpdateBooks, - private val insertHistory: InsertHistory, private val getText: GetText, - private val getBooksById: GetBooksById + private val getLatestHistory: GetLatestHistory ) : ViewModel() { private val _state = MutableStateFlow(ReaderState()) @@ -67,7 +63,6 @@ class ReaderViewModel @Inject constructor( event.onTextIsEmpty() } - val time = Date().time val letters = text .replace("\n", "") .length @@ -76,50 +71,46 @@ class ReaderViewModel @Inject constructor( .split("\\s+".toRegex()) .size + val history = _state.value.book.id?.let { + getLatestHistory.execute( + it + ) + } + _state.update { it.copy( book = it.book.copy( text = text .split("\n") .map { line -> StringWithId(line.trim()) }, - lastOpened = time - ), - letters = letters, - words = words + lastOpened = history?.time, + letters = letters, + words = words + ) ) } updateBooks.execute( listOf(_state.value.book) ) - insertHistory.execute( - listOf( - History( - null, - _state.value.book.id!!, - time - ) - ) - ) event.refreshList(_state.value.book) - event.navigator.putArgument( - Argument("book", _state.value.book.id) - ) viewModelScope.launch { snapshotFlow { - event.scrollState.layoutInfo.totalItemsCount + event.listState.layoutInfo.totalItemsCount }.collectLatest { itemsCount -> - val scrollTo = - (_state.value.book.text.size * _state.value.book.progress) - .toInt() + val index = _state.value.book.scrollIndex + val offset = _state.value.book.scrollOffset if (itemsCount >= _state.value.book.text.size) { - if (scrollTo > 0) { + if (index > 0 || offset > 0) { var loaded = false for (i in 1..100) { try { - event.scrollState.scrollToItem(scrollTo) + event.listState.scrollToItem( + index, + offset + ) loaded = true break } catch (e: Exception) { @@ -143,6 +134,7 @@ class ReaderViewModel @Inject constructor( is ReaderEvent.OnShowHideMenu -> { val shouldShow = event.show ?: !_state.value.showMenu + val insetsController = WindowCompat.getInsetsController( event.context.window, event.context.window.decorView @@ -166,20 +158,57 @@ class ReaderViewModel @Inject constructor( } } - is ReaderEvent.OnShowSystemBars -> { - val insetsController = WindowCompat.getInsetsController( - event.context.window, - event.context.window.decorView - ) + is ReaderEvent.OnGoBack -> { + viewModelScope.launch { + val insetsController = WindowCompat.getInsetsController( + event.context.window, + event.context.window.decorView + ) - insetsController.show(WindowInsetsCompat.Type.systemBars()) + val firstVisibleItemIndex = event.listState.firstVisibleItemIndex + val firstVisibleItemOffset = event.listState.firstVisibleItemScrollOffset + val lastVisibleItemIndex = event.listState.layoutInfo + .visibleItemsInfo.last().index + + val progress = if (firstVisibleItemIndex > 0) { + if (lastVisibleItemIndex >= (event.listState.layoutInfo.totalItemsCount - 1)) { + 1f + } else { + (firstVisibleItemIndex.toFloat() / (_state.value.book.text.lastIndex) + .toFloat()) + } + } else { + 0f + } + + _state.update { + it.copy( + book = it.book.copy( + progress = progress, + scrollIndex = firstVisibleItemIndex, + scrollOffset = firstVisibleItemOffset + ) + ) + } + + updateBooks.execute( + listOf(_state.value.book) + ) + event.navigator.putArgument( + Argument("book", _state.value.book) + ) + event.refreshList(_state.value.book) + + insetsController.show(WindowInsetsCompat.Type.systemBars()) + event.navigate(event.navigator) + } } is ReaderEvent.OnScroll -> { viewModelScope.launch { val scrollTo = (_state.value.book.text.size * event.progress).roundToInt() - event.scrollState.scrollToItem( + event.listState.scrollToItem( scrollTo ) } @@ -190,7 +219,9 @@ class ReaderViewModel @Inject constructor( _state.update { it.copy( book = it.book.copy( - progress = event.progress + progress = event.progress, + scrollIndex = event.firstVisibleItemIndex, + scrollOffset = event.firstVisibleItemOffset ) ) } @@ -199,18 +230,23 @@ class ReaderViewModel @Inject constructor( listOf(_state.value.book) ) event.navigator.putArgument( - Argument("book", _state.value.book.id) + Argument( + "book", + _state.value.book + ) ) event.refreshList(_state.value.book) } } is ReaderEvent.OnShowHideSettingsBottomSheet -> { - _state.update { - it.copy( - currentPage = if (it.showSettingsBottomSheet) it.currentPage else 0, - showSettingsBottomSheet = !it.showSettingsBottomSheet - ) + viewModelScope.launch(Dispatchers.IO) { + _state.update { + it.copy( + currentPage = if (it.showSettingsBottomSheet) it.currentPage else 0, + showSettingsBottomSheet = !it.showSettingsBottomSheet + ) + } } } @@ -222,16 +258,32 @@ class ReaderViewModel @Inject constructor( is ReaderEvent.OnMoveBookToAlreadyRead -> { viewModelScope.launch { - onEvent(ReaderEvent.OnShowSystemBars(event.context)) - - val book = _state.value.book.copy( - category = Category.ALREADY_READ, - progress = 1f + onEvent( + ReaderEvent.OnGoBack( + event.context, + event.navigator, + event.listState, + refreshList = {}, + navigate = {} + ) ) - updateBooks.execute(listOf(book)) - event.refreshList() + val firstVisibleItemIndex = event.listState.firstVisibleItemIndex + val firstVisibleItemOffset = event.listState.firstVisibleItemScrollOffset + _state.update { + it.copy( + book = it.book.copy( + category = Category.ALREADY_READ, + progress = 1f, + scrollIndex = firstVisibleItemIndex, + scrollOffset = firstVisibleItemOffset + ) + ) + } + updateBooks.execute(listOf(_state.value.book)) + + event.onUpdateCategories(_state.value.book) event.updatePage( Category.entries.dropLastWhile { it != Category.ALREADY_READ @@ -298,30 +350,30 @@ class ReaderViewModel @Inject constructor( fun init( navigator: Navigator, context: ComponentActivity, - scrollState: LazyListState, + listState: LazyListState, refreshList: (Book) -> Unit, onLoaded: () -> Unit ) { - viewModelScope.launch { - val bookIndex = navigator.retrieveArgument("book") as? Int + viewModelScope.launch(Dispatchers.IO) { + val book = navigator.retrieveArgument("book") as? Book - if (bookIndex == null || bookIndex < 0) { + if (book == null) { navigator.navigateBack() return@launch } - val book = getBooksById.execute(listOf(bookIndex)).first() + viewModelScope.launch { + onEvent(ReaderEvent.OnShowHideMenu(false, context)) + } _state.update { ReaderState(book = book) } - onEvent(ReaderEvent.OnShowHideMenu(false, context)) onEvent( ReaderEvent.OnLoadText( - scrollState, - navigator, refreshList = { refreshList(it) }, + listState = listState, 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 43f034fd..5fbb86a7 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 @@ -29,7 +29,7 @@ import ua.acclorite.book_story.presentation.components.GoBackButton import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.screens.settings.components.SettingsCategoryItem -import ua.acclorite.book_story.ui.elevation +import ua.acclorite.book_story.presentation.ui.elevation @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -39,7 +39,7 @@ fun SettingsScreen( val listState = rememberLazyListState() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( canScroll = { - listState.canScrollForward + listState.canScrollForward || listState.canScrollBackward } ) @@ -71,6 +71,10 @@ fun SettingsScreen( .padding(top = paddingValues.calculateTopPadding()), state = listState ) { + item { + Spacer(modifier = Modifier.height(16.dp)) + } + item { SettingsCategoryItem( icon = Icons.Default.DisplaySettings, 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 76f360f4..105b2d8e 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 @@ -17,6 +17,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -32,8 +33,8 @@ import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle import ua.acclorite.book_story.presentation.screens.settings.components.ColorPickerWithTitle import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.theme_switcher.AppearanceSettingsThemeSwitcher -import ua.acclorite.book_story.ui.DarkTheme -import ua.acclorite.book_story.ui.elevation +import ua.acclorite.book_story.presentation.ui.DarkTheme +import ua.acclorite.book_story.presentation.ui.elevation @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -44,13 +45,11 @@ fun AppearanceSettings( val listState = rememberLazyListState() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( canScroll = { - listState.canScrollForward + listState.canScrollForward || listState.canScrollBackward } ) - val darkTheme = mainViewModel.darkTheme.collectAsState().value!! - val backgroundColor = mainViewModel.backgroundColor.collectAsState().value!! - val fontColor = mainViewModel.fontColor.collectAsState().value!! + val state by mainViewModel.state.collectAsState() Scaffold( Modifier @@ -81,7 +80,7 @@ fun AppearanceSettings( state = listState ) { item { - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(16.dp)) } item { CategoryTitle( @@ -105,7 +104,7 @@ fun AppearanceSettings( DarkTheme.FOLLOW_SYSTEM -> stringResource(id = R.string.dark_theme_follow_system) }, MaterialTheme.typography.labelLarge, - it == darkTheme + it == state.darkTheme ) }, ) { @@ -136,7 +135,7 @@ fun AppearanceSettings( item { ColorPickerWithTitle( - value = Color(backgroundColor.toULong()), + value = Color(state.backgroundColor!!.toULong()), title = stringResource(id = R.string.background_color_option), onValueChange = { mainViewModel.onEvent( @@ -149,7 +148,7 @@ fun AppearanceSettings( } item { ColorPickerWithTitle( - value = Color(fontColor.toULong()), + value = Color(state.fontColor!!.toULong()), title = stringResource(id = R.string.font_color_option), onValueChange = { mainViewModel.onEvent( 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 9274a3d1..5ee744bf 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,17 +10,18 @@ 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.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.util.Constants import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainViewModel -import ua.acclorite.book_story.ui.Theme -import ua.acclorite.book_story.ui.isDark -import ua.acclorite.book_story.util.Constants +import ua.acclorite.book_story.presentation.ui.Theme +import ua.acclorite.book_story.presentation.ui.isDark /** * Theme switcher. @@ -29,8 +30,7 @@ import ua.acclorite.book_story.util.Constants fun AppearanceSettingsThemeSwitcher( mainViewModel: MainViewModel ) { - val theme = mainViewModel.theme.collectAsState().value!! - val darkTheme = mainViewModel.darkTheme.collectAsState().value!! + val state by mainViewModel.state.collectAsState() val themes = remember { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Constants.THEMES @@ -56,8 +56,8 @@ fun AppearanceSettingsThemeSwitcher( AppearanceSettingsThemeSwitcherItem( theme = themeEntry, - darkTheme = darkTheme.isDark(), - selected = theme == themeEntry.first + darkTheme = state.darkTheme!!.isDark(), + selected = state.theme == themeEntry.first ) { mainViewModel.onEvent(MainEvent.OnChangeTheme(themeEntry.first.toString())) } 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 75df2813..686ce1ab 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 @@ -31,9 +31,9 @@ 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 +import ua.acclorite.book_story.domain.util.UIText +import ua.acclorite.book_story.presentation.ui.Theme +import ua.acclorite.book_story.presentation.ui.colorScheme /** * Theme switcher item. 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 24e1e168..743cc998 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 @@ -17,19 +17,20 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue 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.domain.model.ChipItem +import ua.acclorite.book_story.domain.util.Constants import ua.acclorite.book_story.presentation.components.GoBackButton import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainViewModel import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle -import ua.acclorite.book_story.ui.elevation -import ua.acclorite.book_story.util.Constants +import ua.acclorite.book_story.presentation.ui.elevation @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -40,11 +41,11 @@ fun GeneralSettings( val listState = rememberLazyListState() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( canScroll = { - listState.canScrollForward + listState.canScrollForward || listState.canScrollBackward } ) - val language = mainViewModel.language.collectAsState().value!! + val state by mainViewModel.state.collectAsState() Scaffold( Modifier @@ -74,6 +75,9 @@ fun GeneralSettings( .padding(top = paddingValues.calculateTopPadding()), state = listState ) { + item { + Spacer(modifier = Modifier.height(8.dp)) + } item { ChipsWithTitle( title = stringResource(id = R.string.language_option), @@ -82,7 +86,7 @@ fun GeneralSettings( it.first, it.second, MaterialTheme.typography.labelLarge, - it.first == language + it.first == state.language ) }.sortedBy { it.title } ) { 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 13c36694..ee379b6e 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 @@ -17,6 +17,8 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults 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.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource @@ -24,6 +26,7 @@ import androidx.compose.ui.text.font.FontStyle 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.domain.util.Constants import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.components.GoBackButton import ua.acclorite.book_story.presentation.data.MainEvent @@ -32,8 +35,7 @@ import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.screens.settings.components.CheckboxWithTitle import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle import ua.acclorite.book_story.presentation.screens.settings.components.SliderWithTitle -import ua.acclorite.book_story.ui.elevation -import ua.acclorite.book_story.util.Constants +import ua.acclorite.book_story.presentation.ui.elevation @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -44,18 +46,16 @@ fun ReaderSettings( val listState = rememberLazyListState() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( canScroll = { - listState.canScrollForward + listState.canScrollForward || listState.canScrollBackward } ) - val fontFamily = Constants.FONTS.find { - it.id == mainViewModel.fontFamily.collectAsState().value!! - } ?: Constants.FONTS[0] - val fontStyle = mainViewModel.isItalic.collectAsState().value!! - val fontSize = mainViewModel.fontSize.collectAsState().value!! - val lineHeight = mainViewModel.lineHeight.collectAsState().value!! - val paragraphHeight = mainViewModel.paragraphHeight.collectAsState().value!! - val paragraphIndentation = mainViewModel.paragraphIndentation.collectAsState().value!! + val state by mainViewModel.state.collectAsState() + val fontFamily = remember(state.fontFamily) { + Constants.FONTS.find { + it.id == state.fontFamily + } ?: Constants.FONTS[0] + } Scaffold( Modifier @@ -86,7 +86,7 @@ fun ReaderSettings( state = listState ) { item { - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(16.dp)) } item { CategoryTitle( @@ -127,7 +127,7 @@ fun ReaderSettings( fontFamily = fontFamily.font, fontStyle = FontStyle.Normal ), - !fontStyle + !state.isItalic!! ), ChipItem( "italic", @@ -136,7 +136,7 @@ fun ReaderSettings( fontFamily = fontFamily.font, fontStyle = FontStyle.Italic ), - fontStyle + state.isItalic!! ), ), onClick = { @@ -153,7 +153,7 @@ fun ReaderSettings( } item { SliderWithTitle( - value = fontSize to "pt", + value = state.fontSize!! to "pt", fromValue = 10, toValue = 35, title = stringResource(id = R.string.font_size_option), @@ -178,7 +178,7 @@ fun ReaderSettings( } item { SliderWithTitle( - value = lineHeight to "pt", + value = state.lineHeight!! to "pt", fromValue = 1, toValue = 16, title = stringResource(id = R.string.line_height_option), @@ -191,7 +191,7 @@ fun ReaderSettings( } item { SliderWithTitle( - value = paragraphHeight to "pt", + value = state.paragraphHeight!! to "pt", fromValue = 0, toValue = 24, title = stringResource(id = R.string.paragraph_height_option), @@ -204,11 +204,11 @@ fun ReaderSettings( } item { CheckboxWithTitle( - selected = paragraphIndentation, + selected = state.paragraphIndentation!!, title = stringResource(id = R.string.paragraph_indentation_option) ) { mainViewModel.onEvent( - MainEvent.OnChangeParagraphIndentation(!paragraphIndentation) + MainEvent.OnChangeParagraphIndentation(!state.paragraphIndentation!!) ) } } diff --git a/app/src/main/java/ua/acclorite/book_story/ui/Color.kt b/app/src/main/java/ua/acclorite/book_story/presentation/ui/Color.kt similarity index 72% rename from app/src/main/java/ua/acclorite/book_story/ui/Color.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/ui/Color.kt index c676f833..a6624616 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/Color.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/ui/Color.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.ui +package ua.acclorite.book_story.presentation.ui import android.annotation.SuppressLint import androidx.compose.material3.ColorScheme @@ -6,13 +6,13 @@ import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext -import ua.acclorite.book_story.ui.theme.aquaTheme -import ua.acclorite.book_story.ui.theme.blueTheme -import ua.acclorite.book_story.ui.theme.greenTheme -import ua.acclorite.book_story.ui.theme.pinkTheme -import ua.acclorite.book_story.ui.theme.purpleTheme -import ua.acclorite.book_story.ui.theme.redTheme -import ua.acclorite.book_story.ui.theme.yellowTheme +import ua.acclorite.book_story.presentation.ui.theme.aquaTheme +import ua.acclorite.book_story.presentation.ui.theme.blueTheme +import ua.acclorite.book_story.presentation.ui.theme.greenTheme +import ua.acclorite.book_story.presentation.ui.theme.pinkTheme +import ua.acclorite.book_story.presentation.ui.theme.purpleTheme +import ua.acclorite.book_story.presentation.ui.theme.redTheme +import ua.acclorite.book_story.presentation.ui.theme.yellowTheme enum class Theme { DYNAMIC, @@ -29,16 +29,7 @@ enum class Theme { * Converting [String] into [Theme]. */ fun String.toTheme(): Theme { - return when (this) { - "DYNAMIC" -> Theme.DYNAMIC - "PURPLE" -> Theme.PURPLE - "GREEN" -> Theme.GREEN - "PINK" -> Theme.PINK - "YELLOW" -> Theme.YELLOW - "RED" -> Theme.RED - "AQUA" -> Theme.AQUA - else -> Theme.BLUE - } + return Theme.valueOf(this) } /** diff --git a/app/src/main/java/ua/acclorite/book_story/ui/Elevation.kt b/app/src/main/java/ua/acclorite/book_story/presentation/ui/Elevation.kt similarity index 70% rename from app/src/main/java/ua/acclorite/book_story/ui/Elevation.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/ui/Elevation.kt index c12a5505..beca6587 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/Elevation.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/ui/Elevation.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.ui +package ua.acclorite.book_story.presentation.ui import androidx.compose.ui.unit.dp diff --git a/app/src/main/java/ua/acclorite/book_story/ui/Theme.kt b/app/src/main/java/ua/acclorite/book_story/presentation/ui/Theme.kt similarity index 93% rename from app/src/main/java/ua/acclorite/book_story/ui/Theme.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/ui/Theme.kt index e3c2bc07..3f7075ff 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/Theme.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/ui/Theme.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.ui +package ua.acclorite.book_story.presentation.ui import android.app.Activity import android.os.Build @@ -22,11 +22,7 @@ enum class DarkTheme { } fun String.toDarkTheme(): DarkTheme { - return when (this) { - "OFF" -> DarkTheme.OFF - "ON" -> DarkTheme.ON - else -> DarkTheme.FOLLOW_SYSTEM - } + return DarkTheme.valueOf(this) } @Composable diff --git a/app/src/main/java/ua/acclorite/book_story/ui/Transitions.kt b/app/src/main/java/ua/acclorite/book_story/presentation/ui/Transitions.kt similarity index 96% rename from app/src/main/java/ua/acclorite/book_story/ui/Transitions.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/ui/Transitions.kt index 71b8279f..ec36796f 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/Transitions.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/ui/Transitions.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.ui +package ua.acclorite.book_story.presentation.ui import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween diff --git a/app/src/main/java/ua/acclorite/book_story/ui/Typography.kt b/app/src/main/java/ua/acclorite/book_story/presentation/ui/Typography.kt similarity index 97% rename from app/src/main/java/ua/acclorite/book_story/ui/Typography.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/ui/Typography.kt index 82258847..c8e919e8 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/Typography.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/ui/Typography.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.ui +package ua.acclorite.book_story.presentation.ui import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle diff --git a/app/src/main/java/ua/acclorite/book_story/ui/theme/AquaTheme.kt b/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/AquaTheme.kt similarity index 99% rename from app/src/main/java/ua/acclorite/book_story/ui/theme/AquaTheme.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/AquaTheme.kt index 782d3c9c..290a8fcc 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/theme/AquaTheme.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/AquaTheme.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.ui.theme +package ua.acclorite.book_story.presentation.ui.theme import androidx.compose.material3.ColorScheme import androidx.compose.material3.darkColorScheme diff --git a/app/src/main/java/ua/acclorite/book_story/ui/theme/BlueTheme.kt b/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/BlueTheme.kt similarity index 99% rename from app/src/main/java/ua/acclorite/book_story/ui/theme/BlueTheme.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/BlueTheme.kt index e3e09f53..f1966ac3 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/theme/BlueTheme.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/BlueTheme.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.ui.theme +package ua.acclorite.book_story.presentation.ui.theme import androidx.compose.material3.ColorScheme import androidx.compose.material3.darkColorScheme diff --git a/app/src/main/java/ua/acclorite/book_story/ui/theme/GreenTheme.kt b/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/GreenTheme.kt similarity index 99% rename from app/src/main/java/ua/acclorite/book_story/ui/theme/GreenTheme.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/GreenTheme.kt index 19cebb58..308bba5a 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/theme/GreenTheme.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/GreenTheme.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.ui.theme +package ua.acclorite.book_story.presentation.ui.theme import androidx.compose.material3.ColorScheme import androidx.compose.material3.darkColorScheme diff --git a/app/src/main/java/ua/acclorite/book_story/ui/theme/PinkTheme.kt b/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/PinkTheme.kt similarity index 99% rename from app/src/main/java/ua/acclorite/book_story/ui/theme/PinkTheme.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/PinkTheme.kt index 36a0f03a..fca9320f 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/theme/PinkTheme.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/PinkTheme.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.ui.theme +package ua.acclorite.book_story.presentation.ui.theme import androidx.compose.material3.ColorScheme import androidx.compose.material3.darkColorScheme diff --git a/app/src/main/java/ua/acclorite/book_story/ui/theme/PurpleTheme.kt b/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/PurpleTheme.kt similarity index 99% rename from app/src/main/java/ua/acclorite/book_story/ui/theme/PurpleTheme.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/PurpleTheme.kt index 1c31f974..0767a954 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/theme/PurpleTheme.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/PurpleTheme.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.ui.theme +package ua.acclorite.book_story.presentation.ui.theme import androidx.compose.material3.ColorScheme import androidx.compose.material3.darkColorScheme diff --git a/app/src/main/java/ua/acclorite/book_story/ui/theme/RedTheme.kt b/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/RedTheme.kt similarity index 99% rename from app/src/main/java/ua/acclorite/book_story/ui/theme/RedTheme.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/RedTheme.kt index bc98c65c..76bc92ae 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/theme/RedTheme.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/RedTheme.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.ui.theme +package ua.acclorite.book_story.presentation.ui.theme import androidx.compose.material3.ColorScheme import androidx.compose.material3.darkColorScheme diff --git a/app/src/main/java/ua/acclorite/book_story/ui/theme/YellowTheme.kt b/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/YellowTheme.kt similarity index 99% rename from app/src/main/java/ua/acclorite/book_story/ui/theme/YellowTheme.kt rename to app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/YellowTheme.kt index a601635d..03a8e281 100644 --- a/app/src/main/java/ua/acclorite/book_story/ui/theme/YellowTheme.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/YellowTheme.kt @@ -1,4 +1,4 @@ -package ua.acclorite.book_story.ui.theme +package ua.acclorite.book_story.presentation.ui.theme import androidx.compose.material3.ColorScheme import androidx.compose.material3.darkColorScheme diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 71eca021..d774d0bc 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -61,7 +61,6 @@ Хибний формат файлу. Помилка: %1$s - Цей файл зашифрований. Цей файл пустий або пошкоджений. Не знайдено жодного перекладача. Не знайдено жодного браузера. @@ -84,8 +83,8 @@ %1$s обрано Знайти %1$s… - Ви прочитали %1$s цієї книги. %1$s прочитано + Шлях до файлу: %1$s Відмінити @@ -221,6 +220,22 @@ %1$s букв, %2$s слів. + + Версія застосунку + Версія %1$s + Всі релізи + Перевірити на нову версію + Знайшли помилку? + Розкажіть мені! + Сторінка проєкту + Головна сторінка застосунку + Сподобався застосунок? + Перевірте мій профіль! + Хочете підтримати мою роботу? + Купіть мені каву ♥ + Слава Україні! 🇺🇦 + Героям Слава! 🐉 + За замовчуванням @@ -252,5 +267,6 @@ Видалити обрані книги Повернути Вибрано + Іконка застосунку \ 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 85fe7ae7..3a84c2cb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,13 +2,28 @@ 0.9.3 Book\'s Story + + https://github.com/Acclorite/book-story/releases/latest + + + https://github.com/Acclorite/book-story/issues + + + https://github.com/Acclorite/book-story + + + https://github.com/Acclorite + + + https://www.buymeacoffee.com/acclorite + Library History Browse Settings - About + About app Help @@ -62,7 +77,6 @@ Wrong file format. Error: %1$s - This file is encrypted. This file is empty or corrupted. No translator app found. No browser app found. @@ -84,8 +98,8 @@ %1$s selected Search %1$s… - You read %1$s of this book. %1$s read + File path: %1$s Cancel @@ -221,6 +235,22 @@ %1$s characters, %2$s words. + + App version + Version %1$s + All releases + Check for newer version + Found a bug? + Tell me! + Project\'s page + General app\'s page + Like the app? + Check my profile! + Want to support my work? + Buy me a coffee ♥ + Slava Ukraini! 🇺🇦 + Heroyam Slava! 🐉 + Default @@ -252,5 +282,6 @@ Delete selected books Revert Selected + App icon \ No newline at end of file