0.9.7 - Help Screen + Pure Dark (Oled) theme + Bug fixes

This commit is contained in:
acclorite 2024-04-05 14:16:29 +03:00
parent 76e1ca3e3f
commit 513b286e07
63 changed files with 1915 additions and 487 deletions

View file

@ -15,7 +15,7 @@ android {
minSdk = 26 minSdk = 26
targetSdk = 34 targetSdk = 34
versionCode = 1 versionCode = 1
versionName = "0.9.6" versionName = "0.9.7"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
@ -66,7 +66,7 @@ dependencies {
implementation("androidx.activity:activity-compose:1.8.2") implementation("androidx.activity:activity-compose:1.8.2")
// Compose BOM libraries // 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.foundation:foundation")
implementation("androidx.compose.animation:animation") implementation("androidx.compose.animation:animation")
implementation("androidx.compose.animation:animation-core") implementation("androidx.compose.animation:animation-core")
@ -90,7 +90,7 @@ dependencies {
testImplementation("androidx.test.ext:truth:1.5.0") testImplementation("androidx.test.ext:truth:1.5.0")
testImplementation("com.squareup.okhttp3:mockwebserver:latest.release") testImplementation("com.squareup.okhttp3:mockwebserver:latest.release")
testImplementation("io.mockk:mockk: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 // Instrumentation tests
androidTestImplementation("com.google.dagger:hilt-android-testing:latest.release") androidTestImplementation("com.google.dagger:hilt-android-testing:latest.release")
@ -109,7 +109,7 @@ dependencies {
implementation("com.google.accompanist:accompanist-swiperefresh:0.24.2-alpha") implementation("com.google.accompanist:accompanist-swiperefresh:0.24.2-alpha")
// Dagger - Hilt // 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") ksp("com.google.dagger:hilt-android-compiler:2.51")
implementation("com.google.dagger:hilt-compiler:latest.release") implementation("com.google.dagger:hilt-compiler:latest.release")
ksp("androidx.hilt:hilt-compiler:latest.release") ksp("androidx.hilt:hilt-compiler:latest.release")

View file

@ -34,6 +34,7 @@
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Start.Splash" android:theme="@style/Theme.Start.Splash"
android:requestLegacyExternalStorage="true"
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="true"
tools:targetApi="tiramisu" tools:targetApi="tiramisu"
tools:ignore="DataExtractionRules"> tools:ignore="DataExtractionRules">

View file

@ -9,19 +9,25 @@ import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier 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.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp 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.appearance.AppearanceSettings
import ua.acclorite.book_story.presentation.screens.settings.nested.general.GeneralSettings 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.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.Transitions
import ua.acclorite.book_story.presentation.ui.isDark import ua.acclorite.book_story.presentation.ui.isDark
import ua.acclorite.book_story.presentation.ui.isPureDark
import java.lang.reflect.Field 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) { if (isLoaded) {
BooksHistoryResurrectionTheme( BookStoryTheme(
theme = state.theme!!, theme = state.theme!!,
isDark = state.darkTheme!!.isDark(), isDark = state.darkTheme!!.isDark(),
isPureDark = state.pureDark!!.isPureDark(this),
themeContrast = state.themeContrast!! themeContrast = state.themeContrast!!
) { ) {
NavigationHost(startScreen = Screen.LIBRARY) { NavigationHost(startScreen = Screen.LIBRARY) {

View file

@ -5,5 +5,7 @@ import kotlinx.coroutines.flow.Flow
interface DataStore { interface DataStore {
suspend fun <T> getData(key: Preferences.Key<T>, defaultValue: T): Flow<T> suspend fun <T> getData(key: Preferences.Key<T>, defaultValue: T): Flow<T>
suspend fun <T> getNullableData(key: Preferences.Key<T>): T?
suspend fun getAllData(): Set<Preferences.Key<*>>?
suspend fun <T> putData(key: Preferences.Key<T>, value: T) suspend fun <T> putData(key: Preferences.Key<T>, value: T)
} }

View file

@ -8,6 +8,7 @@ import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
@ -29,6 +30,26 @@ class DataStoreImpl @Inject constructor(context: Application) : DataStore {
result result
} }
override suspend fun <T> getNullableData(key: Preferences.Key<T>): 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<Preferences.Key<*>>? {
val keys = dataStore.data
.map {
it.asMap().keys
}
return keys.firstOrNull()
}
override suspend fun <T> putData(key: Preferences.Key<T>, value: T) { override suspend fun <T> putData(key: Preferences.Key<T>, value: T) {
dataStore.edit { preferences -> dataStore.edit { preferences ->
preferences[key] = value preferences[key] = value

View file

@ -6,9 +6,14 @@ import android.net.Uri
import android.os.Environment import android.os.Environment
import android.util.Log import android.util.Log
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers 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.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.data.local.data_store.DataStore 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.CoverImage
import ua.acclorite.book_story.domain.util.Resource import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.domain.util.UIText 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.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.UUID import java.util.UUID
@ -380,6 +385,32 @@ class BookRepositoryImpl @Inject constructor(
dataStore.putData(key, value) dataStore.putData(key, value)
} }
override suspend fun getAllSettings(scope: CoroutineScope): MainState {
val result = CompletableDeferred<MainState>()
scope.launch {
val keys = dataStore.getAllData()
val data = mutableMapOf<String, Any>()
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<Resource<List<File>>> { override suspend fun getFilesFromDevice(query: String): Flow<Resource<List<File>>> {
fun getAllFilesInDirectory(directory: File): List<File> { fun getAllFilesInDirectory(directory: File): List<File> {
val filesList = mutableListOf<File>() val filesList = mutableListOf<File>()
@ -437,7 +468,7 @@ class BookRepositoryImpl @Inject constructor(
return@filter false 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()) .contains(query.trim().lowercase())
if (!isQuery) { if (!isQuery) {
@ -451,18 +482,9 @@ class BookRepositoryImpl @Inject constructor(
) )
} }
filteredFiles = if (query.trim().isNotEmpty()) { filteredFiles = filteredFiles.sortedByDescending {
filteredFiles.sortedByDescending {
calculateFamiliarity(
query,
it.name.lowercase().trim()
)
}.toMutableList()
} else {
filteredFiles.sortedByDescending {
it.lastModified() it.lastModified()
}.toMutableList() }.toMutableList()
}
emit( emit(
Resource.Success( Resource.Success(

View file

@ -1,6 +1,7 @@
package ua.acclorite.book_story.domain.repository package ua.acclorite.book_story.domain.repository
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.History 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.model.StringWithId
import ua.acclorite.book_story.domain.util.CoverImage import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.domain.util.Resource import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.presentation.data.MainState
import java.io.File import java.io.File
interface BookRepository { interface BookRepository {
@ -55,6 +57,8 @@ interface BookRepository {
value: T value: T
) )
suspend fun getAllSettings(scope: CoroutineScope): MainState
suspend fun getFilesFromDevice(query: String = ""): Flow<Resource<List<File>>> suspend fun getFilesFromDevice(query: String = ""): Flow<Resource<List<File>>>
suspend fun getBooksFromFiles(files: List<File>): List<NullableBook> suspend fun getBooksFromFiles(files: List<File>): List<NullableBook>

View file

@ -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)
}
}

View file

@ -9,6 +9,7 @@ object DataStoreConstants {
val LANGUAGE = stringPreferencesKey("language") val LANGUAGE = stringPreferencesKey("language")
val THEME = stringPreferencesKey("theme") val THEME = stringPreferencesKey("theme")
val DARK_THEME = stringPreferencesKey("dark_theme") val DARK_THEME = stringPreferencesKey("dark_theme")
val PURE_DARK = stringPreferencesKey("pure_dark")
val THEME_CONTRAST = stringPreferencesKey("theme_contrast") val THEME_CONTRAST = stringPreferencesKey("theme_contrast")
val SHOW_START_SCREEN = booleanPreferencesKey("guide") val SHOW_START_SCREEN = booleanPreferencesKey("guide")
val BACKGROUND_COLOR = longPreferencesKey("background_color") val BACKGROUND_COLOR = longPreferencesKey("background_color")

View file

@ -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<TopAppBarScrollBehavior, LazyListState> {
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
}

View file

@ -10,6 +10,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -31,6 +32,9 @@ fun CustomIconButton(
CustomTooltip(text = stringResource(id = contentDescription)) { CustomTooltip(text = stringResource(id = contentDescription)) {
IconButton( IconButton(
enabled = enabled && !isClicked, enabled = enabled && !isClicked,
modifier = Modifier.focusProperties {
canFocus = false
},
onClick = { onClick = {
if (disableOnClick) { if (disableOnClick) {
isClicked = true isClicked = true

View file

@ -7,13 +7,15 @@ import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.rememberTooltipState import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun CustomTooltip(text: String, content: @Composable () -> Unit) { fun CustomTooltip(text: String, padding: Dp = 14.dp, content: @Composable () -> Unit) {
TooltipBox( TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(12.dp), positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(padding),
focusable = false,
tooltip = { tooltip = {
PlainTooltip { PlainTooltip {
Text(text = text) Text(text = text)

View file

@ -34,32 +34,38 @@ fun BottomNavigationBar(
} }
NavigationBar { NavigationBar {
BottomNavigationBarItem( BottomNavigationBarItem(
item = NavigationItem( item = NavigationItem(
stringResource(id = R.string.library_screen), stringResource(id = R.string.library_screen),
selectedIcon = painterResource(id = R.drawable.library_screen_filled), selectedIcon = painterResource(id = R.drawable.library_screen_filled),
unselectedIcon = painterResource(id = R.drawable.library_screen_outlined) unselectedIcon = painterResource(id = R.drawable.library_screen_outlined)
), ),
tooltipText = stringResource(id = R.string.library_content_desc),
isSelected = currentScreen == Screen.LIBRARY isSelected = currentScreen == Screen.LIBRARY
) { ) {
navigator.navigate(Screen.LIBRARY, false) navigator.navigate(Screen.LIBRARY, false)
} }
BottomNavigationBarItem( BottomNavigationBarItem(
item = NavigationItem( item = NavigationItem(
stringResource(id = R.string.history_screen), stringResource(id = R.string.history_screen),
selectedIcon = painterResource(id = R.drawable.history_screen_filled), selectedIcon = painterResource(id = R.drawable.history_screen_filled),
unselectedIcon = painterResource(id = R.drawable.history_screen_outlined) unselectedIcon = painterResource(id = R.drawable.history_screen_outlined)
), ),
tooltipText = stringResource(id = R.string.history_content_desc),
isSelected = currentScreen == Screen.HISTORY isSelected = currentScreen == Screen.HISTORY
) { ) {
navigator.navigate(Screen.HISTORY, false) navigator.navigate(Screen.HISTORY, false)
} }
BottomNavigationBarItem( BottomNavigationBarItem(
item = NavigationItem( item = NavigationItem(
stringResource(id = R.string.browse_screen), stringResource(id = R.string.browse_screen),
selectedIcon = painterResource(id = R.drawable.browse_screen_filled), selectedIcon = painterResource(id = R.drawable.browse_screen_filled),
unselectedIcon = painterResource(id = R.drawable.browse_screen_outlined) unselectedIcon = painterResource(id = R.drawable.browse_screen_outlined)
), ),
tooltipText = stringResource(id = R.string.browse_content_desc),
isSelected = currentScreen == Screen.BROWSE isSelected = currentScreen == Screen.BROWSE
) { ) {
navigator.navigate(Screen.BROWSE, false) navigator.navigate(Screen.BROWSE, false)

View file

@ -11,6 +11,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.domain.model.NavigationItem import ua.acclorite.book_story.domain.model.NavigationItem
import ua.acclorite.book_story.presentation.components.CustomTooltip
/** /**
* Bottom Navigation Bar Item, uses default [NavigationBarItem]. * Bottom Navigation Bar Item, uses default [NavigationBarItem].
@ -19,6 +20,7 @@ import ua.acclorite.book_story.domain.model.NavigationItem
fun RowScope.BottomNavigationBarItem( fun RowScope.BottomNavigationBarItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
item: NavigationItem, item: NavigationItem,
tooltipText: String,
isSelected: Boolean, isSelected: Boolean,
onClick: () -> Unit onClick: () -> Unit
) { ) {
@ -27,22 +29,33 @@ fun RowScope.BottomNavigationBarItem(
else item.unselectedIcon else item.unselectedIcon
} }
NavigationBarItem( NavigationBarItem(
label = { label = {
CustomTooltip(
text = tooltipText,
padding = 64.dp
) {
Text( Text(
text = item.title, text = item.title,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
}
}, },
selected = isSelected, selected = isSelected,
onClick = { onClick() }, onClick = { onClick() },
icon = { icon = {
CustomTooltip(
text = tooltipText,
padding = 32.dp
) {
Icon( Icon(
painter = icon, painter = icon,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp)
) )
}
}, },
modifier = modifier modifier = modifier
) )

View file

@ -76,6 +76,7 @@ fun BoxScope.CustomNavigationRail(
selectedIcon = painterResource(id = R.drawable.library_screen_filled), selectedIcon = painterResource(id = R.drawable.library_screen_filled),
unselectedIcon = painterResource(id = R.drawable.library_screen_outlined) unselectedIcon = painterResource(id = R.drawable.library_screen_outlined)
), ),
tooltipText = stringResource(id = R.string.library_content_desc),
isSelected = currentScreen == Screen.LIBRARY isSelected = currentScreen == Screen.LIBRARY
) { ) {
navigator.navigate(Screen.LIBRARY, false) navigator.navigate(Screen.LIBRARY, false)
@ -86,6 +87,7 @@ fun BoxScope.CustomNavigationRail(
selectedIcon = painterResource(id = R.drawable.history_screen_filled), selectedIcon = painterResource(id = R.drawable.history_screen_filled),
unselectedIcon = painterResource(id = R.drawable.history_screen_outlined) unselectedIcon = painterResource(id = R.drawable.history_screen_outlined)
), ),
tooltipText = stringResource(id = R.string.history_content_desc),
isSelected = currentScreen == Screen.HISTORY isSelected = currentScreen == Screen.HISTORY
) { ) {
navigator.navigate(Screen.HISTORY, false) navigator.navigate(Screen.HISTORY, false)
@ -96,6 +98,7 @@ fun BoxScope.CustomNavigationRail(
selectedIcon = painterResource(id = R.drawable.browse_screen_filled), selectedIcon = painterResource(id = R.drawable.browse_screen_filled),
unselectedIcon = painterResource(id = R.drawable.browse_screen_outlined) unselectedIcon = painterResource(id = R.drawable.browse_screen_outlined)
), ),
tooltipText = stringResource(id = R.string.browse_content_desc),
isSelected = currentScreen == Screen.BROWSE isSelected = currentScreen == Screen.BROWSE
) { ) {
navigator.navigate(Screen.BROWSE, false) navigator.navigate(Screen.BROWSE, false)

View file

@ -10,6 +10,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.domain.model.NavigationItem import ua.acclorite.book_story.domain.model.NavigationItem
import ua.acclorite.book_story.presentation.components.CustomTooltip
/** /**
* Custom Navigation Rail item. * Custom Navigation Rail item.
@ -18,6 +19,7 @@ import ua.acclorite.book_story.domain.model.NavigationItem
fun CustomNavigationRailItem( fun CustomNavigationRailItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
item: NavigationItem, item: NavigationItem,
tooltipText: String,
isSelected: Boolean, isSelected: Boolean,
onClick: () -> Unit onClick: () -> Unit
) { ) {
@ -28,20 +30,30 @@ fun CustomNavigationRailItem(
NavigationRailItem( NavigationRailItem(
label = { label = {
CustomTooltip(
text = tooltipText,
padding = 48.dp
) {
Text( Text(
text = item.title, text = item.title,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
}
}, },
selected = isSelected, selected = isSelected,
onClick = { onClick() }, onClick = { onClick() },
icon = { icon = {
CustomTooltip(
text = tooltipText,
padding = 16.dp
) {
Icon( Icon(
painter = icon, painter = icon,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp)
) )
}
}, },
modifier = modifier modifier = modifier
) )

View file

@ -10,9 +10,3 @@ fun String.removeTrailingZero(): String {
fun Double.removeDigits(digits: Int) = "%.${digits}f".format(this).replace(",", ".") 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
}

View file

@ -4,6 +4,7 @@ sealed class MainEvent {
data class OnChangeLanguage(val lang: String) : MainEvent() data class OnChangeLanguage(val lang: String) : MainEvent()
data class OnChangeTheme(val theme: String) : MainEvent() data class OnChangeTheme(val theme: String) : MainEvent()
data class OnChangeDarkTheme(val darkTheme: 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 OnChangeThemeContrast(val themeContrast: String) : MainEvent()
data class OnChangeFontFamily(val fontFamily: String) : MainEvent() data class OnChangeFontFamily(val fontFamily: String) : MainEvent()
data class OnChangeFontStyle(val fontStyle: Boolean) : MainEvent() data class OnChangeFontStyle(val fontStyle: Boolean) : MainEvent()

View file

@ -1,11 +1,21 @@
package ua.acclorite.book_story.presentation.data package ua.acclorite.book_story.presentation.data
import android.os.Build
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.runtime.Immutable 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.DarkTheme
import ua.acclorite.book_story.presentation.ui.PureDark
import ua.acclorite.book_story.presentation.ui.Theme import ua.acclorite.book_story.presentation.ui.Theme
import ua.acclorite.book_story.presentation.ui.ThemeContrast 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. * 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 language: String? = null,
val theme: Theme? = null, val theme: Theme? = null,
val darkTheme: DarkTheme? = null, val darkTheme: DarkTheme? = null,
val pureDark: PureDark? = null,
val themeContrast: ThemeContrast? = null, val themeContrast: ThemeContrast? = null,
val fontFamily: String? = null, val fontFamily: String? = null,
val isItalic: Boolean? = null, val isItalic: Boolean? = null,
@ -34,6 +45,8 @@ data class MainState(
// String // String
darkTheme = DarkTheme.valueOf(parcel.readString() ?: DarkTheme.FOLLOW_SYSTEM.name), darkTheme = DarkTheme.valueOf(parcel.readString() ?: DarkTheme.FOLLOW_SYSTEM.name),
// String // String
pureDark = PureDark.valueOf(parcel.readString() ?: PureDark.OFF.name),
// String
themeContrast = ThemeContrast.valueOf(parcel.readString() ?: ThemeContrast.STANDARD.name), themeContrast = ThemeContrast.valueOf(parcel.readString() ?: ThemeContrast.STANDARD.name),
// String // String
fontFamily = parcel.readString(), fontFamily = parcel.readString(),
@ -64,6 +77,8 @@ data class MainState(
parcel.writeString(theme?.name) parcel.writeString(theme?.name)
// Dark Theme // Dark Theme
parcel.writeString(darkTheme?.name) parcel.writeString(darkTheme?.name)
// Pure Dark
parcel.writeString(pureDark?.name)
// Contrast Level // Contrast Level
parcel.writeString(themeContrast?.name) parcel.writeString(themeContrast?.name)
// Font Family // Font Family
@ -106,5 +121,79 @@ data class MainState(
override fun newArray(size: Int): Array<MainState?> { override fun newArray(size: Int): Array<MainState?> {
return arrayOfNulls(size) return arrayOfNulls(size)
} }
/**
* Initializes [MainState] by given [Map].
*/
fun initialize(data: Map<String, Any>): 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
)
}
}
} }
} }

View file

@ -1,7 +1,5 @@
package ua.acclorite.book_story.presentation.data package ua.acclorite.book_story.presentation.data
import android.os.Build
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
@ -9,23 +7,21 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ua.acclorite.book_story.domain.use_case.ChangeLanguage 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.use_case.SetDatastore
import ua.acclorite.book_story.domain.util.Constants import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.domain.util.DataStoreConstants import ua.acclorite.book_story.domain.util.DataStoreConstants
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel 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.toDarkTheme
import ua.acclorite.book_story.presentation.ui.toPureDark
import ua.acclorite.book_story.presentation.ui.toTheme import ua.acclorite.book_story.presentation.ui.toTheme
import ua.acclorite.book_story.presentation.ui.toThemeContrast import ua.acclorite.book_story.presentation.ui.toThemeContrast
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -35,14 +31,17 @@ import javax.inject.Inject
class MainViewModel @Inject constructor( class MainViewModel @Inject constructor(
private val stateHandle: SavedStateHandle, private val stateHandle: SavedStateHandle,
private val getDatastore: GetDatastore,
private val setDatastore: SetDatastore, private val setDatastore: SetDatastore,
private val changeLanguage: ChangeLanguage, private val changeLanguage: ChangeLanguage,
private val getAllSettings: GetAllSettings
) : ViewModel() { ) : ViewModel() {
private val _isReady = MutableStateFlow(false) private val _isReady = MutableStateFlow(false)
val isReady = _isReady.asStateFlow() val isReady = _isReady.asStateFlow()
private val isSettingsReady = MutableStateFlow(false)
private val isViewModelReady = MutableStateFlow(false)
private val _state: MutableStateFlow<MainState> = MutableStateFlow( private val _state: MutableStateFlow<MainState> = MutableStateFlow(
stateHandle[Constants.MAIN_STATE] ?: MainState() stateHandle[Constants.MAIN_STATE] ?: MainState()
) )
@ -53,7 +52,7 @@ class MainViewModel @Inject constructor(
is MainEvent.OnChangeLanguage -> { is MainEvent.OnChangeLanguage -> {
viewModelScope.launch(Dispatchers.Main) { viewModelScope.launch(Dispatchers.Main) {
changeLanguage.execute(event.lang) changeLanguage.execute(event.lang)
_state.updateWithSavedHandle { updateStateWithSavedHandle {
it.copy( it.copy(
language = event.lang language = event.lang
) )
@ -64,7 +63,7 @@ class MainViewModel @Inject constructor(
is MainEvent.OnChangeDarkTheme -> { is MainEvent.OnChangeDarkTheme -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.DARK_THEME, event.darkTheme) setDatastore.execute(DataStoreConstants.DARK_THEME, event.darkTheme)
_state.updateWithSavedHandle { updateStateWithSavedHandle {
it.copy( it.copy(
darkTheme = event.darkTheme.toDarkTheme() 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 -> { is MainEvent.OnChangeThemeContrast -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.THEME_CONTRAST, event.themeContrast) setDatastore.execute(DataStoreConstants.THEME_CONTRAST, event.themeContrast)
_state.updateWithSavedHandle { updateStateWithSavedHandle {
it.copy( it.copy(
themeContrast = event.themeContrast.toThemeContrast() themeContrast = event.themeContrast.toThemeContrast()
) )
@ -86,7 +96,7 @@ class MainViewModel @Inject constructor(
is MainEvent.OnChangeTheme -> { is MainEvent.OnChangeTheme -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.THEME, event.theme) setDatastore.execute(DataStoreConstants.THEME, event.theme)
_state.updateWithSavedHandle { updateStateWithSavedHandle {
it.copy( it.copy(
theme = event.theme.toTheme() theme = event.theme.toTheme()
) )
@ -97,7 +107,7 @@ class MainViewModel @Inject constructor(
is MainEvent.OnChangeFontFamily -> { is MainEvent.OnChangeFontFamily -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.FONT, event.fontFamily) setDatastore.execute(DataStoreConstants.FONT, event.fontFamily)
_state.updateWithSavedHandle { updateStateWithSavedHandle {
it.copy( it.copy(
fontFamily = Constants.FONTS.find { font -> font.id == event.fontFamily }?.id fontFamily = Constants.FONTS.find { font -> font.id == event.fontFamily }?.id
?: Constants.FONTS[0].id ?: Constants.FONTS[0].id
@ -109,7 +119,7 @@ class MainViewModel @Inject constructor(
is MainEvent.OnChangeFontStyle -> { is MainEvent.OnChangeFontStyle -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.IS_ITALIC, event.fontStyle) setDatastore.execute(DataStoreConstants.IS_ITALIC, event.fontStyle)
_state.updateWithSavedHandle { updateStateWithSavedHandle {
it.copy( it.copy(
isItalic = event.fontStyle isItalic = event.fontStyle
) )
@ -120,7 +130,7 @@ class MainViewModel @Inject constructor(
is MainEvent.OnChangeFontSize -> { is MainEvent.OnChangeFontSize -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.FONT_SIZE, event.fontSize) setDatastore.execute(DataStoreConstants.FONT_SIZE, event.fontSize)
_state.updateWithSavedHandle { updateStateWithSavedHandle {
it.copy( it.copy(
fontSize = event.fontSize fontSize = event.fontSize
) )
@ -131,7 +141,7 @@ class MainViewModel @Inject constructor(
is MainEvent.OnChangeLineHeight -> { is MainEvent.OnChangeLineHeight -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.LINE_HEIGHT, event.lineHeight) setDatastore.execute(DataStoreConstants.LINE_HEIGHT, event.lineHeight)
_state.updateWithSavedHandle { updateStateWithSavedHandle {
it.copy( it.copy(
lineHeight = event.lineHeight lineHeight = event.lineHeight
) )
@ -142,7 +152,7 @@ class MainViewModel @Inject constructor(
is MainEvent.OnChangeParagraphHeight -> { is MainEvent.OnChangeParagraphHeight -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.PARAGRAPH_HEIGHT, event.paragraphHeight) setDatastore.execute(DataStoreConstants.PARAGRAPH_HEIGHT, event.paragraphHeight)
_state.updateWithSavedHandle { updateStateWithSavedHandle {
it.copy( it.copy(
paragraphHeight = event.paragraphHeight paragraphHeight = event.paragraphHeight
) )
@ -153,7 +163,7 @@ class MainViewModel @Inject constructor(
is MainEvent.OnChangeParagraphIndentation -> { is MainEvent.OnChangeParagraphIndentation -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.PARAGRAPH_INDENTATION, event.bool) setDatastore.execute(DataStoreConstants.PARAGRAPH_INDENTATION, event.bool)
_state.updateWithSavedHandle { updateStateWithSavedHandle {
it.copy( it.copy(
paragraphIndentation = event.bool paragraphIndentation = event.bool
) )
@ -164,7 +174,7 @@ class MainViewModel @Inject constructor(
is MainEvent.OnChangeBackgroundColor -> { is MainEvent.OnChangeBackgroundColor -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.BACKGROUND_COLOR, event.color) setDatastore.execute(DataStoreConstants.BACKGROUND_COLOR, event.color)
_state.updateWithSavedHandle { updateStateWithSavedHandle {
it.copy( it.copy(
backgroundColor = event.color backgroundColor = event.color
) )
@ -175,7 +185,7 @@ class MainViewModel @Inject constructor(
is MainEvent.OnChangeFontColor -> { is MainEvent.OnChangeFontColor -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.FONT_COLOR, event.color) setDatastore.execute(DataStoreConstants.FONT_COLOR, event.color)
_state.updateWithSavedHandle { updateStateWithSavedHandle {
it.copy( it.copy(
fontColor = event.color fontColor = event.color
) )
@ -186,7 +196,7 @@ class MainViewModel @Inject constructor(
is MainEvent.OnChangeShowStartScreen -> { is MainEvent.OnChangeShowStartScreen -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.SHOW_START_SCREEN, event.bool) setDatastore.execute(DataStoreConstants.SHOW_START_SCREEN, event.bool)
_state.updateWithSavedHandle { updateStateWithSavedHandle {
it.copy( it.copy(
showStartScreen = event.bool showStartScreen = event.bool
) )
@ -199,204 +209,54 @@ class MainViewModel @Inject constructor(
fun init( fun init(
libraryViewModel: LibraryViewModel, libraryViewModel: LibraryViewModel,
) { ) {
val isViewModelsReady = combine( viewModelScope.launch(Dispatchers.Main) {
libraryViewModel.isReady, val settings = getAllSettings.execute(viewModelScope)
) { values ->
values.all { it } // All additional execution
changeLanguage.execute(settings.language!!)
updateStateWithSavedHandle {
settings
}
isSettingsReady.update { true }
} }
val isDataReady = combine( viewModelScope.launch(Dispatchers.IO) {
_state libraryViewModel.isReady.collectLatest { ready ->
) { isViewModelReady.update {
val value = it.first() ready
}
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( val isReady = combine(
isViewModelsReady, isViewModelReady,
isDataReady isSettingsReady
) { values -> ) { values ->
values.all { it } values.all { it }
} }
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
isReady.collect { bool -> isReady.first { bool ->
if (bool) { if (bool) {
_isReady.update { _isReady.update {
true true
} }
return@collect }
bool
} }
} }
} }
// 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"
}
)
.first {
onEvent(MainEvent.OnChangeLanguage(it))
it.isNotBlank()
}
}
// 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 <T> MutableStateFlow<T>.updateWithSavedHandle( private fun updateStateWithSavedHandle(
const: String = Constants.MAIN_STATE, function: (MainState) -> MainState
function: (T) -> T
) { ) {
val nextValue = function(value) _state.update {
update { stateHandle[Constants.MAIN_STATE] = function(it)
stateHandle[const] = nextValue function(it)
nextValue
} }
} }
} }

View file

@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -32,6 +31,7 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.GoBackButton 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.Navigator
import ua.acclorite.book_story.presentation.screens.about.components.AboutItem import ua.acclorite.book_story.presentation.screens.about.components.AboutItem
import ua.acclorite.book_story.presentation.screens.about.data.AboutEvent import ua.acclorite.book_story.presentation.screens.about.data.AboutEvent
@ -44,17 +44,12 @@ fun AboutScreen(
navigator: Navigator navigator: Navigator
) { ) {
val context = LocalContext.current val context = LocalContext.current
val listState = rememberLazyListState() val scrollState = TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
canScroll = {
listState.canScrollForward || listState.canScrollBackward
}
)
Scaffold( Scaffold(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection) .nestedScroll(scrollState.first.nestedScrollConnection)
.windowInsetsPadding(WindowInsets.navigationBars), .windowInsetsPadding(WindowInsets.navigationBars),
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
topBar = { topBar = {
@ -65,7 +60,7 @@ fun AboutScreen(
navigationIcon = { navigationIcon = {
GoBackButton(navigator = navigator) GoBackButton(navigator = navigator)
}, },
scrollBehavior = scrollBehavior, scrollBehavior = scrollState.first,
colors = TopAppBarDefaults.largeTopAppBarColors( colors = TopAppBarDefaults.largeTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer
@ -77,7 +72,7 @@ fun AboutScreen(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.padding(top = paddingValues.calculateTopPadding()), .padding(top = paddingValues.calculateTopPadding()),
state = listState state = scrollState.second
) { ) {
item { item {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))

View file

@ -93,7 +93,7 @@ fun BookInfoScreen(
val listState = rememberLazyListState() val listState = rememberLazyListState()
val snackbarState = remember { SnackbarHostState() } val snackbarState = remember { SnackbarHostState() }
val refreshState = rememberPullRefreshState( val refreshState = rememberPullRefreshState(
refreshing = state.isRefreshing, refreshing = state.isRefreshing || state.isLoadingUpdate,
onRefresh = { onRefresh = {
viewModel.onEvent( viewModel.onEvent(
BookInfoEvent.OnLoadUpdate( BookInfoEvent.OnLoadUpdate(
@ -161,7 +161,9 @@ fun BookInfoScreen(
isTopBarScrolled = null, isTopBarScrolled = null,
content1NavigationIcon = { content1NavigationIcon = {
GoBackButton(navigator = navigator, enabled = !state.isRefreshing) GoBackButton(navigator = navigator, enabled = !state.isRefreshing) {
viewModel.onEvent(BookInfoEvent.OnCancelUpdate)
}
}, },
content1Title = { content1Title = {
DefaultTransition(visible = firstVisibleItemIndex > 0) { DefaultTransition(visible = firstVisibleItemIndex > 0) {
@ -179,7 +181,7 @@ fun BookInfoScreen(
icon = Icons.Default.Refresh, icon = Icons.Default.Refresh,
contentDescription = R.string.refresh_book_content_desc, contentDescription = R.string.refresh_book_content_desc,
disableOnClick = false, disableOnClick = false,
enabled = !state.isRefreshing enabled = !state.isRefreshing && !state.isLoadingUpdate
) { ) {
viewModel.onEvent( viewModel.onEvent(
BookInfoEvent.OnLoadUpdate( BookInfoEvent.OnLoadUpdate(
@ -319,7 +321,7 @@ fun BookInfoScreen(
) )
PullRefreshIndicator( PullRefreshIndicator(
state.isRefreshing, state.isRefreshing || state.isLoadingUpdate,
refreshState, refreshState,
Modifier Modifier
.align(Alignment.TopCenter) .align(Alignment.TopCenter)
@ -332,6 +334,7 @@ fun BookInfoScreen(
BackHandler { BackHandler {
if (!state.isRefreshing) { if (!state.isRefreshing) {
viewModel.onEvent(BookInfoEvent.OnCancelUpdate)
navigator.navigateBack() navigator.navigateBack()
} }
} }

View file

@ -126,7 +126,7 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
fontFamily = MaterialTheme.typography.headlineSmall.fontFamily fontFamily = MaterialTheme.typography.headlineSmall.fontFamily
), ),
onValueChange = { onValueChange = {
if (it.length < 80) { if (it.length < 100 || it.length < state.titleValue.length) {
viewModel.onEvent(BookInfoEvent.OnTitleValueChange(it)) viewModel.onEvent(BookInfoEvent.OnTitleValueChange(it))
} }
}, },

View file

@ -61,6 +61,8 @@ sealed class BookInfoEvent {
val textUpdated: Boolean, val textUpdated: Boolean,
) : BookInfoEvent() ) : BookInfoEvent()
data object OnCancelUpdate : BookInfoEvent()
data class OnConfirmUpdate( data class OnConfirmUpdate(
val snackbarState: SnackbarHostState, val snackbarState: SnackbarHostState,
val context: Context, val context: Context,

View file

@ -9,6 +9,7 @@ import ua.acclorite.book_story.domain.util.Constants
data class BookInfoState( data class BookInfoState(
val book: Book = Constants.EMPTY_BOOK, val book: Book = Constants.EMPTY_BOOK,
val isLoadingUpdate: Boolean = false,
val isRefreshing: Boolean = false, val isRefreshing: Boolean = false,
val showConfirmUpdateDialog: Boolean = false, val showConfirmUpdateDialog: Boolean = false,
val updatedBook: Book? = null, val updatedBook: Book? = null,

View file

@ -52,6 +52,7 @@ class BookInfoViewModel @Inject constructor(
val state = _state.asStateFlow() val state = _state.asStateFlow()
private var job: Job? = null private var job: Job? = null
private var job2: Job? = null
fun onEvent(event: BookInfoEvent) { fun onEvent(event: BookInfoEvent) {
when (event) { when (event) {
@ -290,14 +291,17 @@ class BookInfoViewModel @Inject constructor(
} }
is BookInfoEvent.OnLoadUpdate -> { is BookInfoEvent.OnLoadUpdate -> {
viewModelScope.launch(Dispatchers.IO) { onEvent(BookInfoEvent.OnCancelUpdate)
job2 = viewModelScope.launch(Dispatchers.IO) {
_state.update { _state.update {
it.copy( it.copy(
isRefreshing = true, isLoadingUpdate = true,
editTitle = false editTitle = false
) )
} }
yield()
if (_state.value.book.file == null) { if (_state.value.book.file == null) {
onEvent( onEvent(
BookInfoEvent.OnShowSnackbar( BookInfoEvent.OnShowSnackbar(
@ -323,13 +327,15 @@ class BookInfoViewModel @Inject constructor(
delay(500) delay(500)
_state.update { _state.update {
it.copy( it.copy(
isRefreshing = false isLoadingUpdate = false
) )
} }
return@launch return@launch
} }
yield()
val nullableBook = getBookFromFile.execute(_state.value.book.file!!) val nullableBook = getBookFromFile.execute(_state.value.book.file!!)
yield()
if (nullableBook is NullableBook.Null) { if (nullableBook is NullableBook.Null) {
onEvent( onEvent(
BookInfoEvent.OnShowSnackbar( BookInfoEvent.OnShowSnackbar(
@ -352,11 +358,12 @@ class BookInfoViewModel @Inject constructor(
delay(500) delay(500)
_state.update { _state.update {
it.copy( it.copy(
isRefreshing = false isLoadingUpdate = false
) )
} }
return@launch return@launch
} }
yield()
val updatedBook = nullableBook.book?.first ?: return@launch val updatedBook = nullableBook.book?.first ?: return@launch
val book = _state.value.book val book = _state.value.book
@ -383,6 +390,7 @@ class BookInfoViewModel @Inject constructor(
textUpdated = true textUpdated = true
} }
yield()
if (!authorUpdated && !descriptionUpdated && !textUpdated) { if (!authorUpdated && !descriptionUpdated && !textUpdated) {
onEvent( onEvent(
BookInfoEvent.OnShowSnackbar( BookInfoEvent.OnShowSnackbar(
@ -395,12 +403,13 @@ class BookInfoViewModel @Inject constructor(
delay(500) delay(500)
_state.update { _state.update {
it.copy( it.copy(
isRefreshing = false isLoadingUpdate = false
) )
} }
return@launch return@launch
} }
yield()
onEvent( onEvent(
BookInfoEvent.OnShowConfirmUpdateDialog( BookInfoEvent.OnShowConfirmUpdateDialog(
updatedBook = updatedBook, updatedBook = updatedBook,
@ -412,7 +421,7 @@ class BookInfoViewModel @Inject constructor(
_state.update { _state.update {
it.copy( 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 -> { is BookInfoEvent.OnNavigateToReaderScreen -> {
viewModelScope.launch { viewModelScope.launch {
onEvent(BookInfoEvent.OnCancelUpdate)
_state.value.book.id.let { _state.value.book.id.let {
insertHistory.execute( insertHistory.execute(
listOf( listOf(

View file

@ -6,7 +6,6 @@ import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
@ -71,7 +70,6 @@ import ua.acclorite.book_story.presentation.ui.Transitions
@OptIn( @OptIn(
ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
ExperimentalPermissionsApi::class, ExperimentalPermissionsApi::class,
ExperimentalFoundationApi::class,
ExperimentalMaterialApi::class ExperimentalMaterialApi::class
) )
@Composable @Composable
@ -249,7 +247,7 @@ fun BrowseScreen(
) { selectableFile -> ) { selectableFile ->
BrowseFileItem( BrowseFileItem(
file = selectableFile, file = selectableFile,
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
hasSelectedFiles = state.selectableFiles.any { it.second }, hasSelectedFiles = state.selectableFiles.any { it.second },
onLongClick = { onLongClick = {
Toast.makeText( Toast.makeText(

View file

@ -4,11 +4,11 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -18,34 +18,38 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.GoBackButton 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.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 import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(
ExperimentalMaterial3Api::class
)
@Composable @Composable
fun HelpScreen( fun HelpScreen(
viewModel: HelpViewModel = hiltViewModel(), viewModel: HelpViewModel = hiltViewModel(),
navigator: Navigator navigator: Navigator
) { ) {
val context = LocalContext.current val scrollState = TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState()
val listState = rememberLazyListState()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
canScroll = {
listState.canScrollForward || listState.canScrollBackward
}
)
Scaffold( Scaffold(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection) .nestedScroll(scrollState.first.nestedScrollConnection)
.windowInsetsPadding(WindowInsets.navigationBars), .windowInsetsPadding(WindowInsets.navigationBars),
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
topBar = { topBar = {
@ -56,7 +60,7 @@ fun HelpScreen(
navigationIcon = { navigationIcon = {
GoBackButton(navigator = navigator) GoBackButton(navigator = navigator)
}, },
scrollBehavior = scrollBehavior, scrollBehavior = scrollState.first,
colors = TopAppBarDefaults.largeTopAppBarColors( colors = TopAppBarDefaults.largeTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer
@ -67,19 +71,88 @@ fun HelpScreen(
LazyColumn( LazyColumn(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.padding(top = paddingValues.calculateTopPadding()) .padding(top = paddingValues.calculateTopPadding(), start = 18.dp, end = 18.dp)
.padding(horizontal = 18.dp), .imePadding(),
state = listState 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)
)
}
} }
} }
} }

View file

@ -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()
}

View file

@ -1,79 +1,69 @@
package ua.acclorite.book_story.presentation.screens.help.components package ua.acclorite.book_story.presentation.screens.help.components
import android.widget.Toast
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column 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.Spacer
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size 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.Icons
import androidx.compose.material.icons.outlined.Info import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource 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 androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R 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 @Composable
fun HelpClickableNote() { fun LazyItemScope.HelpClickableNote(
val context = LocalContext.current text: AnnotatedString,
tags: List<String> = emptyList(),
Column { isNoteFullyShown: Boolean,
onIconClick: () -> Unit,
onTagClick: (String) -> Unit
) {
Column(Modifier.animateItem()) {
CustomTooltip(text = stringResource(R.string.note_content_desc)) {
Icon( Icon(
imageVector = Icons.Outlined.Info, imageVector = Icons.Outlined.Info,
contentDescription = stringResource(id = R.string.note_content_desc),
modifier = Modifier modifier = Modifier
.size(20.dp), .size(20.dp)
.clickable(interactionSource = null, indication = null) {
onIconClick()
},
contentDescription = stringResource(R.string.note_content_desc),
tint = MaterialTheme.colorScheme.onSurfaceVariant tint = MaterialTheme.colorScheme.onSurfaceVariant
) )
}
SlidingTransition(visible = isNoteFullyShown) {
Column {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
FlowRow(verticalArrangement = Arrangement.spacedBy(1.dp)) { ClickableText(
Text( text = text,
stringResource(id = R.string.clickable_note_1) + " ", style = MaterialTheme.typography.bodySmall.copy(
style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant
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,
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)
) )
) { offset ->
tags.forEach { tag ->
text.getStringAnnotations(tag = tag, start = offset, end = offset)
.firstOrNull()?.let {
onTagClick(tag)
}
}
}
}
} }
} }
} }

View file

@ -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<String>,
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)
)
}
}

View file

@ -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)
}
}
}
)
}

View file

@ -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()
}
}
}
)
}

View file

@ -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)
}
}
}
)
}

View file

@ -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)
}
}
}
)
}

View file

@ -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)
}
}
}
)
}

View file

@ -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 = {}
)
}

View file

@ -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)
}
}
}
)
}

View file

@ -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)
}
}
}
)
}

View file

@ -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)
}
}
}
)
}

View file

@ -3,6 +3,11 @@ package ua.acclorite.book_story.presentation.screens.help.data
import android.content.Context import android.content.Context
sealed class HelpEvent { sealed class HelpEvent {
data class OnSearchInWeb(
val context: Context,
val noAppsFound: () -> Unit
) : HelpEvent()
data class OnNavigateToBrowserPage( data class OnNavigateToBrowserPage(
val page: String, val page: String,
val context: Context, val context: Context,

View file

@ -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,
)

View file

@ -1,10 +1,14 @@
package ua.acclorite.book_story.presentation.screens.help.data package ua.acclorite.book_story.presentation.screens.help.data
import android.app.SearchManager
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel 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 kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ -13,6 +17,9 @@ class HelpViewModel @Inject constructor(
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow(HelpState())
val state = _state.asStateFlow()
fun onEvent(event: HelpEvent) { fun onEvent(event: HelpEvent) {
when (event) { when (event) {
is HelpEvent.OnNavigateToBrowserPage -> { is HelpEvent.OnNavigateToBrowserPage -> {
@ -30,6 +37,40 @@ class HelpViewModel @Inject constructor(
event.noAppsFound() 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)
} }
} }
} }

View file

@ -4,7 +4,6 @@ import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@ -68,7 +67,7 @@ import ua.acclorite.book_story.presentation.ui.Transitions
@OptIn( @OptIn(
ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
ExperimentalMaterialApi::class, ExperimentalFoundationApi::class ExperimentalMaterialApi::class
) )
@Composable @Composable
fun HistoryScreen( fun HistoryScreen(
@ -214,7 +213,7 @@ fun HistoryScreen(
} }
CategoryTitle( CategoryTitle(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
title = when (groupedHistory.title) { title = when (groupedHistory.title) {
"today" -> stringResource(id = R.string.today) "today" -> stringResource(id = R.string.today)
"yesterday" -> stringResource(id = R.string.yesterday) "yesterday" -> stringResource(id = R.string.yesterday)
@ -230,7 +229,7 @@ fun HistoryScreen(
groupedHistory.history, key = { it.id } groupedHistory.history, key = { it.id }
) { ) {
HistoryItem( HistoryItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItem(),
history = it, history = it,
isOnClickEnabled = !state.isRefreshing, isOnClickEnabled = !state.isRefreshing,
onBodyClick = { onBodyClick = {

View file

@ -347,6 +347,7 @@ class LibraryViewModel @Inject constructor(
query: String = if (_state.value.showSearch) _state.value.searchQuery else "" query: String = if (_state.value.showSearch) _state.value.searchQuery else ""
) { ) {
val books = getBooks.execute(query).map { book -> Pair(book, false) } val books = getBooks.execute(query).map { book -> Pair(book, false) }
val categorizedBooks = books.groupBy { val categorizedBooks = books.groupBy {
it.first.category it.first.category
}.toList().map { }.toList().map {

View file

@ -89,7 +89,11 @@ fun ReaderScreen(
val listState = rememberLazyListState() val listState = rememberLazyListState()
val state by viewModel.state.collectAsState() 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 { val nestedScrollConnection = remember {
object : NestedScrollConnection { object : NestedScrollConnection {
override fun onPostScroll( override fun onPostScroll(
@ -129,8 +133,8 @@ fun ReaderScreen(
} }
) )
} }
LaunchedEffect(canScrollForward) { LaunchedEffect(canScroll) {
if (!canScrollForward && !state.showMenu && !loading) { if (!canScroll && !state.showMenu && !loading) {
viewModel.onEvent(ReaderEvent.OnShowHideMenu(context = context)) viewModel.onEvent(ReaderEvent.OnShowHideMenu(context = context))
} }
} }

View file

@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.MenuBook import androidx.compose.material.icons.automirrored.filled.MenuBook
import androidx.compose.material.icons.filled.DisplaySettings import androidx.compose.material.icons.filled.DisplaySettings
@ -26,6 +25,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.GoBackButton 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.Navigator
import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.settings.components.SettingsCategoryItem 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( fun SettingsScreen(
navigator: Navigator navigator: Navigator
) { ) {
val listState = rememberLazyListState() val scrollState = TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
canScroll = {
listState.canScrollForward || listState.canScrollBackward
}
)
Scaffold( Scaffold(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection) .nestedScroll(scrollState.first.nestedScrollConnection)
.windowInsetsPadding(WindowInsets.navigationBars), .windowInsetsPadding(WindowInsets.navigationBars),
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
topBar = { topBar = {
@ -56,7 +51,7 @@ fun SettingsScreen(
navigationIcon = { navigationIcon = {
GoBackButton(navigator = navigator) GoBackButton(navigator = navigator)
}, },
scrollBehavior = scrollBehavior, scrollBehavior = scrollState.first,
colors = TopAppBarDefaults.largeTopAppBarColors( colors = TopAppBarDefaults.largeTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer
@ -68,7 +63,7 @@ fun SettingsScreen(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.padding(top = paddingValues.calculateTopPadding()), .padding(top = paddingValues.calculateTopPadding()),
state = listState state = scrollState.second
) { ) {
item { item {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))

View file

@ -32,6 +32,7 @@ import ua.acclorite.book_story.presentation.components.CustomIconButton
*/ */
@Composable @Composable
fun ColorPickerWithTitle( fun ColorPickerWithTitle(
modifier: Modifier = Modifier,
value: Color, value: Color,
title: String, title: String,
horizontalPadding: Dp = 18.dp, horizontalPadding: Dp = 18.dp,
@ -42,7 +43,7 @@ fun ColorPickerWithTitle(
var color by remember { mutableStateOf(value) } var color by remember { mutableStateOf(value) }
Column( Column(
Modifier modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = verticalPadding, horizontal = horizontalPadding) .padding(vertical = verticalPadding, horizontal = horizontalPadding)
) { ) {

View file

@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow 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.domain.model.ButtonItem
import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.components.CategoryTitle
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SegmentedButtonWithTitle( fun SegmentedButtonWithTitle(
modifier: Modifier = Modifier,
title: String, title: String,
buttons: List<ButtonItem>, buttons: List<ButtonItem>,
enabled: Boolean, locked: Boolean,
horizontalPadding: Dp = 18.dp, horizontalPadding: Dp = 18.dp,
verticalPadding: Dp = 8.dp, verticalPadding: Dp = 8.dp,
onClick: (ButtonItem) -> Unit onClick: (ButtonItem) -> Unit
@ -32,7 +31,7 @@ fun SegmentedButtonWithTitle(
val colors = SegmentedButtonDefaults.colors() val colors = SegmentedButtonDefaults.colors()
Column( Column(
Modifier modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = horizontalPadding, vertical = verticalPadding) .padding(horizontal = horizontalPadding, vertical = verticalPadding)
) { ) {
@ -43,7 +42,7 @@ fun SegmentedButtonWithTitle(
SingleChoiceSegmentedButtonRow { SingleChoiceSegmentedButtonRow {
buttons.forEachIndexed { index, buttonItem -> buttons.forEachIndexed { index, buttonItem ->
SegmentedButton( SegmentedButton(
enabled = enabled, enabled = locked,
selected = buttonItem.selected, selected = buttonItem.selected,
onClick = { onClick(buttonItem) }, onClick = { onClick(buttonItem) },
shape = when (index) { shape = when (index) {
@ -60,9 +59,12 @@ fun SegmentedButtonWithTitle(
else -> RoundedCornerShape(0) else -> RoundedCornerShape(0)
}, },
colors = SegmentedButtonDefaults.colors( colors = SegmentedButtonDefaults.colors(
disabledInactiveBorderColor = colors.disabledActiveBorderColor, disabledInactiveBorderColor = colors.activeBorderColor,
disabledInactiveContentColor = colors.disabledActiveContentColor, disabledInactiveContentColor = colors.inactiveContentColor,
disabledActiveContainerColor = colors.activeContainerColor.copy(0.12f), disabledActiveContainerColor = colors.activeContainerColor,
disabledActiveContentColor = colors.activeContentColor,
disabledInactiveContainerColor = colors.inactiveContainerColor,
disabledActiveBorderColor = colors.activeBorderColor,
), ),
label = { label = {
Text( Text(

View file

@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -16,26 +15,37 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue 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.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource 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.R
import ua.acclorite.book_story.domain.model.ButtonItem import ua.acclorite.book_story.domain.model.ButtonItem
import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.components.GoBackButton 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.MainEvent
import ua.acclorite.book_story.presentation.data.MainViewModel import ua.acclorite.book_story.presentation.data.MainViewModel
import ua.acclorite.book_story.presentation.data.Navigator 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.ColorPickerWithTitle
import ua.acclorite.book_story.presentation.screens.settings.components.SegmentedButtonWithTitle 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.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.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.Theme
import ua.acclorite.book_story.presentation.ui.ThemeContrast 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -43,19 +53,21 @@ fun AppearanceSettings(
mainViewModel: MainViewModel, mainViewModel: MainViewModel,
navigator: Navigator navigator: Navigator
) { ) {
val listState = rememberLazyListState() val scrollState = TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
canScroll = {
listState.canScrollForward || listState.canScrollBackward
}
)
val state by mainViewModel.state.collectAsState() 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( Scaffold(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection) .nestedScroll(scrollState.first.nestedScrollConnection)
.windowInsetsPadding(WindowInsets.navigationBars), .windowInsetsPadding(WindowInsets.navigationBars),
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
topBar = { topBar = {
@ -66,7 +78,7 @@ fun AppearanceSettings(
navigationIcon = { navigationIcon = {
GoBackButton(navigator = navigator) GoBackButton(navigator = navigator)
}, },
scrollBehavior = scrollBehavior, scrollBehavior = scrollState.first,
colors = TopAppBarDefaults.largeTopAppBarColors( colors = TopAppBarDefaults.largeTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer
@ -78,25 +90,35 @@ fun AppearanceSettings(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.padding(top = paddingValues.calculateTopPadding()), .padding(top = paddingValues.calculateTopPadding()),
state = listState state = scrollState.second
) { ) {
item { item {
Spacer(modifier = Modifier.height(16.dp)) Spacer(
modifier = Modifier
.animateItem()
.height(16.dp)
)
} }
item { item {
CategoryTitle( CategoryTitle(
modifier = Modifier.animateItem(),
title = stringResource(id = R.string.theme_appearance_settings), title = stringResource(id = R.string.theme_appearance_settings),
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
} }
item { item {
Spacer(modifier = Modifier.height(8.dp)) Spacer(
modifier = Modifier
.animateItem()
.height(8.dp)
)
} }
item { item {
SegmentedButtonWithTitle( SegmentedButtonWithTitle(
modifier = Modifier.animateItem(),
title = stringResource(id = R.string.dark_theme_option), title = stringResource(id = R.string.dark_theme_option),
enabled = true, locked = true,
buttons = DarkTheme.entries.map { buttons = DarkTheme.entries.map {
ButtonItem( ButtonItem(
it.toString(), it.toString(),
@ -119,9 +141,29 @@ fun AppearanceSettings(
} }
item { item {
AppearanceSettingsThemeSwitcher(
modifier = Modifier.animateItem(),
mainViewModel = mainViewModel
)
}
item {
BookStoryTheme(
theme = themeContrastTheme,
isDark = state.darkTheme!!.isDark(),
isPureDark = state.pureDark!!.isPureDark(context = LocalContext.current),
themeContrast = state.themeContrast!!
) {
SlidingTransition(
modifier = Modifier.animateItem(
fadeInSpec = null,
fadeOutSpec = null
),
visible = state.theme != Theme.DYNAMIC,
) {
SegmentedButtonWithTitle( SegmentedButtonWithTitle(
title = stringResource(id = R.string.theme_contrast_option), title = stringResource(id = R.string.theme_contrast_option),
enabled = state.theme != Theme.DYNAMIC, locked = state.theme != Theme.DYNAMIC,
buttons = ThemeContrast.entries.map { buttons = ThemeContrast.entries.map {
ButtonItem( ButtonItem(
it.toString(), it.toString(),
@ -142,26 +184,63 @@ fun AppearanceSettings(
) )
} }
} }
}
item {
AppearanceSettingsThemeSwitcher(mainViewModel = mainViewModel)
} }
item { item {
Spacer(modifier = Modifier.height(22.dp)) SlidingTransition(
} modifier = Modifier.animateItem(
item { fadeInSpec = null,
CategoryTitle( fadeOutSpec = null
title = stringResource(id = R.string.colors_appearance_settings), ),
color = MaterialTheme.colorScheme.primary 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 { item {
Spacer(modifier = Modifier.height(8.dp)) Spacer(
modifier = Modifier
.animateItem()
.height(22.dp)
)
CategoryTitle(
title = stringResource(id = R.string.colors_appearance_settings),
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.animateItem()
)
Spacer(
modifier = Modifier
.animateItem()
.height(8.dp)
)
} }
item { item {
ColorPickerWithTitle( ColorPickerWithTitle(
modifier = Modifier.animateItem(),
value = Color(state.backgroundColor!!.toULong()), value = Color(state.backgroundColor!!.toULong()),
title = stringResource(id = R.string.background_color_option), title = stringResource(id = R.string.background_color_option),
onValueChange = { onValueChange = {
@ -175,6 +254,7 @@ fun AppearanceSettings(
} }
item { item {
ColorPickerWithTitle( ColorPickerWithTitle(
modifier = Modifier.animateItem(),
value = Color(state.fontColor!!.toULong()), value = Color(state.fontColor!!.toULong()),
title = stringResource(id = R.string.font_color_option), title = stringResource(id = R.string.font_color_option),
onValueChange = { onValueChange = {
@ -187,7 +267,13 @@ fun AppearanceSettings(
) )
} }
item { Spacer(modifier = Modifier.height(48.dp)) } item {
Spacer(
modifier = Modifier
.animateItem()
.height(48.dp)
)
}
} }
} }
} }

View file

@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
@ -13,7 +14,9 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.util.Constants 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.data.MainViewModel
import ua.acclorite.book_story.presentation.ui.Theme import ua.acclorite.book_story.presentation.ui.Theme
import ua.acclorite.book_story.presentation.ui.isDark import ua.acclorite.book_story.presentation.ui.isDark
import ua.acclorite.book_story.presentation.ui.isPureDark
/** /**
* Theme switcher. * Theme switcher.
*/ */
@Composable @Composable
fun AppearanceSettingsThemeSwitcher( fun AppearanceSettingsThemeSwitcher(
mainViewModel: MainViewModel modifier: Modifier = Modifier,
mainViewModel: MainViewModel,
verticalPadding: Dp = 8.dp
) { ) {
val state by mainViewModel.state.collectAsState() val state by mainViewModel.state.collectAsState()
val themes = remember { val themes = remember {
@ -37,10 +43,10 @@ fun AppearanceSettingsThemeSwitcher(
} }
Column( Column(
Modifier modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = verticalPadding)
) { ) {
Spacer(modifier = Modifier.height(8.dp))
CategoryTitle( CategoryTitle(
title = stringResource(id = R.string.app_theme_option) title = stringResource(id = R.string.app_theme_option)
) )
@ -60,6 +66,7 @@ fun AppearanceSettingsThemeSwitcher(
theme = themeEntry, theme = themeEntry,
darkTheme = state.darkTheme!!.isDark(), darkTheme = state.darkTheme!!.isDark(),
themeContrast = state.themeContrast!!, themeContrast = state.themeContrast!!,
isPureDark = state.pureDark!!.isPureDark(context = LocalContext.current),
selected = state.theme == themeEntry.first selected = state.theme == themeEntry.first
) { ) {
mainViewModel.onEvent(MainEvent.OnChangeTheme(themeEntry.first.toString())) mainViewModel.onEvent(MainEvent.OnChangeTheme(themeEntry.first.toString()))

View file

@ -45,11 +45,17 @@ import ua.acclorite.book_story.presentation.ui.animatedColorScheme
fun AppearanceSettingsThemeSwitcherItem( fun AppearanceSettingsThemeSwitcherItem(
theme: Pair<Theme, UIText>, theme: Pair<Theme, UIText>,
darkTheme: Boolean, darkTheme: Boolean,
isPureDark: Boolean,
themeContrast: ThemeContrast, themeContrast: ThemeContrast,
selected: Boolean, selected: Boolean,
onClick: () -> Unit onClick: () -> Unit
) { ) {
val colorScheme = animatedColorScheme(theme.first, darkTheme, themeContrast) val colorScheme = animatedColorScheme(
theme.first,
darkTheme,
isPureDark,
themeContrast
)
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),

View file

@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme 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.model.ButtonItem
import ua.acclorite.book_story.domain.util.Constants import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.presentation.components.GoBackButton 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.MainEvent
import ua.acclorite.book_story.presentation.data.MainViewModel import ua.acclorite.book_story.presentation.data.MainViewModel
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
@ -37,19 +37,13 @@ fun GeneralSettings(
mainViewModel: MainViewModel, mainViewModel: MainViewModel,
navigator: Navigator navigator: Navigator
) { ) {
val listState = rememberLazyListState() val scrollState = TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
canScroll = {
listState.canScrollForward || listState.canScrollBackward
}
)
val state by mainViewModel.state.collectAsState() val state by mainViewModel.state.collectAsState()
Scaffold( Scaffold(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection) .nestedScroll(scrollState.first.nestedScrollConnection)
.windowInsetsPadding(WindowInsets.navigationBars), .windowInsetsPadding(WindowInsets.navigationBars),
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
topBar = { topBar = {
@ -60,7 +54,7 @@ fun GeneralSettings(
navigationIcon = { navigationIcon = {
GoBackButton(navigator = navigator) GoBackButton(navigator = navigator)
}, },
scrollBehavior = scrollBehavior, scrollBehavior = scrollState.first,
colors = TopAppBarDefaults.largeTopAppBarColors( colors = TopAppBarDefaults.largeTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer
@ -72,7 +66,7 @@ fun GeneralSettings(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.padding(top = paddingValues.calculateTopPadding()), .padding(top = paddingValues.calculateTopPadding()),
state = listState state = scrollState.second
) { ) {
item { item {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))

View file

@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme 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.domain.util.Constants
import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.components.GoBackButton 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.MainEvent
import ua.acclorite.book_story.presentation.data.MainViewModel import ua.acclorite.book_story.presentation.data.MainViewModel
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
@ -42,12 +42,7 @@ fun ReaderSettings(
mainViewModel: MainViewModel, mainViewModel: MainViewModel,
navigator: Navigator navigator: Navigator
) { ) {
val listState = rememberLazyListState() val scrollState = TopAppBarDefaults.collapsibleScrollBehaviorWithLazyListState()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
canScroll = {
listState.canScrollForward || listState.canScrollBackward
}
)
val state by mainViewModel.state.collectAsState() val state by mainViewModel.state.collectAsState()
val fontFamily = remember(state.fontFamily) { val fontFamily = remember(state.fontFamily) {
@ -59,7 +54,7 @@ fun ReaderSettings(
Scaffold( Scaffold(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection) .nestedScroll(scrollState.first.nestedScrollConnection)
.windowInsetsPadding(WindowInsets.navigationBars), .windowInsetsPadding(WindowInsets.navigationBars),
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
topBar = { topBar = {
@ -70,7 +65,7 @@ fun ReaderSettings(
navigationIcon = { navigationIcon = {
GoBackButton(navigator = navigator) GoBackButton(navigator = navigator)
}, },
scrollBehavior = scrollBehavior, scrollBehavior = scrollState.first,
colors = TopAppBarDefaults.largeTopAppBarColors( colors = TopAppBarDefaults.largeTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer
@ -82,7 +77,7 @@ fun ReaderSettings(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.padding(top = paddingValues.calculateTopPadding()), .padding(top = paddingValues.calculateTopPadding()),
state = listState state = scrollState.second
) { ) {
item { item {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))

View file

@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import ua.acclorite.book_story.presentation.ui.theme.aquaTheme 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.blueTheme
import ua.acclorite.book_story.presentation.ui.theme.greenTheme import ua.acclorite.book_story.presentation.ui.theme.greenTheme
import ua.acclorite.book_story.presentation.ui.theme.pinkTheme import ua.acclorite.book_story.presentation.ui.theme.pinkTheme
@ -43,49 +44,61 @@ fun String.toTheme(): Theme {
*/ */
@SuppressLint("NewApi") @SuppressLint("NewApi")
@Composable @Composable
fun colorScheme(theme: Theme, darkTheme: Boolean, themeContrast: ThemeContrast): ColorScheme { fun colorScheme(
when (theme) { theme: Theme,
darkTheme: Boolean,
isPureDark: Boolean,
themeContrast: ThemeContrast
): ColorScheme {
val colorScheme = when (theme) {
Theme.DYNAMIC -> { Theme.DYNAMIC -> {
/* Dynamic Theme */ /* Dynamic Theme */
return if (darkTheme) if (darkTheme) {
dynamicDarkColorScheme(LocalContext.current) dynamicDarkColorScheme(LocalContext.current)
else } else {
dynamicLightColorScheme(LocalContext.current) dynamicLightColorScheme(LocalContext.current)
} }
}
Theme.BLUE -> { Theme.BLUE -> {
/* Blue Theme */ /* Blue Theme */
return blueTheme(isDark = darkTheme, themeContrast = themeContrast) blueTheme(isDark = darkTheme, themeContrast = themeContrast)
} }
Theme.PURPLE -> { Theme.PURPLE -> {
/* Purple Theme */ /* Purple Theme */
return purpleTheme(isDark = darkTheme, themeContrast = themeContrast) purpleTheme(isDark = darkTheme, themeContrast = themeContrast)
} }
Theme.GREEN -> { Theme.GREEN -> {
/* Green Theme */ /* Green Theme */
return greenTheme(isDark = darkTheme, themeContrast = themeContrast) greenTheme(isDark = darkTheme, themeContrast = themeContrast)
} }
Theme.PINK -> { Theme.PINK -> {
/* Pink Theme */ /* Pink Theme */
return pinkTheme(isDark = darkTheme, themeContrast = themeContrast) pinkTheme(isDark = darkTheme, themeContrast = themeContrast)
} }
Theme.YELLOW -> { Theme.YELLOW -> {
/* Yellow Theme */ /* Yellow Theme */
return yellowTheme(isDark = darkTheme, themeContrast = themeContrast) yellowTheme(isDark = darkTheme, themeContrast = themeContrast)
} }
Theme.RED -> { Theme.RED -> {
/* Red Theme */ /* Red Theme */
return redTheme(isDark = darkTheme, themeContrast = themeContrast) redTheme(isDark = darkTheme, themeContrast = themeContrast)
} }
Theme.AQUA -> { Theme.AQUA -> {
/* Aqua Theme */ /* Aqua Theme */
return aquaTheme(isDark = darkTheme, themeContrast = themeContrast) aquaTheme(isDark = darkTheme, themeContrast = themeContrast)
} }
} }
return if (isPureDark && darkTheme) {
blackTheme(initialTheme = colorScheme)
} else {
colorScheme
}
} }

View file

@ -1,7 +1,9 @@
package ua.acclorite.book_story.presentation.ui package ua.acclorite.book_story.presentation.ui
import android.app.Activity import android.app.Activity
import android.content.Context
import android.os.Build import android.os.Build
import android.os.PowerManager
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
@ -16,6 +18,13 @@ import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
@Immutable
enum class PureDark {
OFF,
ON,
SAVER
}
@Immutable @Immutable
enum class ThemeContrast { enum class ThemeContrast {
STANDARD, STANDARD,
@ -34,6 +43,10 @@ fun String.toDarkTheme(): DarkTheme {
return DarkTheme.valueOf(this) return DarkTheme.valueOf(this)
} }
fun String.toPureDark(): PureDark {
return PureDark.valueOf(this)
}
fun String.toThemeContrast(): ThemeContrast { fun String.toThemeContrast(): ThemeContrast {
return ThemeContrast.valueOf(this) return ThemeContrast.valueOf(this)
} }
@ -48,9 +61,21 @@ fun DarkTheme.isDark(): Boolean {
} }
@Composable @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, theme: Theme,
isDark: Boolean, isDark: Boolean,
isPureDark: Boolean,
themeContrast: ThemeContrast, themeContrast: ThemeContrast,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
@ -76,6 +101,7 @@ fun BooksHistoryResurrectionTheme(
theme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) theme theme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) theme
else if (theme != Theme.DYNAMIC) theme else Theme.BLUE, else if (theme != Theme.DYNAMIC) theme else Theme.BLUE,
darkTheme = isDark, darkTheme = isDark,
isPureDark = isPureDark,
themeContrast = themeContrast themeContrast = themeContrast
) )
val animatedColorScheme = animateColorScheme(targetColorScheme = colorScheme) val animatedColorScheme = animateColorScheme(targetColorScheme = colorScheme)
@ -143,11 +169,13 @@ private fun animateColorScheme(targetColorScheme: ColorScheme): ColorScheme {
fun animatedColorScheme( fun animatedColorScheme(
theme: Theme, theme: Theme,
isDark: Boolean, isDark: Boolean,
isPureDark: Boolean,
themeContrast: ThemeContrast themeContrast: ThemeContrast
): ColorScheme { ): ColorScheme {
val colorScheme = colorScheme( val colorScheme = colorScheme(
theme = theme, theme = theme,
darkTheme = isDark, darkTheme = isDark,
isPureDark = isPureDark,
themeContrast = themeContrast themeContrast = themeContrast
) )

View file

@ -5,7 +5,9 @@ import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.slideOutVertically
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -13,20 +15,20 @@ object Transitions {
val DefaultTransitionIn = fadeIn(tween(300)) val DefaultTransitionIn = fadeIn(tween(300))
val DefaultTransitionOut = fadeOut(tween(300)) val DefaultTransitionOut = fadeOut(tween(300))
val FadeTransitionIn = fadeIn(tween(300)) val FadeTransitionIn = fadeIn(tween(350))
val FadeTransitionOut = fadeOut(tween(50)) val FadeTransitionOut = fadeOut(tween(100))
val SlidingTransitionIn = fadeIn(tween(300)) + val SlidingTransitionIn = fadeIn(tween(350)) +
slideInHorizontally(tween(300)) { it / 16 } slideInHorizontally(tween(350)) { it / 16 }
val BackSlidingTransitionIn = fadeIn(tween(300)) + val BackSlidingTransitionIn = fadeIn(tween(350)) +
slideInHorizontally(tween(300)) { -it / 16 } slideInHorizontally(tween(350)) { -it / 16 }
val SlidingTransitionOut = fadeOut(tween(200)) + val SlidingTransitionOut = fadeOut(tween(250)) +
slideOutHorizontally(tween(300)) { -it / 16 } slideOutHorizontally(tween(350)) { -it / 16 }
val BackSlidingTransitionOut = fadeOut(tween(200)) + val BackSlidingTransitionOut = fadeOut(tween(250)) +
slideOutHorizontally(tween(300)) { it / 16 } slideOutHorizontally(tween(350)) { it / 16 }
} }
@Composable @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()
}
}

View file

@ -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)
}

View file

@ -3,7 +3,7 @@
<style name="Theme.Start.Splash" parent="Theme.SplashScreen"> <style name="Theme.Start.Splash" parent="Theme.SplashScreen">
<item name="android:forceDarkAllowed" tools:ignore="NewApi">false</item> <item name="android:forceDarkAllowed" tools:ignore="NewApi">false</item>
<item name="postSplashScreenTheme">@style/Theme.BooksHistoryResurrection</item> <item name="postSplashScreenTheme">@style/Theme.BookStory</item>
<item name="windowSplashScreenBackground">@color/splash_bg_dark</item> <item name="windowSplashScreenBackground">@color/splash_bg_dark</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item> <item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>

View file

@ -1,7 +1,12 @@
<resources> <resources xmlns:tools="http://schemas.android.com/tools">
<!-- Basic Strings --> <!-- Basic Strings -->
<string name="app_name">Історія Книги</string> <string name="app_name">Історія Книги</string>
<!-- Book sources -->
<string-array name="book_sites" tools:ignore="InconsistentArrays">
<item>Укрліб|https://www.ukrlib.com.ua</item>
</string-array>
<!-- Screens --> <!-- Screens -->
<string name="library_screen">Бібліотека</string> <string name="library_screen">Бібліотека</string>
<string name="history_screen">Історія</string> <string name="history_screen">Історія</string>
@ -108,7 +113,7 @@
<string name="continue_read">Продовжити</string> <string name="continue_read">Продовжити</string>
<string name="never">Ніколи</string> <string name="never">Ніколи</string>
<string name="go_back">Назад</string> <string name="go_back">Назад</string>
<string name="move_to_read">Перемістити цю книгу в \"Прочитано\"</string> <string name="move_to_read">Перемістити цю книгу в «Прочитано»</string>
<string name="back_to_library">Назад в Бібліотеку</string> <string name="back_to_library">Назад в Бібліотеку</string>
<string name="undo">Скасувати</string> <string name="undo">Скасувати</string>
<string name="copy">Копіювати</string> <string name="copy">Копіювати</string>
@ -160,6 +165,7 @@
<!-- Settings options --> <!-- Settings options -->
<string name="language_option">Мова застосунка</string> <string name="language_option">Мова застосунка</string>
<string name="dark_theme_option">Темна тема</string> <string name="dark_theme_option">Темна тема</string>
<string name="pure_dark_option">Суцільна темрява (ОЛЕД)</string>
<string name="theme_contrast_option">Контрастність теми</string> <string name="theme_contrast_option">Контрастність теми</string>
<string name="app_theme_option">Тема застосунка</string> <string name="app_theme_option">Тема застосунка</string>
<string name="font_family_option">Сімейство шрифтів</string> <string name="font_family_option">Сімейство шрифтів</string>
@ -181,6 +187,11 @@
<string name="dark_theme_on">Ввімкнена</string> <string name="dark_theme_on">Ввімкнена</string>
<string name="dark_theme_follow_system">Системна</string> <string name="dark_theme_follow_system">Системна</string>
<!-- Pure Dark properties -->
<string name="pure_dark_off">Вимкнена</string>
<string name="pure_dark_on">Ввімкнена</string>
<string name="pure_dark_power_saver">Економія</string>
<!-- Theme Contrast properties --> <!-- Theme Contrast properties -->
<string name="theme_contrast_standard">Типова</string> <string name="theme_contrast_standard">Типова</string>
<string name="theme_contrast_medium">Середня</string> <string name="theme_contrast_medium">Середня</string>
@ -257,10 +268,106 @@
<string name="slava_ukraini_option_desc">Героям Слава! 🐉</string> <string name="slava_ukraini_option_desc">Героям Слава! 🐉</string>
<!-- Help --> <!-- Help -->
<string name="clickable_note_1">Будь ласка, зверніть увагу, що на</string> <string name="help_clickable_note_1">Будь ласка, зверніть увагу, що на</string>
<string name="clickable_note_2">такий виділений текст</string> <string name="help_clickable_note_2">такий виділений текст</string>
<string name="clickable_note_3">можна натиснути.</string> <string name="help_clickable_note_3">
<string name="clickable_note_action">На мене можна натиснути!</string> можна натиснути.
Також ви можете приховати цю примітку, натиснувши на іконку, спробуйте!
</string>
<string name="help_clickable_note_action">На мене можна натиснути!</string>
<string name="help_title_how_to_find_books">Як мені знайти та завантажити книги?</string>
<string name="help_desc_how_to_find_books">
Ви можете спробувати знайти бажані книги використовуючи сайти з наведеної примітки
або ви можете використати текстове поле, щоб шукати книги в інтернеті.
</string>
<string name="help_field_placeholder">Назва книги</string>
<string name="help_field_support">Корисні сайти з книгами:</string>
<string name="help_field_error">Назва книги не має бути порожньою</string>
<string name="help_title_how_to_add_books">Як мені додати книги в Бібліотеку?</string>
<string name="help_desc_how_to_add_books_1">Перейдіть в</string>
<string name="help_desc_how_to_add_books_2">Огляд</string>
<string name="help_desc_how_to_add_books_3">
Якщо ви не бачите завантажені книги, спробуйте оновити список, потягнувши його вниз.
Переконайтеся, що ваші книги має підтримуваний формат файлу.
Потім натисніть на книгу, щоб вибрати її, або утримуйте, щоб показати її місцезнаходження.
Після того, як ви вибрали всі потрібні книги, натисніть на іконку з галочкою у верхньому правому куті,
зачекайте поки всі книги завантажаться, і натисніть «Додати».
Тепер ви повинні побачити всі додані вами книги в
</string>
<string name="help_desc_how_to_add_books_4">Бібліотеці</string>
<string name="help_title_how_to_customize_app">Як мені налаштувати застосунок?</string>
<string name="help_desc_how_to_customize_app_1">Перейдіть в</string>
<string name="help_desc_how_to_customize_app_2">Налаштування</string>
<string name="help_desc_how_to_customize_app_3">
Ви можете зробити це з екранів Бібліотека, Історія та Огляд
натиснувши на три крапки в правому верхньому кутку і вибравши
«Налаштування» з випадаючого списку.
Після цього ви побачите всі доступні налаштування для цього застосунку.
Не соромтеся експерементувати з ними!
</string>
<string name="help_title_how_to_move_or_delete_books">Як мені перемістити або видалити книги?</string>
<string name="help_desc_how_to_move_or_delete_books_1">Перейдіть в</string>
<string name="help_desc_how_to_move_or_delete_books_2">Бібліотеку</string>
<string name="help_desc_how_to_move_or_delete_books_3">
Утримуйте обкладинку першої книги, поки вона не буде вибрана,
тепер ви можете просто натиснути на всі обкладинки книг, які ви хочете вибрати.
Ви також можете вибрати книги в декількох категоріях.
Після того, як ви вибрали всі бажані книги, ви можете натиснути на іконку зі стрілкою,
щоб перемістити та на іконку кошика, щоб видалити книги.
</string>
<string name="help_title_how_to_edit_book">Як мені відредагувати книгу?</string>
<string name="help_desc_how_to_edit_book_1">Перейдіть в</string>
<string name="help_desc_how_to_edit_book_2">Бібліотеку</string>
<string name="help_desc_how_to_edit_book_3">
та натісніть на обкладинку книги, щоб побачити подробиці книги.
Тепер ви можете утримувати назву книги, щоб відредагувати назву
або обкладинку, щоб змінити або видалити обкладинку.
</string>
<string name="help_title_how_to_read_book">Як мені читати книгу?</string>
<string name="help_desc_how_to_read_book_1">Перейдіть в</string>
<string name="help_desc_how_to_read_book_2">Бібліотеку</string>
<string name="help_desc_how_to_read_book_3">
Щоб почати читати книгу, натисніть на кнопку в правому нижньому куті книги, яку ви хочете читати.
Також ви можете почати читати книгу, натиснувши на її обкладинку, щоб побачити подробиці книги,
а потім натиснути кнопку в правому нижньому куті.
Прогрес буде збережено і відновлено автоматично.
Ви можете показати верхню і нижню панелі, натиснувши в будь-якому місці екрану.
Натиснувши на назву книги, ви перейдете до подробиць книги.
Натиснувши на іконку з шестернею, ви побачите налаштування Читача.
Ви можете використовувати повзунок нижньої панелі для зміни прогресу.
</string>
<string name="help_title_how_to_customize_reader">Як мені налаштувати Читач?</string>
<string name="help_desc_how_to_customize_reader_1">
Почніть читати книгу(див: Як мені читати книгу?),
після цього покажіть верхню і нижню панелі, натиснувши в будь-якому місці екрану.
Потім натисніть на іконку з шестернею в правому верхньому кутку.
Це покаже всі налаштування Читача.
Другий спосіб - перейти в
</string>
<string name="help_desc_how_to_customize_reader_2">
Налаштування
</string>
<string name="help_desc_how_to_customize_reader_3">
та вибрати категорію Читач, щоб налаштувати Головні параметри,
або категорію Зовнішній Вигляд, щоб налаштувати параметри Кольорів.
</string>
<string name="help_title_how_to_update_book">Як мені оновити книгу?</string>
<string name="help_desc_how_to_update_book_1">Перейдіть в</string>
<string name="help_desc_how_to_update_book_2">Бібліотеку</string>
<string name="help_desc_how_to_update_book_3">
та натісніть на обкладинку книги, щоб побачити подробиці книги.
Потягніть екран вниз або натисніть на іконку оновлення у верхньому правому куті, щоб оновити книгу.
Якщо файл книги змінився, ви можете підтвердити і оновити книгу або скасувати оновлення.
</string>
<!-- Font families --> <!-- Font families -->
<string name="default_font">За замовчуванням</string> <string name="default_font">За замовчуванням</string>
@ -294,6 +401,11 @@
<string name="app_icon_content_desc">Іконка застосунку</string> <string name="app_icon_content_desc">Іконка застосунку</string>
<string name="note_content_desc">Примітка</string> <string name="note_content_desc">Примітка</string>
<string name="delete_history_element_content_desc">Видалити елемент історії</string> <string name="delete_history_element_content_desc">Видалити елемент історії</string>
<string name="library_content_desc">Ваша бібліотека</string>
<string name="history_content_desc">Історія читання</string>
<string name="browse_content_desc">Додавайте книги</string>
<string name="arrow_content_desc">Стрілка</string>
<string name="arrow_anim_content_desc">Анімація стрілки</string>
</resources> </resources>

View file

@ -1,7 +1,9 @@
<resources> <resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="InconsistentArrays">
<!-- Basic Strings --> <!-- Basic Strings -->
<string name="app_version" translatable="false">0.9.6</string> <string name="app_version" translatable="false">0.9.7</string>
<string name="app_name">Book\'s Story</string> <string name="app_name">Book\'s Story</string>
<!-- About sites -->
<string name="releases_page" translatable="false"> <string name="releases_page" translatable="false">
https://github.com/Acclorite/book-story/releases/latest https://github.com/Acclorite/book-story/releases/latest
</string> </string>
@ -18,6 +20,13 @@
https://www.buymeacoffee.com/acclorite https://www.buymeacoffee.com/acclorite
</string> </string>
<!-- Book sources -->
<string-array name="book_sites">
<item>Anna\'s Archive|https://annas-archive.org</item>
<item>Project Gutenberg|https://www.gutenberg.org</item>
<item>Standard Ebooks|https://standardebooks.org</item>
</string-array>
<!-- Screens --> <!-- Screens -->
<string name="library_screen">Library</string> <string name="library_screen">Library</string>
<string name="history_screen">History</string> <string name="history_screen">History</string>
@ -123,7 +132,7 @@
<string name="continue_read">Continue</string> <string name="continue_read">Continue</string>
<string name="never">Never</string> <string name="never">Never</string>
<string name="go_back">Go back</string> <string name="go_back">Go back</string>
<string name="move_to_read">Move this book to \"Already Read\"</string> <string name="move_to_read">Move this book to «Already Read»</string>
<string name="back_to_library">Back to Library</string> <string name="back_to_library">Back to Library</string>
<string name="undo">Undo</string> <string name="undo">Undo</string>
<string name="copy">Copy</string> <string name="copy">Copy</string>
@ -175,6 +184,7 @@
<!-- Settings options --> <!-- Settings options -->
<string name="language_option">App language</string> <string name="language_option">App language</string>
<string name="dark_theme_option">Dark theme</string> <string name="dark_theme_option">Dark theme</string>
<string name="pure_dark_option">Pure dark (OLED)</string>
<string name="theme_contrast_option">Theme contrast</string> <string name="theme_contrast_option">Theme contrast</string>
<string name="app_theme_option">App theme</string> <string name="app_theme_option">App theme</string>
<string name="font_family_option">Font family</string> <string name="font_family_option">Font family</string>
@ -196,6 +206,11 @@
<string name="dark_theme_on">Enabled</string> <string name="dark_theme_on">Enabled</string>
<string name="dark_theme_follow_system">System</string> <string name="dark_theme_follow_system">System</string>
<!-- Pure Dark properties -->
<string name="pure_dark_off">Disabled</string>
<string name="pure_dark_on">Enabled</string>
<string name="pure_dark_power_saver">Saver</string>
<!-- Theme Contrast properties --> <!-- Theme Contrast properties -->
<string name="theme_contrast_standard">Standard</string> <string name="theme_contrast_standard">Standard</string>
<string name="theme_contrast_medium">Medium</string> <string name="theme_contrast_medium">Medium</string>
@ -272,10 +287,105 @@
<string name="slava_ukraini_option_desc">Heroyam Slava! 🐉</string> <string name="slava_ukraini_option_desc">Heroyam Slava! 🐉</string>
<!-- Help --> <!-- Help -->
<string name="clickable_note_1">Please note, that</string> <string name="help_clickable_note_1">Please note, that</string>
<string name="clickable_note_2">such highlighted text</string> <string name="help_clickable_note_2">such highlighted text</string>
<string name="clickable_note_3">can be clicked.</string> <string name="help_clickable_note_3">
<string name="clickable_note_action">I\'m clickable!</string> can be clicked.
Also you can hide this note by clicking the icon, try it out!
</string>
<string name="help_clickable_note_action">I\'m clickable!</string>
<string name="help_title_how_to_find_books">How do I find and download books?</string>
<string name="help_desc_how_to_find_books">
You can try to find desired books using sites from provided note
or you can use text field to search for books online.
</string>
<string name="help_field_placeholder">Book\'s name</string>
<string name="help_field_support">Useful sites with books:</string>
<string name="help_field_error">Book\'s name should not be empty</string>
<string name="help_title_how_to_add_books">How do I add books to the Library?</string>
<string name="help_desc_how_to_add_books_1">Go to the</string>
<string name="help_desc_how_to_add_books_2">Browse</string>
<string name="help_desc_how_to_add_books_3">
If you don\'t see downloaded books try to refresh list by pulling it down.
Make sure your books has supported file format.
Then click the book to select it or hold to show it\'s location.
After you selected all books you want, click the checkmark icon in the upper right corner,
wait for all books to load and click «Add».
Now you should see all books you have added in the
</string>
<string name="help_desc_how_to_add_books_4">Library</string>
<string name="help_title_how_to_customize_app">How do I customize the app?</string>
<string name="help_desc_how_to_customize_app_1">Go to the</string>
<string name="help_desc_how_to_customize_app_2">Settings</string>
<string name="help_desc_how_to_customize_app_3">
You can do this from the Library, History and Browse screens
by clicking three dots in the upper right corner
and selecting «Settings» from the drop-down menu.
Then you will see all available settings for this app.
Feel free to experiment with those!
</string>
<string name="help_title_how_to_move_or_delete_books">How do I move or delete books?</string>
<string name="help_desc_how_to_move_or_delete_books_1">Go to the</string>
<string name="help_desc_how_to_move_or_delete_books_2">Library</string>
<string name="help_desc_how_to_move_or_delete_books_3">
Hold down the first book\'s cover image until it is selected,
now you can simply click all cover images of books you want to select.
You can also select books in multiple categories.
After you have chosen all desired books, you can click the arrow icon to move
and trash bin icon to delete books.
</string>
<string name="help_title_how_to_edit_book">How do I edit a book?</string>
<string name="help_desc_how_to_edit_book_1">Go to the</string>
<string name="help_desc_how_to_edit_book_2">Library</string>
<string name="help_desc_how_to_edit_book_3">
and click the book\'s cover image to see book\'s details.
Now you can hold down the book\'s title to edit title
or cover image to change or delete cover image.
</string>
<string name="help_title_how_to_read_book">How do I read a book?</string>
<string name="help_desc_how_to_read_book_1">Go to the</string>
<string name="help_desc_how_to_read_book_2">Library</string>
<string name="help_desc_how_to_read_book_3">
To start reading the book click the button in the bottom right corner of the book you want to read.
Also you can start reading the book by clicking it\'s cover image
to see book\'s details and then click the button in the bottom right corner.
The progress will be saved and restored automatically.
You can bring top and bottom bars by clicking anywhere on the screen.
By clicking the title of the book you will be transferred to the book\'s details.
By clicking the gear icon you will see Reader settings.
You can use bottom bar\'s slider to change progress.
</string>
<string name="help_title_how_to_customize_reader">How do I customize the Reader?</string>
<string name="help_desc_how_to_customize_reader_1">
Start reading the book(see: How do I read a book?),
after that bring top and bottom bars by clicking anywhere on the screen.
Then click the gear icon in the upper right corner.
This will show all Reader settings.
Second way is to go to the
</string>
<string name="help_desc_how_to_customize_reader_2">
Settings
</string>
<string name="help_desc_how_to_customize_reader_3">
and select Reader category to customize General settings,
or Appearance category to customize Colors settings.
</string>
<string name="help_title_how_to_update_book">How do I update a book?</string>
<string name="help_desc_how_to_update_book_1">Go to the</string>
<string name="help_desc_how_to_update_book_2">Library</string>
<string name="help_desc_how_to_update_book_3">
and click the book\'s cover image to see book\'s details.
Pull down the screen or click the refresh icon in the upper right corner to update the book.
If book\'s file changed you may confirm and update the book or cancel update.
</string>
<!-- Font families --> <!-- Font families -->
<string name="default_font">Default</string> <string name="default_font">Default</string>
@ -309,5 +419,10 @@
<string name="app_icon_content_desc">App icon</string> <string name="app_icon_content_desc">App icon</string>
<string name="note_content_desc">Note</string> <string name="note_content_desc">Note</string>
<string name="delete_history_element_content_desc">Delete history element</string> <string name="delete_history_element_content_desc">Delete history element</string>
<string name="library_content_desc">Your library</string>
<string name="history_content_desc">Reading history</string>
<string name="browse_content_desc">Add books</string>
<string name="arrow_content_desc">Arrow</string>
<string name="arrow_anim_content_desc">Arrow animation</string>
</resources> </resources>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.BooksHistoryResurrection" parent="Theme.AppCompat.NoActionBar"> <style name="Theme.BookStory" parent="Theme.AppCompat.NoActionBar">
<item name="android:forceDarkAllowed" tools:ignore="NewApi">false</item> <item name="android:forceDarkAllowed" tools:ignore="NewApi">false</item>
<item name="android:windowContentTransitions">false</item> <item name="android:windowContentTransitions">false</item>
@ -12,7 +12,7 @@
<style name="Theme.Start.Splash" parent="Theme.SplashScreen"> <style name="Theme.Start.Splash" parent="Theme.SplashScreen">
<item name="android:forceDarkAllowed" tools:ignore="NewApi">false</item> <item name="android:forceDarkAllowed" tools:ignore="NewApi">false</item>
<item name="postSplashScreenTheme">@style/Theme.BooksHistoryResurrection</item> <item name="postSplashScreenTheme">@style/Theme.BookStory</item>
<item name="windowSplashScreenBackground">@color/splash_bg_light</item> <item name="windowSplashScreenBackground">@color/splash_bg_light</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item> <item name="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>