diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7ab78bb7..37cea978 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -15,7 +15,7 @@ android { minSdk = 26 targetSdk = 34 versionCode = 1 - versionName = "0.9.6" + versionName = "0.9.7" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -66,7 +66,7 @@ dependencies { implementation("androidx.activity:activity-compose:1.8.2") // Compose BOM libraries - implementation(platform("androidx.compose:compose-bom:2024.03.00")) + implementation(platform("androidx.compose:compose-bom:2024.04.00")) implementation("androidx.compose.foundation:foundation") implementation("androidx.compose.animation:animation") implementation("androidx.compose.animation:animation-core") @@ -90,7 +90,7 @@ dependencies { testImplementation("androidx.test.ext:truth:1.5.0") testImplementation("com.squareup.okhttp3:mockwebserver:latest.release") testImplementation("io.mockk:mockk:latest.release") - debugImplementation("androidx.compose.ui:ui-test-manifest:1.6.4") + debugImplementation("androidx.compose.ui:ui-test-manifest:1.6.5") // Instrumentation tests androidTestImplementation("com.google.dagger:hilt-android-testing:latest.release") @@ -109,7 +109,7 @@ dependencies { implementation("com.google.accompanist:accompanist-swiperefresh:0.24.2-alpha") // Dagger - Hilt - implementation("com.google.dagger:hilt-android:2.51") + implementation("com.google.dagger:hilt-android:2.51.1") ksp("com.google.dagger:hilt-android-compiler:2.51") implementation("com.google.dagger:hilt-compiler:latest.release") ksp("androidx.hilt:hilt-compiler:latest.release") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0f77188d..ecadbad2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -34,6 +34,7 @@ android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/Theme.Start.Splash" + android:requestLegacyExternalStorage="true" android:enableOnBackInvokedCallback="true" tools:targetApi="tiramisu" tools:ignore="DataExtractionRules"> 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 a3c8e867..a72639ec 100644 --- a/app/src/main/java/ua/acclorite/book_story/Activity.kt +++ b/app/src/main/java/ua/acclorite/book_story/Activity.kt @@ -9,19 +9,25 @@ 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.WindowInsets import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp @@ -49,9 +55,10 @@ 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.presentation.ui.BooksHistoryResurrectionTheme +import ua.acclorite.book_story.presentation.ui.BookStoryTheme import ua.acclorite.book_story.presentation.ui.Transitions import ua.acclorite.book_story.presentation.ui.isDark +import ua.acclorite.book_story.presentation.ui.isPureDark import java.lang.reflect.Field @@ -109,10 +116,25 @@ class Activity : AppCompatActivity() { } } + val density = LocalDensity.current + val imeInsets = WindowInsets.ime + val focusManager = LocalFocusManager.current + val isKeyboardVisible by remember { + derivedStateOf { + imeInsets.getBottom(density) > 0 + } + } + LaunchedEffect(isKeyboardVisible) { + if (!isKeyboardVisible) { + focusManager.clearFocus() + } + } + if (isLoaded) { - BooksHistoryResurrectionTheme( + BookStoryTheme( theme = state.theme!!, isDark = state.darkTheme!!.isDark(), + isPureDark = state.pureDark!!.isPureDark(this), themeContrast = state.themeContrast!! ) { NavigationHost(startScreen = Screen.LIBRARY) { diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/data_store/DataStore.kt b/app/src/main/java/ua/acclorite/book_story/data/local/data_store/DataStore.kt index 62342537..83153c59 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/local/data_store/DataStore.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/local/data_store/DataStore.kt @@ -5,5 +5,7 @@ import kotlinx.coroutines.flow.Flow interface DataStore { suspend fun getData(key: Preferences.Key, defaultValue: T): Flow + suspend fun getNullableData(key: Preferences.Key): T? + suspend fun getAllData(): Set>? suspend fun putData(key: Preferences.Key, value: T) } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/data_store/DataStoreImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/local/data_store/DataStoreImpl.kt index ca8f3df2..53599a1f 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/local/data_store/DataStoreImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/local/data_store/DataStoreImpl.kt @@ -8,6 +8,7 @@ import androidx.datastore.preferences.core.emptyPreferences import androidx.datastore.preferences.preferencesDataStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import java.io.IOException import javax.inject.Inject @@ -29,6 +30,26 @@ class DataStoreImpl @Inject constructor(context: Application) : DataStore { result } + override suspend fun getNullableData(key: Preferences.Key): T? = + dataStore.data.catch { exception -> + if (exception is IOException) { + emit(emptyPreferences()) + } else { + throw exception + } + }.map { preferences -> + val result = preferences[key] + result + }.firstOrNull() + + override suspend fun getAllData(): Set>? { + val keys = dataStore.data + .map { + it.asMap().keys + } + return keys.firstOrNull() + } + override suspend fun putData(key: Preferences.Key, value: T) { dataStore.edit { preferences -> preferences[key] = value 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 45baac80..774a89ea 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 @@ -6,9 +6,14 @@ import android.net.Uri import android.os.Environment import android.util.Log import androidx.datastore.preferences.core.Preferences +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import ua.acclorite.book_story.R import ua.acclorite.book_story.data.local.data_store.DataStore @@ -26,7 +31,7 @@ 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.presentation.data.MainState import java.io.File import java.io.FileOutputStream import java.util.UUID @@ -380,6 +385,32 @@ class BookRepositoryImpl @Inject constructor( dataStore.putData(key, value) } + override suspend fun getAllSettings(scope: CoroutineScope): MainState { + val result = CompletableDeferred() + + scope.launch { + val keys = dataStore.getAllData() + val data = mutableMapOf() + + val jobs = keys?.map { key -> + async { + val nullableData = dataStore.getNullableData(key) + + if (nullableData == null) { + data.remove(key.name) + } else { + data[key.name] = nullableData + } + } + } + jobs?.awaitAll() + + result.complete(MainState.initialize(data)) + } + + return result.await() + } + override suspend fun getFilesFromDevice(query: String): Flow>> { fun getAllFilesInDirectory(directory: File): List { val filesList = mutableListOf() @@ -437,7 +468,7 @@ class BookRepositoryImpl @Inject constructor( return@filter false } - val isQuery = if (query.isEmpty()) true else file.name.lowercase() + val isQuery = if (query.isEmpty()) true else file.name.trim().lowercase() .contains(query.trim().lowercase()) if (!isQuery) { @@ -451,18 +482,9 @@ class BookRepositoryImpl @Inject constructor( ) } - filteredFiles = if (query.trim().isNotEmpty()) { - filteredFiles.sortedByDescending { - calculateFamiliarity( - query, - it.name.lowercase().trim() - ) - }.toMutableList() - } else { - filteredFiles.sortedByDescending { - it.lastModified() - }.toMutableList() - } + filteredFiles = filteredFiles.sortedByDescending { + it.lastModified() + }.toMutableList() emit( Resource.Success( 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 f37991d8..7da83b8d 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 @@ -1,6 +1,7 @@ package ua.acclorite.book_story.domain.repository import androidx.datastore.preferences.core.Preferences +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.History @@ -8,6 +9,7 @@ import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.model.StringWithId import ua.acclorite.book_story.domain.util.CoverImage import ua.acclorite.book_story.domain.util.Resource +import ua.acclorite.book_story.presentation.data.MainState import java.io.File interface BookRepository { @@ -55,6 +57,8 @@ interface BookRepository { value: T ) + suspend fun getAllSettings(scope: CoroutineScope): MainState + suspend fun getFilesFromDevice(query: String = ""): Flow>> suspend fun getBooksFromFiles(files: List): List diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetAllSettings.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetAllSettings.kt new file mode 100644 index 00000000..ab8a124e --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetAllSettings.kt @@ -0,0 +1,15 @@ +package ua.acclorite.book_story.domain.use_case + +import kotlinx.coroutines.CoroutineScope +import ua.acclorite.book_story.domain.repository.BookRepository +import ua.acclorite.book_story.presentation.data.MainState +import javax.inject.Inject + +class GetAllSettings @Inject constructor( + private val repository: BookRepository +) { + + suspend fun execute(scope: CoroutineScope): MainState { + return repository.getAllSettings(scope) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/util/DataStoreConstants.kt b/app/src/main/java/ua/acclorite/book_story/domain/util/DataStoreConstants.kt index d119c3c3..598c24a1 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/util/DataStoreConstants.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/util/DataStoreConstants.kt @@ -9,6 +9,7 @@ object DataStoreConstants { val LANGUAGE = stringPreferencesKey("language") val THEME = stringPreferencesKey("theme") val DARK_THEME = stringPreferencesKey("dark_theme") + val PURE_DARK = stringPreferencesKey("pure_dark") val THEME_CONTRAST = stringPreferencesKey("theme_contrast") val SHOW_START_SCREEN = booleanPreferencesKey("guide") val BACKGROUND_COLOR = longPreferencesKey("background_color") 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 61109d8a..0c5d8309 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 @@ -164,7 +164,3 @@ fun AnimatedTopAppBar( - - - - diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CollapsibleScrollBehaivor.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CollapsibleScrollBehaivor.kt new file mode 100644 index 00000000..e4d3a42f --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/CollapsibleScrollBehaivor.kt @@ -0,0 +1,55 @@ +package ua.acclorite.book_story.presentation.components + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import kotlinx.coroutines.flow.collectLatest + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState( + +): Pair { + val listState = rememberLazyListState() + + var canScroll by remember { mutableStateOf(false) } + val scrollBehavior = exitUntilCollapsedScrollBehavior( + canScroll = { + listState.canScrollForward || canScroll + } + ) + + LaunchedEffect(scrollBehavior.state) { + snapshotFlow { + scrollBehavior.state.collapsedFraction + }.collectLatest { fraction -> + canScroll = fraction > 0.01f + } + } + + return scrollBehavior to listState +} + + + + + + + + + + + + + + + diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomIconButton.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomIconButton.kt index 44a01dcf..0ebf50c0 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomIconButton.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomIconButton.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource @@ -31,6 +32,9 @@ fun CustomIconButton( CustomTooltip(text = stringResource(id = contentDescription)) { IconButton( enabled = enabled && !isClicked, + modifier = Modifier.focusProperties { + canFocus = false + }, onClick = { if (disableOnClick) { isClicked = true diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomTooltip.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomTooltip.kt index 8d5eb394..f9a55814 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomTooltip.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/CustomTooltip.kt @@ -7,13 +7,15 @@ import androidx.compose.material3.TooltipBox import androidx.compose.material3.TooltipDefaults import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @OptIn(ExperimentalMaterial3Api::class) @Composable -fun CustomTooltip(text: String, content: @Composable () -> Unit) { +fun CustomTooltip(text: String, padding: Dp = 14.dp, content: @Composable () -> Unit) { TooltipBox( - positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(12.dp), + positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(padding), + focusable = false, tooltip = { PlainTooltip { Text(text = text) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationBar.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationBar.kt index e3cdcf32..c03f898c 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 @@ -34,32 +34,38 @@ fun BottomNavigationBar( } NavigationBar { + BottomNavigationBarItem( item = NavigationItem( stringResource(id = R.string.library_screen), selectedIcon = painterResource(id = R.drawable.library_screen_filled), unselectedIcon = painterResource(id = R.drawable.library_screen_outlined) ), + tooltipText = stringResource(id = R.string.library_content_desc), isSelected = currentScreen == Screen.LIBRARY ) { navigator.navigate(Screen.LIBRARY, false) } + BottomNavigationBarItem( item = NavigationItem( stringResource(id = R.string.history_screen), selectedIcon = painterResource(id = R.drawable.history_screen_filled), unselectedIcon = painterResource(id = R.drawable.history_screen_outlined) ), + tooltipText = stringResource(id = R.string.history_content_desc), isSelected = currentScreen == Screen.HISTORY ) { navigator.navigate(Screen.HISTORY, false) } + BottomNavigationBarItem( item = NavigationItem( stringResource(id = R.string.browse_screen), selectedIcon = painterResource(id = R.drawable.browse_screen_filled), unselectedIcon = painterResource(id = R.drawable.browse_screen_outlined) ), + tooltipText = stringResource(id = R.string.browse_content_desc), isSelected = currentScreen == Screen.BROWSE ) { navigator.navigate(Screen.BROWSE, false) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationItem.kt index a87c9831..f00b92c6 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/bottom_navigation_bar/BottomNavigationItem.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import ua.acclorite.book_story.domain.model.NavigationItem +import ua.acclorite.book_story.presentation.components.CustomTooltip /** * Bottom Navigation Bar Item, uses default [NavigationBarItem]. @@ -19,6 +20,7 @@ import ua.acclorite.book_story.domain.model.NavigationItem fun RowScope.BottomNavigationBarItem( modifier: Modifier = Modifier, item: NavigationItem, + tooltipText: String, isSelected: Boolean, onClick: () -> Unit ) { @@ -27,22 +29,33 @@ fun RowScope.BottomNavigationBarItem( else item.unselectedIcon } + NavigationBarItem( label = { - Text( - text = item.title, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + CustomTooltip( + text = tooltipText, + padding = 64.dp + ) { + Text( + text = item.title, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } }, selected = isSelected, onClick = { onClick() }, icon = { - Icon( - painter = icon, - contentDescription = null, - modifier = Modifier.size(24.dp) - ) + CustomTooltip( + text = tooltipText, + padding = 32.dp + ) { + Icon( + painter = icon, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } }, modifier = modifier ) 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 075ffbae..36abcf96 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 @@ -76,6 +76,7 @@ fun BoxScope.CustomNavigationRail( selectedIcon = painterResource(id = R.drawable.library_screen_filled), unselectedIcon = painterResource(id = R.drawable.library_screen_outlined) ), + tooltipText = stringResource(id = R.string.library_content_desc), isSelected = currentScreen == Screen.LIBRARY ) { navigator.navigate(Screen.LIBRARY, false) @@ -86,6 +87,7 @@ fun BoxScope.CustomNavigationRail( selectedIcon = painterResource(id = R.drawable.history_screen_filled), unselectedIcon = painterResource(id = R.drawable.history_screen_outlined) ), + tooltipText = stringResource(id = R.string.history_content_desc), isSelected = currentScreen == Screen.HISTORY ) { navigator.navigate(Screen.HISTORY, false) @@ -96,6 +98,7 @@ fun BoxScope.CustomNavigationRail( selectedIcon = painterResource(id = R.drawable.browse_screen_filled), unselectedIcon = painterResource(id = R.drawable.browse_screen_outlined) ), + tooltipText = stringResource(id = R.string.browse_content_desc), isSelected = currentScreen == Screen.BROWSE ) { navigator.navigate(Screen.BROWSE, false) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRailItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRailItem.kt index 0083ddd3..8d2bae4e 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRailItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/custom_navigation_rail/CustomNavigationRailItem.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import ua.acclorite.book_story.domain.model.NavigationItem +import ua.acclorite.book_story.presentation.components.CustomTooltip /** * Custom Navigation Rail item. @@ -18,6 +19,7 @@ import ua.acclorite.book_story.domain.model.NavigationItem fun CustomNavigationRailItem( modifier: Modifier = Modifier, item: NavigationItem, + tooltipText: String, isSelected: Boolean, onClick: () -> Unit ) { @@ -28,20 +30,30 @@ fun CustomNavigationRailItem( NavigationRailItem( label = { - Text( - text = item.title, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + CustomTooltip( + text = tooltipText, + padding = 48.dp + ) { + Text( + text = item.title, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } }, selected = isSelected, onClick = { onClick() }, icon = { - Icon( - painter = icon, - contentDescription = null, - modifier = Modifier.size(24.dp) - ) + CustomTooltip( + text = tooltipText, + padding = 16.dp + ) { + Icon( + painter = icon, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } }, modifier = modifier ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/data/ArithmeticOperations.kt b/app/src/main/java/ua/acclorite/book_story/presentation/data/ArithmeticOperations.kt index 03c0b79c..1759bdd2 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/data/ArithmeticOperations.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/data/ArithmeticOperations.kt @@ -10,9 +10,3 @@ fun String.removeTrailingZero(): String { fun Double.removeDigits(digits: Int) = "%.${digits}f".format(this).replace(",", ".") -fun calculateFamiliarity(string: String, target: String): Int { - val targetCounts = target.lowercase().trim().groupingBy { it }.eachCount() - val familiarity = string.lowercase().trim().sumOf { targetCounts.getOrDefault(it, 0) } - return familiarity -} - 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 863c2a7b..5c0ea80c 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 @@ -4,6 +4,7 @@ sealed class MainEvent { data class OnChangeLanguage(val lang: String) : MainEvent() data class OnChangeTheme(val theme: String) : MainEvent() data class OnChangeDarkTheme(val darkTheme: String) : MainEvent() + data class OnChangePureDark(val pureDark: String) : MainEvent() data class OnChangeThemeContrast(val themeContrast: String) : MainEvent() data class OnChangeFontFamily(val fontFamily: String) : MainEvent() data class OnChangeFontStyle(val fontStyle: Boolean) : MainEvent() 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 index d0e77fff..c0e525b9 100644 --- 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 @@ -1,11 +1,21 @@ package ua.acclorite.book_story.presentation.data +import android.os.Build import android.os.Parcel import android.os.Parcelable import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color +import ua.acclorite.book_story.domain.util.Constants +import ua.acclorite.book_story.domain.util.DataStoreConstants import ua.acclorite.book_story.presentation.ui.DarkTheme +import ua.acclorite.book_story.presentation.ui.PureDark import ua.acclorite.book_story.presentation.ui.Theme import ua.acclorite.book_story.presentation.ui.ThemeContrast +import ua.acclorite.book_story.presentation.ui.toDarkTheme +import ua.acclorite.book_story.presentation.ui.toPureDark +import ua.acclorite.book_story.presentation.ui.toTheme +import ua.acclorite.book_story.presentation.ui.toThemeContrast +import java.util.Locale /** * Main State. All app's settings are here. Wrapped in SavedStateHandle, so it won't reset. @@ -15,6 +25,7 @@ data class MainState( val language: String? = null, val theme: Theme? = null, val darkTheme: DarkTheme? = null, + val pureDark: PureDark? = null, val themeContrast: ThemeContrast? = null, val fontFamily: String? = null, val isItalic: Boolean? = null, @@ -34,6 +45,8 @@ data class MainState( // String darkTheme = DarkTheme.valueOf(parcel.readString() ?: DarkTheme.FOLLOW_SYSTEM.name), // String + pureDark = PureDark.valueOf(parcel.readString() ?: PureDark.OFF.name), + // String themeContrast = ThemeContrast.valueOf(parcel.readString() ?: ThemeContrast.STANDARD.name), // String fontFamily = parcel.readString(), @@ -64,6 +77,8 @@ data class MainState( parcel.writeString(theme?.name) // Dark Theme parcel.writeString(darkTheme?.name) + // Pure Dark + parcel.writeString(pureDark?.name) // Contrast Level parcel.writeString(themeContrast?.name) // Font Family @@ -106,5 +121,79 @@ data class MainState( override fun newArray(size: Int): Array { return arrayOfNulls(size) } + + /** + * Initializes [MainState] by given [Map]. + */ + fun initialize(data: Map): MainState { + DataStoreConstants.apply { + + val language: String = data[LANGUAGE.name] as? String ?: if ( + Constants.LANGUAGES.any { Locale.getDefault().language.take(2) == it.first } + ) { + Locale.getDefault().language.take(2) + } else { + "en" + } + + val theme: String = data[THEME.name] as? String ?: if ( + Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + ) Theme.DYNAMIC.name else Theme.BLUE.name + + val darkTheme: String = data[DARK_THEME.name] as? String + ?: DarkTheme.FOLLOW_SYSTEM.name + + val pureDark: String = data[PURE_DARK.name] as? String + ?: PureDark.OFF.name + + val themeContrast: String = data[THEME_CONTRAST.name] as? String + ?: ThemeContrast.STANDARD.name + + val showStartScreen: Boolean = data[SHOW_START_SCREEN.name] as? Boolean + ?: true + + val backgroundColor: Long = data[BACKGROUND_COLOR.name] as? Long + ?: Color.DarkGray.value.toLong() + + val fontColor: Long = data[FONT_COLOR.name] as? Long + ?: Color.LightGray.value.toLong() + + val fontFamily: String = data[FONT.name] as? String + ?: Constants.FONTS[0].id + + val isItalic: Boolean = data[IS_ITALIC.name] as? Boolean + ?: false + + val fontSize: Int = data[FONT_SIZE.name] as? Int + ?: 16 + + val lineHeight: Int = data[LINE_HEIGHT.name] as? Int + ?: 4 + + val paragraphHeight: Int = data[PARAGRAPH_HEIGHT.name] as? Int + ?: 8 + + val paragraphIndentation: Boolean = data[PARAGRAPH_INDENTATION.name] as? Boolean + ?: false + + + return MainState( + language = language, + theme = theme.toTheme(), + darkTheme = darkTheme.toDarkTheme(), + pureDark = pureDark.toPureDark(), + themeContrast = themeContrast.toThemeContrast(), + showStartScreen = showStartScreen, + backgroundColor = backgroundColor, + fontColor = fontColor, + fontFamily = fontFamily, + isItalic = isItalic, + fontSize = fontSize, + lineHeight = lineHeight, + paragraphHeight = paragraphHeight, + paragraphIndentation = paragraphIndentation + ) + } + } } } \ 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 6dec42d2..c09b3abb 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt @@ -1,7 +1,5 @@ package ua.acclorite.book_story.presentation.data -import android.os.Build -import androidx.compose.ui.graphics.Color import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -9,23 +7,21 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update 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.GetAllSettings 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.presentation.ui.DarkTheme -import ua.acclorite.book_story.presentation.ui.Theme -import ua.acclorite.book_story.presentation.ui.ThemeContrast import ua.acclorite.book_story.presentation.ui.toDarkTheme +import ua.acclorite.book_story.presentation.ui.toPureDark import ua.acclorite.book_story.presentation.ui.toTheme import ua.acclorite.book_story.presentation.ui.toThemeContrast -import java.util.Locale import javax.inject.Inject /** @@ -35,14 +31,17 @@ import javax.inject.Inject class MainViewModel @Inject constructor( private val stateHandle: SavedStateHandle, - private val getDatastore: GetDatastore, private val setDatastore: SetDatastore, private val changeLanguage: ChangeLanguage, + private val getAllSettings: GetAllSettings ) : ViewModel() { private val _isReady = MutableStateFlow(false) val isReady = _isReady.asStateFlow() + private val isSettingsReady = MutableStateFlow(false) + private val isViewModelReady = MutableStateFlow(false) + private val _state: MutableStateFlow = MutableStateFlow( stateHandle[Constants.MAIN_STATE] ?: MainState() ) @@ -53,7 +52,7 @@ class MainViewModel @Inject constructor( is MainEvent.OnChangeLanguage -> { viewModelScope.launch(Dispatchers.Main) { changeLanguage.execute(event.lang) - _state.updateWithSavedHandle { + updateStateWithSavedHandle { it.copy( language = event.lang ) @@ -64,7 +63,7 @@ class MainViewModel @Inject constructor( is MainEvent.OnChangeDarkTheme -> { viewModelScope.launch(Dispatchers.IO) { setDatastore.execute(DataStoreConstants.DARK_THEME, event.darkTheme) - _state.updateWithSavedHandle { + updateStateWithSavedHandle { it.copy( darkTheme = event.darkTheme.toDarkTheme() ) @@ -72,10 +71,21 @@ class MainViewModel @Inject constructor( } } + is MainEvent.OnChangePureDark -> { + viewModelScope.launch(Dispatchers.IO) { + setDatastore.execute(DataStoreConstants.PURE_DARK, event.pureDark) + updateStateWithSavedHandle { + it.copy( + pureDark = event.pureDark.toPureDark() + ) + } + } + } + is MainEvent.OnChangeThemeContrast -> { viewModelScope.launch(Dispatchers.IO) { setDatastore.execute(DataStoreConstants.THEME_CONTRAST, event.themeContrast) - _state.updateWithSavedHandle { + updateStateWithSavedHandle { it.copy( themeContrast = event.themeContrast.toThemeContrast() ) @@ -86,7 +96,7 @@ class MainViewModel @Inject constructor( is MainEvent.OnChangeTheme -> { viewModelScope.launch(Dispatchers.IO) { setDatastore.execute(DataStoreConstants.THEME, event.theme) - _state.updateWithSavedHandle { + updateStateWithSavedHandle { it.copy( theme = event.theme.toTheme() ) @@ -97,7 +107,7 @@ class MainViewModel @Inject constructor( is MainEvent.OnChangeFontFamily -> { viewModelScope.launch(Dispatchers.IO) { setDatastore.execute(DataStoreConstants.FONT, event.fontFamily) - _state.updateWithSavedHandle { + updateStateWithSavedHandle { it.copy( fontFamily = Constants.FONTS.find { font -> font.id == event.fontFamily }?.id ?: Constants.FONTS[0].id @@ -109,7 +119,7 @@ class MainViewModel @Inject constructor( is MainEvent.OnChangeFontStyle -> { viewModelScope.launch(Dispatchers.IO) { setDatastore.execute(DataStoreConstants.IS_ITALIC, event.fontStyle) - _state.updateWithSavedHandle { + updateStateWithSavedHandle { it.copy( isItalic = event.fontStyle ) @@ -120,7 +130,7 @@ class MainViewModel @Inject constructor( is MainEvent.OnChangeFontSize -> { viewModelScope.launch(Dispatchers.IO) { setDatastore.execute(DataStoreConstants.FONT_SIZE, event.fontSize) - _state.updateWithSavedHandle { + updateStateWithSavedHandle { it.copy( fontSize = event.fontSize ) @@ -131,7 +141,7 @@ class MainViewModel @Inject constructor( is MainEvent.OnChangeLineHeight -> { viewModelScope.launch(Dispatchers.IO) { setDatastore.execute(DataStoreConstants.LINE_HEIGHT, event.lineHeight) - _state.updateWithSavedHandle { + updateStateWithSavedHandle { it.copy( lineHeight = event.lineHeight ) @@ -142,7 +152,7 @@ class MainViewModel @Inject constructor( is MainEvent.OnChangeParagraphHeight -> { viewModelScope.launch(Dispatchers.IO) { setDatastore.execute(DataStoreConstants.PARAGRAPH_HEIGHT, event.paragraphHeight) - _state.updateWithSavedHandle { + updateStateWithSavedHandle { it.copy( paragraphHeight = event.paragraphHeight ) @@ -153,7 +163,7 @@ class MainViewModel @Inject constructor( is MainEvent.OnChangeParagraphIndentation -> { viewModelScope.launch(Dispatchers.IO) { setDatastore.execute(DataStoreConstants.PARAGRAPH_INDENTATION, event.bool) - _state.updateWithSavedHandle { + updateStateWithSavedHandle { it.copy( paragraphIndentation = event.bool ) @@ -164,7 +174,7 @@ class MainViewModel @Inject constructor( is MainEvent.OnChangeBackgroundColor -> { viewModelScope.launch(Dispatchers.IO) { setDatastore.execute(DataStoreConstants.BACKGROUND_COLOR, event.color) - _state.updateWithSavedHandle { + updateStateWithSavedHandle { it.copy( backgroundColor = event.color ) @@ -175,7 +185,7 @@ class MainViewModel @Inject constructor( is MainEvent.OnChangeFontColor -> { viewModelScope.launch(Dispatchers.IO) { setDatastore.execute(DataStoreConstants.FONT_COLOR, event.color) - _state.updateWithSavedHandle { + updateStateWithSavedHandle { it.copy( fontColor = event.color ) @@ -186,7 +196,7 @@ class MainViewModel @Inject constructor( is MainEvent.OnChangeShowStartScreen -> { viewModelScope.launch(Dispatchers.IO) { setDatastore.execute(DataStoreConstants.SHOW_START_SCREEN, event.bool) - _state.updateWithSavedHandle { + updateStateWithSavedHandle { it.copy( showStartScreen = event.bool ) @@ -199,204 +209,54 @@ class MainViewModel @Inject constructor( fun init( libraryViewModel: LibraryViewModel, ) { - val isViewModelsReady = combine( - libraryViewModel.isReady, - ) { values -> - values.all { it } + viewModelScope.launch(Dispatchers.Main) { + val settings = getAllSettings.execute(viewModelScope) + + // All additional execution + changeLanguage.execute(settings.language!!) + + updateStateWithSavedHandle { + settings + } + isSettingsReady.update { true } } - val isDataReady = combine( - _state - ) { - val value = it.first() - - return@combine value.language != null && - value.theme != null && - value.darkTheme != null && - value.themeContrast != 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( - isViewModelsReady, - isDataReady - ) { values -> - values.all { it } - } - - viewModelScope.launch { - isReady.collect { bool -> - if (bool) { - _isReady.update { - true - } - return@collect + viewModelScope.launch(Dispatchers.IO) { + libraryViewModel.isReady.collectLatest { ready -> + isViewModelReady.update { + ready } } } - // Language - viewModelScope.launch(Dispatchers.Main) { - getDatastore - .execute( - DataStoreConstants.LANGUAGE, - if (Constants.LANGUAGES.any { Locale.getDefault().language.take(2) == it.first }) { - Locale.getDefault().language.take(2) - } else { - "en" + val isReady = combine( + isViewModelReady, + isSettingsReady + ) { values -> + values.all { it } + } + + viewModelScope.launch(Dispatchers.IO) { + isReady.first { bool -> + if (bool) { + _isReady.update { + true } - ) - .first { - onEvent(MainEvent.OnChangeLanguage(it)) - it.isNotBlank() } + bool + } } - - // Theme - viewModelScope.launch(Dispatchers.IO) { - getDatastore - .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() - } - } - - // Dark Theme - viewModelScope.launch(Dispatchers.IO) { - getDatastore - .execute(DataStoreConstants.DARK_THEME, DarkTheme.FOLLOW_SYSTEM.name) - .first { - onEvent(MainEvent.OnChangeDarkTheme(it)) - it.isNotBlank() - } - } - - // Theme Contrast - viewModelScope.launch(Dispatchers.IO) { - getDatastore - .execute(DataStoreConstants.THEME_CONTRAST, ThemeContrast.STANDARD.name) - .first { - onEvent(MainEvent.OnChangeThemeContrast(it)) - it.isNotBlank() - } - } - - // Show Start Screen - viewModelScope.launch(Dispatchers.IO) { - getDatastore - .execute(DataStoreConstants.SHOW_START_SCREEN, true) - .first { - onEvent(MainEvent.OnChangeShowStartScreen(it)) - true - } - } - - // Background Color - viewModelScope.launch(Dispatchers.IO) { - getDatastore - .execute(DataStoreConstants.BACKGROUND_COLOR, Color.DarkGray.value.toLong()) - .first { - onEvent(MainEvent.OnChangeBackgroundColor(it)) - true - } - } - - // Font Color - viewModelScope.launch(Dispatchers.IO) { - getDatastore - .execute(DataStoreConstants.FONT_COLOR, Color.LightGray.value.toLong()) - .first { - onEvent(MainEvent.OnChangeFontColor(it)) - true - } - } - - // Font Family - viewModelScope.launch(Dispatchers.IO) { - getDatastore - .execute(DataStoreConstants.FONT, Constants.FONTS[0].id) - .first { - onEvent(MainEvent.OnChangeFontFamily(it)) - true - } - } - - // Font Style - viewModelScope.launch(Dispatchers.IO) { - getDatastore - .execute(DataStoreConstants.IS_ITALIC, false) - .first { - onEvent(MainEvent.OnChangeFontStyle(it)) - true - } - } - - // Font Size - viewModelScope.launch(Dispatchers.IO) { - getDatastore - .execute(DataStoreConstants.FONT_SIZE, 16) - .first { - onEvent(MainEvent.OnChangeFontSize(it)) - it > 0 - } - } - - // Line Height - viewModelScope.launch(Dispatchers.IO) { - getDatastore - .execute(DataStoreConstants.LINE_HEIGHT, 4) - .first { - onEvent(MainEvent.OnChangeLineHeight(it)) - true - } - } - - // Paragraph Height - viewModelScope.launch(Dispatchers.IO) { - getDatastore - .execute(DataStoreConstants.PARAGRAPH_HEIGHT, 8) - .first { - onEvent(MainEvent.OnChangeParagraphHeight(it)) - true - } - } - - // Paragraph Indentation - viewModelScope.launch(Dispatchers.IO) { - getDatastore - .execute(DataStoreConstants.PARAGRAPH_INDENTATION, false) - .first { - onEvent(MainEvent.OnChangeParagraphIndentation(it)) - true - } - } - } /** - * Updates [MutableStateFlow] along with [SavedStateHandle]. + * Updates [MainState] along with [SavedStateHandle]. */ - private fun MutableStateFlow.updateWithSavedHandle( - const: String = Constants.MAIN_STATE, - function: (T) -> T + private fun updateStateWithSavedHandle( + function: (MainState) -> MainState ) { - val nextValue = function(value) - update { - stateHandle[const] = nextValue - nextValue + _state.update { + stateHandle[Constants.MAIN_STATE] = function(it) + function(it) } } } \ No newline at end of file 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 index 80df794e..34c6f759 100644 --- 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 @@ -12,7 +12,6 @@ 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.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -32,6 +31,7 @@ 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.components.collapsibleScrollBehaviorWithLazyListState 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 @@ -44,17 +44,12 @@ fun AboutScreen( navigator: Navigator ) { val context = LocalContext.current - val listState = rememberLazyListState() - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( - canScroll = { - listState.canScrollForward || listState.canScrollBackward - } - ) + val scrollState = TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState() Scaffold( Modifier .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) + .nestedScroll(scrollState.first.nestedScrollConnection) .windowInsetsPadding(WindowInsets.navigationBars), containerColor = MaterialTheme.colorScheme.surface, topBar = { @@ -65,7 +60,7 @@ fun AboutScreen( navigationIcon = { GoBackButton(navigator = navigator) }, - scrollBehavior = scrollBehavior, + scrollBehavior = scrollState.first, colors = TopAppBarDefaults.largeTopAppBarColors( containerColor = MaterialTheme.colorScheme.surface, scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer @@ -77,7 +72,7 @@ fun AboutScreen( Modifier .fillMaxSize() .padding(top = paddingValues.calculateTopPadding()), - state = listState + state = scrollState.second ) { item { Spacer(modifier = Modifier.height(16.dp)) 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 204b9d61..16f986ba 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 @@ -93,7 +93,7 @@ fun BookInfoScreen( val listState = rememberLazyListState() val snackbarState = remember { SnackbarHostState() } val refreshState = rememberPullRefreshState( - refreshing = state.isRefreshing, + refreshing = state.isRefreshing || state.isLoadingUpdate, onRefresh = { viewModel.onEvent( BookInfoEvent.OnLoadUpdate( @@ -161,7 +161,9 @@ fun BookInfoScreen( isTopBarScrolled = null, content1NavigationIcon = { - GoBackButton(navigator = navigator, enabled = !state.isRefreshing) + GoBackButton(navigator = navigator, enabled = !state.isRefreshing) { + viewModel.onEvent(BookInfoEvent.OnCancelUpdate) + } }, content1Title = { DefaultTransition(visible = firstVisibleItemIndex > 0) { @@ -179,7 +181,7 @@ fun BookInfoScreen( icon = Icons.Default.Refresh, contentDescription = R.string.refresh_book_content_desc, disableOnClick = false, - enabled = !state.isRefreshing + enabled = !state.isRefreshing && !state.isLoadingUpdate ) { viewModel.onEvent( BookInfoEvent.OnLoadUpdate( @@ -319,7 +321,7 @@ fun BookInfoScreen( ) PullRefreshIndicator( - state.isRefreshing, + state.isRefreshing || state.isLoadingUpdate, refreshState, Modifier .align(Alignment.TopCenter) @@ -332,6 +334,7 @@ fun BookInfoScreen( BackHandler { if (!state.isRefreshing) { + viewModel.onEvent(BookInfoEvent.OnCancelUpdate) navigator.navigateBack() } } 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 2ab841f0..b7e8c29c 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 @@ -126,7 +126,7 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) { fontFamily = MaterialTheme.typography.headlineSmall.fontFamily ), onValueChange = { - if (it.length < 80) { + if (it.length < 100 || it.length < state.titleValue.length) { viewModel.onEvent(BookInfoEvent.OnTitleValueChange(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 485b0835..2b08d719 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 @@ -61,6 +61,8 @@ sealed class BookInfoEvent { val textUpdated: Boolean, ) : BookInfoEvent() + data object OnCancelUpdate : BookInfoEvent() + data class OnConfirmUpdate( val snackbarState: SnackbarHostState, val context: Context, 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 3dfba788..14e000e6 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 @@ -9,6 +9,7 @@ import ua.acclorite.book_story.domain.util.Constants data class BookInfoState( val book: Book = Constants.EMPTY_BOOK, + val isLoadingUpdate: Boolean = false, val isRefreshing: Boolean = false, val showConfirmUpdateDialog: Boolean = false, val updatedBook: Book? = null, 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 9a11edb2..525bb554 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 @@ -52,6 +52,7 @@ class BookInfoViewModel @Inject constructor( val state = _state.asStateFlow() private var job: Job? = null + private var job2: Job? = null fun onEvent(event: BookInfoEvent) { when (event) { @@ -290,14 +291,17 @@ class BookInfoViewModel @Inject constructor( } is BookInfoEvent.OnLoadUpdate -> { - viewModelScope.launch(Dispatchers.IO) { + onEvent(BookInfoEvent.OnCancelUpdate) + + job2 = viewModelScope.launch(Dispatchers.IO) { _state.update { it.copy( - isRefreshing = true, + isLoadingUpdate = true, editTitle = false ) } + yield() if (_state.value.book.file == null) { onEvent( BookInfoEvent.OnShowSnackbar( @@ -323,13 +327,15 @@ class BookInfoViewModel @Inject constructor( delay(500) _state.update { it.copy( - isRefreshing = false + isLoadingUpdate = false ) } return@launch } + yield() val nullableBook = getBookFromFile.execute(_state.value.book.file!!) + yield() if (nullableBook is NullableBook.Null) { onEvent( BookInfoEvent.OnShowSnackbar( @@ -352,11 +358,12 @@ class BookInfoViewModel @Inject constructor( delay(500) _state.update { it.copy( - isRefreshing = false + isLoadingUpdate = false ) } return@launch } + yield() val updatedBook = nullableBook.book?.first ?: return@launch val book = _state.value.book @@ -383,6 +390,7 @@ class BookInfoViewModel @Inject constructor( textUpdated = true } + yield() if (!authorUpdated && !descriptionUpdated && !textUpdated) { onEvent( BookInfoEvent.OnShowSnackbar( @@ -395,12 +403,13 @@ class BookInfoViewModel @Inject constructor( delay(500) _state.update { it.copy( - isRefreshing = false + isLoadingUpdate = false ) } return@launch } + yield() onEvent( BookInfoEvent.OnShowConfirmUpdateDialog( updatedBook = updatedBook, @@ -412,7 +421,7 @@ class BookInfoViewModel @Inject constructor( _state.update { it.copy( - isRefreshing = false + isLoadingUpdate = false ) } } @@ -585,8 +594,19 @@ class BookInfoViewModel @Inject constructor( } } + is BookInfoEvent.OnCancelUpdate -> { + _state.update { + job2?.cancel() + it.copy( + showConfirmUpdateDialog = false, + isLoadingUpdate = false + ) + } + } + is BookInfoEvent.OnNavigateToReaderScreen -> { viewModelScope.launch { + onEvent(BookInfoEvent.OnCancelUpdate) _state.value.book.id.let { insertHistory.execute( listOf( 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 62ca379d..f73a1223 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 @@ -6,7 +6,6 @@ import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween import androidx.compose.animation.fadeOut -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight @@ -71,7 +70,6 @@ import ua.acclorite.book_story.presentation.ui.Transitions @OptIn( ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class, - ExperimentalFoundationApi::class, ExperimentalMaterialApi::class ) @Composable @@ -249,7 +247,7 @@ fun BrowseScreen( ) { selectableFile -> BrowseFileItem( file = selectableFile, - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), hasSelectedFiles = state.selectableFiles.any { it.second }, onLongClick = { Toast.makeText( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/HelpScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/HelpScreen.kt index c8e40294..f330f4df 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/HelpScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/HelpScreen.kt @@ -4,11 +4,11 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.MaterialTheme @@ -18,34 +18,38 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import 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.components.collapsibleScrollBehaviorWithLazyListState import ua.acclorite.book_story.presentation.data.Navigator -import ua.acclorite.book_story.presentation.screens.help.components.HelpClickableNote +import ua.acclorite.book_story.presentation.screens.help.components.items.HelpAddBooksItem +import ua.acclorite.book_story.presentation.screens.help.components.items.HelpClickMeNoteItem +import ua.acclorite.book_story.presentation.screens.help.components.items.HelpCustomizeApp +import ua.acclorite.book_story.presentation.screens.help.components.items.HelpCustomizeReader +import ua.acclorite.book_story.presentation.screens.help.components.items.HelpEditBook +import ua.acclorite.book_story.presentation.screens.help.components.items.HelpFindBooksItem +import ua.acclorite.book_story.presentation.screens.help.components.items.HelpMoveOrDeleteBooks +import ua.acclorite.book_story.presentation.screens.help.components.items.HelpReadBook +import ua.acclorite.book_story.presentation.screens.help.components.items.HelpUpdateBook import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel -@OptIn(ExperimentalMaterial3Api::class) +@OptIn( + ExperimentalMaterial3Api::class +) @Composable fun HelpScreen( viewModel: HelpViewModel = hiltViewModel(), navigator: Navigator ) { - val context = LocalContext.current - val listState = rememberLazyListState() - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( - canScroll = { - listState.canScrollForward || listState.canScrollBackward - } - ) + val scrollState = TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState() Scaffold( Modifier .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) + .nestedScroll(scrollState.first.nestedScrollConnection) .windowInsetsPadding(WindowInsets.navigationBars), containerColor = MaterialTheme.colorScheme.surface, topBar = { @@ -56,7 +60,7 @@ fun HelpScreen( navigationIcon = { GoBackButton(navigator = navigator) }, - scrollBehavior = scrollBehavior, + scrollBehavior = scrollState.first, colors = TopAppBarDefaults.largeTopAppBarColors( containerColor = MaterialTheme.colorScheme.surface, scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer @@ -67,19 +71,88 @@ fun HelpScreen( LazyColumn( Modifier .fillMaxSize() - .padding(top = paddingValues.calculateTopPadding()) - .padding(horizontal = 18.dp), - state = listState + .padding(top = paddingValues.calculateTopPadding(), start = 18.dp, end = 18.dp) + .imePadding(), + state = scrollState.second ) { - item { Spacer(modifier = Modifier.height(16.dp)) } + item { + Spacer( + modifier = Modifier + .animateItem() + .height(16.dp) + ) + } - item { HelpClickableNote() } + item { + HelpClickMeNoteItem(viewModel = viewModel) + } - item { Spacer(modifier = Modifier.height(16.dp)) } + item { + Spacer( + modifier = Modifier + .animateItem() + .height(16.dp) + ) + } - /* TODO Help screen */ - item { Spacer(modifier = Modifier.height(48.dp)) } + item { + HelpFindBooksItem(viewModel = viewModel) + } + + item { + HelpAddBooksItem(navigator = navigator, viewModel = viewModel) + } + + item { + HelpCustomizeApp( + navigator = navigator, + viewModel = viewModel + ) + } + + item { + HelpMoveOrDeleteBooks( + navigator = navigator, + viewModel = viewModel + ) + } + + item { + HelpEditBook( + navigator = navigator, + viewModel = viewModel + ) + } + + item { + HelpReadBook( + navigator = navigator, + viewModel = viewModel + ) + } + + item { + HelpCustomizeReader( + navigator = navigator, + viewModel = viewModel + ) + } + + item { + HelpUpdateBook( + navigator = navigator, + viewModel = viewModel + ) + } + + item { + Spacer( + modifier = Modifier + .animateItem() + .height(48.dp) + ) + } } } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpAnnotation.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpAnnotation.kt new file mode 100644 index 00000000..91fbf97c --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpAnnotation.kt @@ -0,0 +1,25 @@ +package ua.acclorite.book_story.presentation.screens.help.components + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle + +@Composable +fun AnnotatedString.Builder.HelpAnnotation( + tag: String, + block: @Composable AnnotatedString.Builder.() -> Unit +) { + pushStringAnnotation(tag = tag, annotation = "") + withStyle( + style = SpanStyle( + color = MaterialTheme.colorScheme.tertiary, + fontWeight = FontWeight.Medium + ) + ) { + block() + } + pop() +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpClickableNote.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpClickableNote.kt index fe12d508..173d1a60 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpClickableNote.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpClickableNote.kt @@ -1,79 +1,69 @@ package ua.acclorite.book_story.presentation.screens.help.components -import android.widget.Toast import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.foundation.text.ClickableText import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Info import androidx.compose.material3.Icon 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.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.components.CustomTooltip +import ua.acclorite.book_story.presentation.ui.SlidingTransition -@OptIn(ExperimentalLayoutApi::class) +/** + * When clicked returns a Tag from [tags] (if present in [text]) + */ @Composable -fun HelpClickableNote() { - val context = LocalContext.current - - Column { - Icon( - imageVector = Icons.Outlined.Info, - contentDescription = stringResource(id = R.string.note_content_desc), - modifier = Modifier - .size(20.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.height(8.dp)) - FlowRow(verticalArrangement = Arrangement.spacedBy(1.dp)) { - Text( - stringResource(id = R.string.clickable_note_1) + " ", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.align(Alignment.CenterVertically) - ) - Text( - stringResource(id = R.string.clickable_note_2) + " ", - style = MaterialTheme.typography.bodySmall, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.tertiary, +fun LazyItemScope.HelpClickableNote( + text: AnnotatedString, + tags: List = emptyList(), + isNoteFullyShown: Boolean, + onIconClick: () -> Unit, + onTagClick: (String) -> Unit +) { + Column(Modifier.animateItem()) { + CustomTooltip(text = stringResource(R.string.note_content_desc)) { + Icon( + imageVector = Icons.Outlined.Info, modifier = Modifier - .align(Alignment.CenterVertically) - .clickable( - interactionSource = null, - indication = null, - onClick = { - Toast - .makeText( - context, - context.getString(R.string.clickable_note_action), - Toast.LENGTH_SHORT - ) - .show() - } - ) - ) - Text( - stringResource(id = R.string.clickable_note_3), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.align(Alignment.CenterVertically) + .size(20.dp) + .clickable(interactionSource = null, indication = null) { + onIconClick() + }, + contentDescription = stringResource(R.string.note_content_desc), + tint = MaterialTheme.colorScheme.onSurfaceVariant ) } - } + SlidingTransition(visible = isNoteFullyShown) { + Column { + Spacer(modifier = Modifier.height(8.dp)) + ClickableText( + text = text, + style = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) { offset -> + tags.forEach { tag -> + text.getStringAnnotations(tag = tag, start = offset, end = offset) + .firstOrNull()?.let { + onTagClick(tag) + } + } + } + } + } + } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpItem.kt new file mode 100644 index 00000000..16d5aaa5 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/HelpItem.kt @@ -0,0 +1,106 @@ +package ua.acclorite.book_story.presentation.screens.help.components + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.ArrowDropUp +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.ui.SlidingTransition + +@Composable +fun LazyItemScope.HelpItem( + title: String, + description: AnnotatedString, + customContent: @Composable ColumnScope.() -> Unit = {}, + tags: List, + shouldShowDescription: Boolean, + onTitleClick: () -> Unit, + onTagClick: (String) -> Unit +) { + val animatedArrowRotation by animateFloatAsState( + targetValue = if (shouldShowDescription) 0f else -180f, + animationSpec = tween(300), + label = stringResource(id = R.string.arrow_anim_content_desc) + ) + + + Column(Modifier.animateItem()) { + Spacer( + modifier = Modifier.height(8.dp) + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(interactionSource = null, indication = null) { + onTitleClick() + }, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.weight(1f) + ) + Spacer(modifier = Modifier.width(8.dp)) + Icon( + imageVector = Icons.Outlined.ArrowDropUp, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .size(24.dp) + .rotate(animatedArrowRotation), + contentDescription = stringResource(id = R.string.arrow_content_desc) + ) + } + + + SlidingTransition( + visible = shouldShowDescription + ) { + Column { + Spacer(modifier = Modifier.height(16.dp)) + ClickableText( + text = description, + style = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) { offset -> + tags.forEach { tag -> + description.getStringAnnotations(tag = tag, start = offset, end = offset) + .firstOrNull()?.let { + onTagClick(tag) + } + } + } + customContent() + } + } + + Spacer( + modifier = Modifier.height(8.dp) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpAddBooksItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpAddBooksItem.kt new file mode 100644 index 00000000..cd3e89ee --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpAddBooksItem.kt @@ -0,0 +1,58 @@ +package ua.acclorite.book_story.presentation.screens.help.components.items + +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.buildAnnotatedString +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.data.Navigator +import ua.acclorite.book_story.presentation.data.Screen +import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation +import ua.acclorite.book_story.presentation.screens.help.components.HelpItem +import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel + +@Composable +fun LazyItemScope.HelpAddBooksItem(navigator: Navigator, viewModel: HelpViewModel) { + val state by viewModel.state.collectAsState() + + HelpItem( + title = stringResource(id = R.string.help_title_how_to_add_books), + description = buildAnnotatedString { + append(stringResource(id = R.string.help_desc_how_to_add_books_1) + " ") + + HelpAnnotation(tag = "browse") { + append(stringResource(id = R.string.help_desc_how_to_add_books_2)) + } + append(". ") + + append(stringResource(id = R.string.help_desc_how_to_add_books_3) + " ") + + HelpAnnotation(tag = "library") { + append(stringResource(id = R.string.help_desc_how_to_add_books_4)) + } + append(".") + }, + tags = listOf("browse", "library"), + shouldShowDescription = state.showHelpItem2, + onTitleClick = { + viewModel.onUpdate { + it.copy( + showHelpItem2 = !it.showHelpItem2 + ) + } + }, + onTagClick = { tag -> + when (tag) { + "browse" -> { + navigator.navigate(Screen.BROWSE, true) + } + + "library" -> { + navigator.navigate(Screen.LIBRARY, true) + } + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpClickMeNoteItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpClickMeNoteItem.kt new file mode 100644 index 00000000..4f004195 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpClickMeNoteItem.kt @@ -0,0 +1,52 @@ +package ua.acclorite.book_story.presentation.screens.help.components.items + +import android.widget.Toast +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.buildAnnotatedString +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation +import ua.acclorite.book_story.presentation.screens.help.components.HelpClickableNote +import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel + +@Composable +fun LazyItemScope.HelpClickMeNoteItem(viewModel: HelpViewModel) { + val state by viewModel.state.collectAsState() + val context = LocalContext.current + + HelpClickableNote( + text = buildAnnotatedString { + append(stringResource(id = R.string.help_clickable_note_1) + " ") + + HelpAnnotation(tag = "note") { + append(stringResource(id = R.string.help_clickable_note_2)) + } + + append(" " + stringResource(id = R.string.help_clickable_note_3)) + }, + tags = listOf("note"), + isNoteFullyShown = state.showNote1, + onIconClick = { + viewModel.onUpdate { + it.copy( + showNote1 = !it.showNote1 + ) + } + }, + onTagClick = { tag -> + when (tag) { + "note" -> { + Toast.makeText( + context, + context.getString(R.string.help_clickable_note_action), + Toast.LENGTH_SHORT + ).show() + } + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpCustomizeApp.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpCustomizeApp.kt new file mode 100644 index 00000000..b4f57d41 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpCustomizeApp.kt @@ -0,0 +1,49 @@ +package ua.acclorite.book_story.presentation.screens.help.components.items + +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.buildAnnotatedString +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.data.Navigator +import ua.acclorite.book_story.presentation.data.Screen +import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation +import ua.acclorite.book_story.presentation.screens.help.components.HelpItem +import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel + +@Composable +fun LazyItemScope.HelpCustomizeApp(viewModel: HelpViewModel, navigator: Navigator) { + val state by viewModel.state.collectAsState() + + HelpItem( + title = stringResource(id = R.string.help_title_how_to_customize_app), + description = buildAnnotatedString { + append(stringResource(id = R.string.help_desc_how_to_customize_app_1) + " ") + + HelpAnnotation(tag = "settings") { + append(stringResource(id = R.string.help_desc_how_to_customize_app_2)) + } + append(". ") + + append(stringResource(id = R.string.help_desc_how_to_customize_app_3)) + }, + tags = listOf("settings"), + shouldShowDescription = state.showHelpItem3, + onTitleClick = { + viewModel.onUpdate { + it.copy( + showHelpItem3 = !it.showHelpItem3 + ) + } + }, + onTagClick = { tag -> + when (tag) { + "settings" -> { + navigator.navigate(Screen.SETTINGS, true) + } + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpCustomizeReader.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpCustomizeReader.kt new file mode 100644 index 00000000..36a2e29b --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpCustomizeReader.kt @@ -0,0 +1,49 @@ +package ua.acclorite.book_story.presentation.screens.help.components.items + +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.buildAnnotatedString +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.data.Navigator +import ua.acclorite.book_story.presentation.data.Screen +import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation +import ua.acclorite.book_story.presentation.screens.help.components.HelpItem +import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel + +@Composable +fun LazyItemScope.HelpCustomizeReader(viewModel: HelpViewModel, navigator: Navigator) { + val state by viewModel.state.collectAsState() + + HelpItem( + title = stringResource(id = R.string.help_title_how_to_customize_reader), + description = buildAnnotatedString { + append(stringResource(id = R.string.help_desc_how_to_customize_reader_1) + " ") + + HelpAnnotation(tag = "settings") { + append(stringResource(id = R.string.help_desc_how_to_customize_reader_2)) + } + append(" ") + + append(stringResource(id = R.string.help_desc_how_to_customize_reader_3)) + }, + tags = listOf("settings"), + shouldShowDescription = state.showHelpItem7, + onTitleClick = { + viewModel.onUpdate { + it.copy( + showHelpItem7 = !it.showHelpItem7 + ) + } + }, + onTagClick = { tag -> + when (tag) { + "settings" -> { + navigator.navigate(Screen.SETTINGS, true) + } + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpEditBook.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpEditBook.kt new file mode 100644 index 00000000..0574e305 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpEditBook.kt @@ -0,0 +1,49 @@ +package ua.acclorite.book_story.presentation.screens.help.components.items + +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.buildAnnotatedString +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.data.Navigator +import ua.acclorite.book_story.presentation.data.Screen +import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation +import ua.acclorite.book_story.presentation.screens.help.components.HelpItem +import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel + +@Composable +fun LazyItemScope.HelpEditBook(viewModel: HelpViewModel, navigator: Navigator) { + val state by viewModel.state.collectAsState() + + HelpItem( + title = stringResource(id = R.string.help_title_how_to_edit_book), + description = buildAnnotatedString { + append(stringResource(id = R.string.help_desc_how_to_edit_book_1) + " ") + + HelpAnnotation(tag = "library") { + append(stringResource(id = R.string.help_desc_how_to_edit_book_2)) + } + append(" ") + + append(stringResource(id = R.string.help_desc_how_to_edit_book_3)) + }, + tags = listOf("library"), + shouldShowDescription = state.showHelpItem5, + onTitleClick = { + viewModel.onUpdate { + it.copy( + showHelpItem5 = !it.showHelpItem5 + ) + } + }, + onTagClick = { tag -> + when (tag) { + "library" -> { + navigator.navigate(Screen.LIBRARY, true) + } + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpFindBooksItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpFindBooksItem.kt new file mode 100644 index 00000000..33b742ef --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpFindBooksItem.kt @@ -0,0 +1,184 @@ +package ua.acclorite.book_story.presentation.screens.help.components.items + +import android.widget.Toast +import androidx.compose.foundation.layout.Box +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.lazy.LazyItemScope +import androidx.compose.foundation.text.ClickableText +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.unit.dp +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.components.CustomIconButton +import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation +import ua.acclorite.book_story.presentation.screens.help.components.HelpItem +import ua.acclorite.book_story.presentation.screens.help.data.HelpEvent +import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel +import ua.acclorite.book_story.presentation.ui.SlidingTransition + +@Composable +fun LazyItemScope.HelpFindBooksItem(viewModel: HelpViewModel) { + val state by viewModel.state.collectAsState() + val context = LocalContext.current + + val sites = stringArrayResource(id = R.array.book_sites) + val text = buildAnnotatedString { + append(stringResource(id = R.string.help_field_support) + " ") + + sites.forEachIndexed { index, string -> + HelpAnnotation(tag = string) { + append(string.substringBeforeLast("|")) + } + + if (index < sites.lastIndex) { + append(", ") + } + } + } + + HelpItem( + title = stringResource(id = R.string.help_title_how_to_find_books), + description = buildAnnotatedString { + append(stringResource(id = R.string.help_desc_how_to_find_books)) + }, + tags = emptyList(), + shouldShowDescription = state.showHelpItem1, + onTitleClick = { + viewModel.onUpdate { + it.copy( + showHelpItem1 = !it.showHelpItem1 + ) + } + }, + customContent = { + Spacer(modifier = Modifier.height(4.dp)) + OutlinedTextField( + value = state.textFieldValue, + modifier = Modifier + .fillMaxWidth(), + onValueChange = { value -> + viewModel.onUpdate { + it.copy( + textFieldValue = value, + showError = false + ) + } + }, + label = { + Text(text = stringResource(id = R.string.help_field_placeholder)) + }, + keyboardActions = KeyboardActions( + onSearch = { + viewModel.onEvent( + HelpEvent.OnSearchInWeb( + context = context, + noAppsFound = { + Toast.makeText( + context, + context.getString(R.string.error_no_browser), + Toast.LENGTH_SHORT + ).show() + } + ) + ) + } + ), + keyboardOptions = KeyboardOptions( + KeyboardCapitalization.Words, + imeAction = ImeAction.Search + ), + trailingIcon = { + CustomIconButton( + icon = Icons.Default.Search, + contentDescription = R.string.search_content_desc, + disableOnClick = false, + enabled = !state.showError, + color = if (state.showError) { + MaterialTheme.colorScheme.outline + } else { + MaterialTheme.colorScheme.primary + } + ) { + viewModel.onEvent( + HelpEvent.OnSearchInWeb( + context = context, + noAppsFound = { + Toast.makeText( + context, + context.getString(R.string.error_no_browser), + Toast.LENGTH_SHORT + ).show() + } + ) + ) + } + }, + supportingText = { + Column { + Spacer(modifier = Modifier.height(2.dp)) + Box { + SlidingTransition(visible = state.showError) { + Text( + text = stringResource(id = R.string.help_field_error), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodySmall + ) + } + SlidingTransition(visible = !state.showError) { + ClickableText( + text = text, + style = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) { offset -> + sites.forEach { tag -> + text.getStringAnnotations( + tag = tag, + start = offset, + end = offset + ).firstOrNull()?.let { + viewModel.onEvent( + HelpEvent.OnNavigateToBrowserPage( + page = tag.substringAfterLast("|"), + context = context, + noAppsFound = { + Toast.makeText( + context, + context.getString(R.string.error_no_browser), + Toast.LENGTH_SHORT + ).show() + } + ) + ) + } + } + + } + } + } + } + }, + singleLine = true + ) + }, + onTagClick = {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpMoveOrDeleteBooks.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpMoveOrDeleteBooks.kt new file mode 100644 index 00000000..878ac747 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpMoveOrDeleteBooks.kt @@ -0,0 +1,49 @@ +package ua.acclorite.book_story.presentation.screens.help.components.items + +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.buildAnnotatedString +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.data.Navigator +import ua.acclorite.book_story.presentation.data.Screen +import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation +import ua.acclorite.book_story.presentation.screens.help.components.HelpItem +import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel + +@Composable +fun LazyItemScope.HelpMoveOrDeleteBooks(viewModel: HelpViewModel, navigator: Navigator) { + val state by viewModel.state.collectAsState() + + HelpItem( + title = stringResource(id = R.string.help_title_how_to_move_or_delete_books), + description = buildAnnotatedString { + append(stringResource(id = R.string.help_desc_how_to_move_or_delete_books_1) + " ") + + HelpAnnotation(tag = "library") { + append(stringResource(id = R.string.help_desc_how_to_move_or_delete_books_2)) + } + append(". ") + + append(stringResource(id = R.string.help_desc_how_to_move_or_delete_books_3)) + }, + tags = listOf("library"), + shouldShowDescription = state.showHelpItem4, + onTitleClick = { + viewModel.onUpdate { + it.copy( + showHelpItem4 = !it.showHelpItem4 + ) + } + }, + onTagClick = { tag -> + when (tag) { + "library" -> { + navigator.navigate(Screen.LIBRARY, true) + } + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpReadBook.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpReadBook.kt new file mode 100644 index 00000000..efeaccbb --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpReadBook.kt @@ -0,0 +1,49 @@ +package ua.acclorite.book_story.presentation.screens.help.components.items + +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.buildAnnotatedString +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.data.Navigator +import ua.acclorite.book_story.presentation.data.Screen +import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation +import ua.acclorite.book_story.presentation.screens.help.components.HelpItem +import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel + +@Composable +fun LazyItemScope.HelpReadBook(viewModel: HelpViewModel, navigator: Navigator) { + val state by viewModel.state.collectAsState() + + HelpItem( + title = stringResource(id = R.string.help_title_how_to_read_book), + description = buildAnnotatedString { + append(stringResource(id = R.string.help_desc_how_to_read_book_1) + " ") + + HelpAnnotation(tag = "library") { + append(stringResource(id = R.string.help_desc_how_to_read_book_2)) + } + append(". ") + + append(stringResource(id = R.string.help_desc_how_to_read_book_3)) + }, + tags = listOf("library"), + shouldShowDescription = state.showHelpItem6, + onTitleClick = { + viewModel.onUpdate { + it.copy( + showHelpItem6 = !it.showHelpItem6 + ) + } + }, + onTagClick = { tag -> + when (tag) { + "library" -> { + navigator.navigate(Screen.LIBRARY, true) + } + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpUpdateBook.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpUpdateBook.kt new file mode 100644 index 00000000..070c184f --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/components/items/HelpUpdateBook.kt @@ -0,0 +1,49 @@ +package ua.acclorite.book_story.presentation.screens.help.components.items + +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.buildAnnotatedString +import ua.acclorite.book_story.R +import ua.acclorite.book_story.presentation.data.Navigator +import ua.acclorite.book_story.presentation.data.Screen +import ua.acclorite.book_story.presentation.screens.help.components.HelpAnnotation +import ua.acclorite.book_story.presentation.screens.help.components.HelpItem +import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel + +@Composable +fun LazyItemScope.HelpUpdateBook(viewModel: HelpViewModel, navigator: Navigator) { + val state by viewModel.state.collectAsState() + + HelpItem( + title = stringResource(id = R.string.help_title_how_to_update_book), + description = buildAnnotatedString { + append(stringResource(id = R.string.help_desc_how_to_update_book_1) + " ") + + HelpAnnotation(tag = "library") { + append(stringResource(id = R.string.help_desc_how_to_update_book_2)) + } + append(" ") + + append(stringResource(id = R.string.help_desc_how_to_update_book_3)) + }, + tags = listOf("library"), + shouldShowDescription = state.showHelpItem8, + onTitleClick = { + viewModel.onUpdate { + it.copy( + showHelpItem8 = !it.showHelpItem8 + ) + } + }, + onTagClick = { tag -> + when (tag) { + "library" -> { + navigator.navigate(Screen.LIBRARY, true) + } + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpEvent.kt index cca81a95..7fd97ecc 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpEvent.kt @@ -3,6 +3,11 @@ package ua.acclorite.book_story.presentation.screens.help.data import android.content.Context sealed class HelpEvent { + data class OnSearchInWeb( + val context: Context, + val noAppsFound: () -> Unit + ) : HelpEvent() + data class OnNavigateToBrowserPage( val page: String, val context: Context, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpState.kt new file mode 100644 index 00000000..7cdb15a4 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpState.kt @@ -0,0 +1,17 @@ +package ua.acclorite.book_story.presentation.screens.help.data + +data class HelpState( + val showNote1: Boolean = true, + + val showHelpItem1: Boolean = false, + val textFieldValue: String = "", + val showError: Boolean = false, + + val showHelpItem2: Boolean = false, + val showHelpItem3: Boolean = false, + val showHelpItem4: Boolean = false, + val showHelpItem5: Boolean = false, + val showHelpItem6: Boolean = false, + val showHelpItem7: Boolean = false, + val showHelpItem8: Boolean = false, +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpViewModel.kt index 4d02775d..b6b56752 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/help/data/HelpViewModel.kt @@ -1,10 +1,14 @@ package ua.acclorite.book_story.presentation.screens.help.data +import android.app.SearchManager 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.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -13,6 +17,9 @@ class HelpViewModel @Inject constructor( ) : ViewModel() { + private val _state = MutableStateFlow(HelpState()) + val state = _state.asStateFlow() + fun onEvent(event: HelpEvent) { when (event) { is HelpEvent.OnNavigateToBrowserPage -> { @@ -30,6 +37,40 @@ class HelpViewModel @Inject constructor( event.noAppsFound() } } + + is HelpEvent.OnSearchInWeb -> { + viewModelScope.launch { + if (_state.value.textFieldValue.isBlank()) { + _state.update { + it.copy( + showError = true + ) + } + return@launch + } + + val intent = Intent() + + intent.action = Intent.ACTION_WEB_SEARCH + intent.putExtra( + SearchManager.QUERY, + "${_state.value.textFieldValue.trim()} filetype:txt OR filetype:pdf" + ) + + if (intent.resolveActivity(event.context.packageManager) != null) { + event.context.startActivity(intent) + return@launch + } + + event.noAppsFound() + } + } + } + } + + fun onUpdate(calculation: (HelpState) -> HelpState) { + _state.update { + calculation(it) } } } 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 7b920b19..33d85d82 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 @@ -4,7 +4,6 @@ import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween import androidx.compose.animation.fadeOut -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer @@ -68,7 +67,7 @@ import ua.acclorite.book_story.presentation.ui.Transitions @OptIn( ExperimentalMaterial3Api::class, - ExperimentalMaterialApi::class, ExperimentalFoundationApi::class + ExperimentalMaterialApi::class ) @Composable fun HistoryScreen( @@ -214,7 +213,7 @@ fun HistoryScreen( } CategoryTitle( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), title = when (groupedHistory.title) { "today" -> stringResource(id = R.string.today) "yesterday" -> stringResource(id = R.string.yesterday) @@ -230,7 +229,7 @@ fun HistoryScreen( groupedHistory.history, key = { it.id } ) { HistoryItem( - modifier = Modifier.animateItemPlacement(), + modifier = Modifier.animateItem(), history = it, isOnClickEnabled = !state.isRefreshing, onBodyClick = { 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 32daf7d8..44d92a88 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 @@ -347,6 +347,7 @@ class LibraryViewModel @Inject constructor( query: String = if (_state.value.showSearch) _state.value.searchQuery else "" ) { val books = getBooks.execute(query).map { book -> Pair(book, false) } + val categorizedBooks = books.groupBy { it.first.category }.toList().map { 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 1a5d42f5..6e86c596 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 @@ -89,7 +89,11 @@ fun ReaderScreen( val listState = rememberLazyListState() val state by viewModel.state.collectAsState() - val canScrollForward by remember { derivedStateOf { listState.canScrollForward } } + val canScroll by remember { + derivedStateOf { + listState.canScrollBackward && listState.canScrollForward + } + } val nestedScrollConnection = remember { object : NestedScrollConnection { override fun onPostScroll( @@ -129,8 +133,8 @@ fun ReaderScreen( } ) } - LaunchedEffect(canScrollForward) { - if (!canScrollForward && !state.showMenu && !loading) { + LaunchedEffect(canScroll) { + if (!canScroll && !state.showMenu && !loading) { viewModel.onEvent(ReaderEvent.OnShowHideMenu(context = context)) } } 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 671eed99..820090e7 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 @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.MenuBook import androidx.compose.material.icons.filled.DisplaySettings @@ -26,6 +25,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.components.GoBackButton +import ua.acclorite.book_story.presentation.components.collapsibleScrollBehaviorWithLazyListState 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 @@ -35,17 +35,12 @@ import ua.acclorite.book_story.presentation.screens.settings.components.Settings fun SettingsScreen( navigator: Navigator ) { - val listState = rememberLazyListState() - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( - canScroll = { - listState.canScrollForward || listState.canScrollBackward - } - ) + val scrollState = TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState() Scaffold( Modifier .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) + .nestedScroll(scrollState.first.nestedScrollConnection) .windowInsetsPadding(WindowInsets.navigationBars), containerColor = MaterialTheme.colorScheme.surface, topBar = { @@ -56,7 +51,7 @@ fun SettingsScreen( navigationIcon = { GoBackButton(navigator = navigator) }, - scrollBehavior = scrollBehavior, + scrollBehavior = scrollState.first, colors = TopAppBarDefaults.largeTopAppBarColors( containerColor = MaterialTheme.colorScheme.surface, scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer @@ -68,7 +63,7 @@ fun SettingsScreen( Modifier .fillMaxSize() .padding(top = paddingValues.calculateTopPadding()), - state = listState + state = scrollState.second ) { item { Spacer(modifier = Modifier.height(16.dp)) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ColorPickerWithTitle.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ColorPickerWithTitle.kt index 720d009e..26c553ab 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ColorPickerWithTitle.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ColorPickerWithTitle.kt @@ -32,6 +32,7 @@ import ua.acclorite.book_story.presentation.components.CustomIconButton */ @Composable fun ColorPickerWithTitle( + modifier: Modifier = Modifier, value: Color, title: String, horizontalPadding: Dp = 18.dp, @@ -42,7 +43,7 @@ fun ColorPickerWithTitle( var color by remember { mutableStateOf(value) } Column( - Modifier + modifier .fillMaxWidth() .padding(vertical = verticalPadding, horizontal = horizontalPadding) ) { diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SegmentedButtonWithTitle.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SegmentedButtonWithTitle.kt index e89ae291..718c92d1 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SegmentedButtonWithTitle.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/SegmentedButtonWithTitle.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SingleChoiceSegmentedButtonRow @@ -19,12 +18,12 @@ import androidx.compose.ui.unit.dp import ua.acclorite.book_story.domain.model.ButtonItem import ua.acclorite.book_story.presentation.components.CategoryTitle -@OptIn(ExperimentalMaterial3Api::class) @Composable fun SegmentedButtonWithTitle( + modifier: Modifier = Modifier, title: String, buttons: List, - enabled: Boolean, + locked: Boolean, horizontalPadding: Dp = 18.dp, verticalPadding: Dp = 8.dp, onClick: (ButtonItem) -> Unit @@ -32,7 +31,7 @@ fun SegmentedButtonWithTitle( val colors = SegmentedButtonDefaults.colors() Column( - Modifier + modifier .fillMaxWidth() .padding(horizontal = horizontalPadding, vertical = verticalPadding) ) { @@ -43,7 +42,7 @@ fun SegmentedButtonWithTitle( SingleChoiceSegmentedButtonRow { buttons.forEachIndexed { index, buttonItem -> SegmentedButton( - enabled = enabled, + enabled = locked, selected = buttonItem.selected, onClick = { onClick(buttonItem) }, shape = when (index) { @@ -60,9 +59,12 @@ fun SegmentedButtonWithTitle( else -> RoundedCornerShape(0) }, colors = SegmentedButtonDefaults.colors( - disabledInactiveBorderColor = colors.disabledActiveBorderColor, - disabledInactiveContentColor = colors.disabledActiveContentColor, - disabledActiveContainerColor = colors.activeContainerColor.copy(0.12f), + disabledInactiveBorderColor = colors.activeBorderColor, + disabledInactiveContentColor = colors.inactiveContentColor, + disabledActiveContainerColor = colors.activeContainerColor, + disabledActiveContentColor = colors.activeContentColor, + disabledInactiveContainerColor = colors.inactiveContainerColor, + disabledActiveBorderColor = colors.activeBorderColor, ), label = { Text( 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 fffba2d1..c6a47ecf 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 @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.MaterialTheme @@ -16,26 +15,37 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.ButtonItem import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.components.GoBackButton +import ua.acclorite.book_story.presentation.components.collapsibleScrollBehaviorWithLazyListState 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.ColorPickerWithTitle import ua.acclorite.book_story.presentation.screens.settings.components.SegmentedButtonWithTitle import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.theme_switcher.AppearanceSettingsThemeSwitcher +import ua.acclorite.book_story.presentation.ui.BookStoryTheme import ua.acclorite.book_story.presentation.ui.DarkTheme +import ua.acclorite.book_story.presentation.ui.PureDark +import ua.acclorite.book_story.presentation.ui.SlidingTransition import ua.acclorite.book_story.presentation.ui.Theme import ua.acclorite.book_story.presentation.ui.ThemeContrast +import ua.acclorite.book_story.presentation.ui.isDark +import ua.acclorite.book_story.presentation.ui.isPureDark @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -43,19 +53,21 @@ fun AppearanceSettings( mainViewModel: MainViewModel, navigator: Navigator ) { - val listState = rememberLazyListState() - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( - canScroll = { - listState.canScrollForward || listState.canScrollBackward - } - ) + val scrollState = TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState() val state by mainViewModel.state.collectAsState() + var themeContrastTheme by remember { mutableStateOf(state.theme!!) } + + LaunchedEffect(state.theme) { + if (themeContrastTheme != state.theme && state.theme != Theme.DYNAMIC) { + themeContrastTheme = state.theme!! + } + } Scaffold( Modifier .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) + .nestedScroll(scrollState.first.nestedScrollConnection) .windowInsetsPadding(WindowInsets.navigationBars), containerColor = MaterialTheme.colorScheme.surface, topBar = { @@ -66,7 +78,7 @@ fun AppearanceSettings( navigationIcon = { GoBackButton(navigator = navigator) }, - scrollBehavior = scrollBehavior, + scrollBehavior = scrollState.first, colors = TopAppBarDefaults.largeTopAppBarColors( containerColor = MaterialTheme.colorScheme.surface, scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer @@ -78,25 +90,35 @@ fun AppearanceSettings( Modifier .fillMaxSize() .padding(top = paddingValues.calculateTopPadding()), - state = listState + state = scrollState.second ) { item { - Spacer(modifier = Modifier.height(16.dp)) + Spacer( + modifier = Modifier + .animateItem() + .height(16.dp) + ) } item { CategoryTitle( + modifier = Modifier.animateItem(), title = stringResource(id = R.string.theme_appearance_settings), color = MaterialTheme.colorScheme.primary ) } item { - Spacer(modifier = Modifier.height(8.dp)) + Spacer( + modifier = Modifier + .animateItem() + .height(8.dp) + ) } item { SegmentedButtonWithTitle( + modifier = Modifier.animateItem(), title = stringResource(id = R.string.dark_theme_option), - enabled = true, + locked = true, buttons = DarkTheme.entries.map { ButtonItem( it.toString(), @@ -119,49 +141,106 @@ fun AppearanceSettings( } item { - SegmentedButtonWithTitle( - title = stringResource(id = R.string.theme_contrast_option), - enabled = state.theme != Theme.DYNAMIC, - buttons = ThemeContrast.entries.map { - ButtonItem( - it.toString(), - when (it) { - ThemeContrast.STANDARD -> stringResource(id = R.string.theme_contrast_standard) - ThemeContrast.MEDIUM -> stringResource(id = R.string.theme_contrast_medium) - ThemeContrast.HIGH -> stringResource(id = R.string.theme_contrast_high) - }, - MaterialTheme.typography.labelLarge, - it == state.themeContrast - ) - } + AppearanceSettingsThemeSwitcher( + modifier = Modifier.animateItem(), + mainViewModel = mainViewModel + ) + } + + item { + BookStoryTheme( + theme = themeContrastTheme, + isDark = state.darkTheme!!.isDark(), + isPureDark = state.pureDark!!.isPureDark(context = LocalContext.current), + themeContrast = state.themeContrast!! ) { - mainViewModel.onEvent( - MainEvent.OnChangeThemeContrast( - it.id - ) - ) + SlidingTransition( + modifier = Modifier.animateItem( + fadeInSpec = null, + fadeOutSpec = null + ), + visible = state.theme != Theme.DYNAMIC, + ) { + SegmentedButtonWithTitle( + title = stringResource(id = R.string.theme_contrast_option), + locked = state.theme != Theme.DYNAMIC, + buttons = ThemeContrast.entries.map { + ButtonItem( + it.toString(), + when (it) { + ThemeContrast.STANDARD -> stringResource(id = R.string.theme_contrast_standard) + ThemeContrast.MEDIUM -> stringResource(id = R.string.theme_contrast_medium) + ThemeContrast.HIGH -> stringResource(id = R.string.theme_contrast_high) + }, + MaterialTheme.typography.labelLarge, + it == state.themeContrast + ) + } + ) { + mainViewModel.onEvent( + MainEvent.OnChangeThemeContrast( + it.id + ) + ) + } + } } } item { - AppearanceSettingsThemeSwitcher(mainViewModel = mainViewModel) + SlidingTransition( + modifier = Modifier.animateItem( + fadeInSpec = null, + fadeOutSpec = null + ), + visible = state.darkTheme!!.isDark(), + ) { + SegmentedButtonWithTitle( + title = stringResource(id = R.string.pure_dark_option), + locked = true, + buttons = PureDark.entries.map { + ButtonItem( + it.toString(), + when (it) { + PureDark.OFF -> stringResource(id = R.string.pure_dark_off) + PureDark.ON -> stringResource(id = R.string.pure_dark_on) + PureDark.SAVER -> stringResource(id = R.string.pure_dark_power_saver) + }, + MaterialTheme.typography.labelLarge, + it == state.pureDark + ) + } + ) { + mainViewModel.onEvent( + MainEvent.OnChangePureDark( + it.id + ) + ) + } + } } item { - Spacer(modifier = Modifier.height(22.dp)) - } - item { + Spacer( + modifier = Modifier + .animateItem() + .height(22.dp) + ) CategoryTitle( title = stringResource(id = R.string.colors_appearance_settings), - color = MaterialTheme.colorScheme.primary + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.animateItem() + ) + Spacer( + modifier = Modifier + .animateItem() + .height(8.dp) ) - } - item { - Spacer(modifier = Modifier.height(8.dp)) } item { ColorPickerWithTitle( + modifier = Modifier.animateItem(), value = Color(state.backgroundColor!!.toULong()), title = stringResource(id = R.string.background_color_option), onValueChange = { @@ -175,6 +254,7 @@ fun AppearanceSettings( } item { ColorPickerWithTitle( + modifier = Modifier.animateItem(), value = Color(state.fontColor!!.toULong()), title = stringResource(id = R.string.font_color_option), onValueChange = { @@ -187,7 +267,13 @@ fun AppearanceSettings( ) } - item { Spacer(modifier = Modifier.height(48.dp)) } + item { + Spacer( + modifier = Modifier + .animateItem() + .height(48.dp) + ) + } } } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcher.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/theme_switcher/AppearanceSettingsThemeSwitcher.kt index 2deba751..1108e26c 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 @@ -5,6 +5,7 @@ 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.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.itemsIndexed @@ -13,7 +14,9 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.util.Constants @@ -22,13 +25,16 @@ import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainViewModel import ua.acclorite.book_story.presentation.ui.Theme import ua.acclorite.book_story.presentation.ui.isDark +import ua.acclorite.book_story.presentation.ui.isPureDark /** * Theme switcher. */ @Composable fun AppearanceSettingsThemeSwitcher( - mainViewModel: MainViewModel + modifier: Modifier = Modifier, + mainViewModel: MainViewModel, + verticalPadding: Dp = 8.dp ) { val state by mainViewModel.state.collectAsState() val themes = remember { @@ -37,10 +43,10 @@ fun AppearanceSettingsThemeSwitcher( } Column( - Modifier + modifier .fillMaxWidth() + .padding(vertical = verticalPadding) ) { - Spacer(modifier = Modifier.height(8.dp)) CategoryTitle( title = stringResource(id = R.string.app_theme_option) ) @@ -60,6 +66,7 @@ fun AppearanceSettingsThemeSwitcher( theme = themeEntry, darkTheme = state.darkTheme!!.isDark(), themeContrast = state.themeContrast!!, + isPureDark = state.pureDark!!.isPureDark(context = LocalContext.current), 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 52cea293..b7611d99 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 @@ -45,11 +45,17 @@ import ua.acclorite.book_story.presentation.ui.animatedColorScheme fun AppearanceSettingsThemeSwitcherItem( theme: Pair, darkTheme: Boolean, + isPureDark: Boolean, themeContrast: ThemeContrast, selected: Boolean, onClick: () -> Unit ) { - val colorScheme = animatedColorScheme(theme.first, darkTheme, themeContrast) + val colorScheme = animatedColorScheme( + theme.first, + darkTheme, + isPureDark, + themeContrast + ) Column( modifier = Modifier.fillMaxWidth(), 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 f91724e4..b738d306 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 @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.MaterialTheme @@ -26,6 +25,7 @@ import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.ButtonItem import ua.acclorite.book_story.domain.util.Constants import ua.acclorite.book_story.presentation.components.GoBackButton +import ua.acclorite.book_story.presentation.components.collapsibleScrollBehaviorWithLazyListState import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainViewModel import ua.acclorite.book_story.presentation.data.Navigator @@ -37,19 +37,13 @@ fun GeneralSettings( mainViewModel: MainViewModel, navigator: Navigator ) { - val listState = rememberLazyListState() - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( - canScroll = { - listState.canScrollForward || listState.canScrollBackward - } - ) - + val scrollState = TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState() val state by mainViewModel.state.collectAsState() Scaffold( Modifier .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) + .nestedScroll(scrollState.first.nestedScrollConnection) .windowInsetsPadding(WindowInsets.navigationBars), containerColor = MaterialTheme.colorScheme.surface, topBar = { @@ -60,7 +54,7 @@ fun GeneralSettings( navigationIcon = { GoBackButton(navigator = navigator) }, - scrollBehavior = scrollBehavior, + scrollBehavior = scrollState.first, colors = TopAppBarDefaults.largeTopAppBarColors( containerColor = MaterialTheme.colorScheme.surface, scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer @@ -72,7 +66,7 @@ fun GeneralSettings( Modifier .fillMaxSize() .padding(top = paddingValues.calculateTopPadding()), - state = listState + state = scrollState.second ) { item { Spacer(modifier = Modifier.height(8.dp)) 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 937e3116..f1cc5042 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 @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.MaterialTheme @@ -29,6 +28,7 @@ import ua.acclorite.book_story.domain.model.ButtonItem 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.components.collapsibleScrollBehaviorWithLazyListState import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainViewModel import ua.acclorite.book_story.presentation.data.Navigator @@ -42,12 +42,7 @@ fun ReaderSettings( mainViewModel: MainViewModel, navigator: Navigator ) { - val listState = rememberLazyListState() - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( - canScroll = { - listState.canScrollForward || listState.canScrollBackward - } - ) + val scrollState = TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState() val state by mainViewModel.state.collectAsState() val fontFamily = remember(state.fontFamily) { @@ -59,7 +54,7 @@ fun ReaderSettings( Scaffold( Modifier .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) + .nestedScroll(scrollState.first.nestedScrollConnection) .windowInsetsPadding(WindowInsets.navigationBars), containerColor = MaterialTheme.colorScheme.surface, topBar = { @@ -70,7 +65,7 @@ fun ReaderSettings( navigationIcon = { GoBackButton(navigator = navigator) }, - scrollBehavior = scrollBehavior, + scrollBehavior = scrollState.first, colors = TopAppBarDefaults.largeTopAppBarColors( containerColor = MaterialTheme.colorScheme.surface, scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer @@ -82,7 +77,7 @@ fun ReaderSettings( Modifier .fillMaxSize() .padding(top = paddingValues.calculateTopPadding()), - state = listState + state = scrollState.second ) { item { Spacer(modifier = Modifier.height(16.dp)) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/Color.kt b/app/src/main/java/ua/acclorite/book_story/presentation/ui/Color.kt index b2030d0e..c5279f9d 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/ui/Color.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/ui/Color.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.ui.platform.LocalContext import ua.acclorite.book_story.presentation.ui.theme.aquaTheme +import ua.acclorite.book_story.presentation.ui.theme.blackTheme 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 @@ -43,49 +44,61 @@ fun String.toTheme(): Theme { */ @SuppressLint("NewApi") @Composable -fun colorScheme(theme: Theme, darkTheme: Boolean, themeContrast: ThemeContrast): ColorScheme { - when (theme) { +fun colorScheme( + theme: Theme, + darkTheme: Boolean, + isPureDark: Boolean, + themeContrast: ThemeContrast +): ColorScheme { + val colorScheme = when (theme) { Theme.DYNAMIC -> { /* Dynamic Theme */ - return if (darkTheme) + if (darkTheme) { dynamicDarkColorScheme(LocalContext.current) - else + } else { dynamicLightColorScheme(LocalContext.current) + } } Theme.BLUE -> { /* Blue Theme */ - return blueTheme(isDark = darkTheme, themeContrast = themeContrast) + blueTheme(isDark = darkTheme, themeContrast = themeContrast) } Theme.PURPLE -> { /* Purple Theme */ - return purpleTheme(isDark = darkTheme, themeContrast = themeContrast) + purpleTheme(isDark = darkTheme, themeContrast = themeContrast) } Theme.GREEN -> { /* Green Theme */ - return greenTheme(isDark = darkTheme, themeContrast = themeContrast) + greenTheme(isDark = darkTheme, themeContrast = themeContrast) } Theme.PINK -> { /* Pink Theme */ - return pinkTheme(isDark = darkTheme, themeContrast = themeContrast) + pinkTheme(isDark = darkTheme, themeContrast = themeContrast) } Theme.YELLOW -> { /* Yellow Theme */ - return yellowTheme(isDark = darkTheme, themeContrast = themeContrast) + yellowTheme(isDark = darkTheme, themeContrast = themeContrast) } Theme.RED -> { /* Red Theme */ - return redTheme(isDark = darkTheme, themeContrast = themeContrast) + redTheme(isDark = darkTheme, themeContrast = themeContrast) } Theme.AQUA -> { /* Aqua Theme */ - return aquaTheme(isDark = darkTheme, themeContrast = themeContrast) + aquaTheme(isDark = darkTheme, themeContrast = themeContrast) } } + + return if (isPureDark && darkTheme) { + blackTheme(initialTheme = colorScheme) + } else { + colorScheme + } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/Theme.kt b/app/src/main/java/ua/acclorite/book_story/presentation/ui/Theme.kt index 5997f87d..2bbfea0c 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/ui/Theme.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/ui/Theme.kt @@ -1,7 +1,9 @@ package ua.acclorite.book_story.presentation.ui import android.app.Activity +import android.content.Context import android.os.Build +import android.os.PowerManager import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.isSystemInDarkTheme @@ -16,6 +18,13 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat +@Immutable +enum class PureDark { + OFF, + ON, + SAVER +} + @Immutable enum class ThemeContrast { STANDARD, @@ -34,6 +43,10 @@ fun String.toDarkTheme(): DarkTheme { return DarkTheme.valueOf(this) } +fun String.toPureDark(): PureDark { + return PureDark.valueOf(this) +} + fun String.toThemeContrast(): ThemeContrast { return ThemeContrast.valueOf(this) } @@ -48,9 +61,21 @@ fun DarkTheme.isDark(): Boolean { } @Composable -fun BooksHistoryResurrectionTheme( +fun PureDark.isPureDark(context: Context): Boolean { + val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager + + return when (this) { + PureDark.OFF -> false + PureDark.ON -> true + PureDark.SAVER -> powerManager.isPowerSaveMode + } +} + +@Composable +fun BookStoryTheme( theme: Theme, isDark: Boolean, + isPureDark: Boolean, themeContrast: ThemeContrast, content: @Composable () -> Unit ) { @@ -76,6 +101,7 @@ fun BooksHistoryResurrectionTheme( theme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) theme else if (theme != Theme.DYNAMIC) theme else Theme.BLUE, darkTheme = isDark, + isPureDark = isPureDark, themeContrast = themeContrast ) val animatedColorScheme = animateColorScheme(targetColorScheme = colorScheme) @@ -143,11 +169,13 @@ private fun animateColorScheme(targetColorScheme: ColorScheme): ColorScheme { fun animatedColorScheme( theme: Theme, isDark: Boolean, + isPureDark: Boolean, themeContrast: ThemeContrast ): ColorScheme { val colorScheme = colorScheme( theme = theme, darkTheme = isDark, + isPureDark = isPureDark, themeContrast = themeContrast ) diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/Transitions.kt b/app/src/main/java/ua/acclorite/book_story/presentation/ui/Transitions.kt index ec36796f..09bd24de 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/ui/Transitions.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/ui/Transitions.kt @@ -5,7 +5,9 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.slideOutVertically import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -13,20 +15,20 @@ object Transitions { val DefaultTransitionIn = fadeIn(tween(300)) val DefaultTransitionOut = fadeOut(tween(300)) - val FadeTransitionIn = fadeIn(tween(300)) - val FadeTransitionOut = fadeOut(tween(50)) + val FadeTransitionIn = fadeIn(tween(350)) + val FadeTransitionOut = fadeOut(tween(100)) - val SlidingTransitionIn = fadeIn(tween(300)) + - slideInHorizontally(tween(300)) { it / 16 } + val SlidingTransitionIn = fadeIn(tween(350)) + + slideInHorizontally(tween(350)) { it / 16 } - val BackSlidingTransitionIn = fadeIn(tween(300)) + - slideInHorizontally(tween(300)) { -it / 16 } + val BackSlidingTransitionIn = fadeIn(tween(350)) + + slideInHorizontally(tween(350)) { -it / 16 } - val SlidingTransitionOut = fadeOut(tween(200)) + - slideOutHorizontally(tween(300)) { -it / 16 } + val SlidingTransitionOut = fadeOut(tween(250)) + + slideOutHorizontally(tween(350)) { -it / 16 } - val BackSlidingTransitionOut = fadeOut(tween(200)) + - slideOutHorizontally(tween(300)) { it / 16 } + val BackSlidingTransitionOut = fadeOut(tween(250)) + + slideOutHorizontally(tween(350)) { it / 16 } } @Composable @@ -45,6 +47,24 @@ fun DefaultTransition( } } +@Composable +fun SlidingTransition( + visible: Boolean, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + AnimatedVisibility( + visible = visible, + modifier = modifier, + enter = slideInVertically(tween(300)) { -it / 10 } + + fadeIn(tween(300)), + exit = slideOutVertically(tween(150)) { -it / 10 } + + fadeOut(tween(100)), + ) { + content() + } +} + diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/BlackTheme.kt b/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/BlackTheme.kt new file mode 100644 index 00000000..883dcdd6 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/ui/theme/BlackTheme.kt @@ -0,0 +1,63 @@ +package ua.acclorite.book_story.presentation.ui.theme + +import androidx.compose.material3.ColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +@Composable +fun blackTheme(initialTheme: ColorScheme): ColorScheme { + return initialTheme.copy( + surface = initialTheme.surface.copy { + val darker = 3f + it.copy( + red = it.red / darker, + green = it.green / darker, + blue = it.blue / darker + ) + }, + surfaceContainer = initialTheme.surfaceContainer.copy { + val darker = 1.95f + it.copy( + red = it.red / darker, + green = it.green / darker, + blue = it.blue / darker + ) + }, + surfaceContainerLowest = initialTheme.surfaceContainerLowest.copy { + val darker = 1.95f + it.copy( + red = it.red / darker, + green = it.green / darker, + blue = it.blue / darker + ) + }, + surfaceContainerLow = initialTheme.surfaceContainerLow.copy { + val darker = 1.95f + it.copy( + red = it.red / darker, + green = it.green / darker, + blue = it.blue / darker + ) + }, + surfaceContainerHigh = initialTheme.surfaceContainerHigh.copy { + val darker = 1.95f + it.copy( + red = it.red / darker, + green = it.green / darker, + blue = it.blue / darker + ) + }, + surfaceContainerHighest = initialTheme.surfaceContainerHighest.copy { + val darker = 1.95f + it.copy( + red = it.red / darker, + green = it.green / darker, + blue = it.blue / darker + ) + }, + ) +} + +private fun Color.copy(calculation: (Color) -> Color): Color { + return calculation(this) +} \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index b14b1add..954e54ad 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -3,7 +3,7 @@