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

View file

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

View file

@ -9,19 +9,25 @@ import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
@ -49,9 +55,10 @@ import ua.acclorite.book_story.presentation.screens.settings.SettingsScreen
import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.AppearanceSettings
import ua.acclorite.book_story.presentation.screens.settings.nested.general.GeneralSettings
import ua.acclorite.book_story.presentation.screens.settings.nested.reader.ReaderSettings
import ua.acclorite.book_story.presentation.ui.BooksHistoryResurrectionTheme
import ua.acclorite.book_story.presentation.ui.BookStoryTheme
import ua.acclorite.book_story.presentation.ui.Transitions
import ua.acclorite.book_story.presentation.ui.isDark
import ua.acclorite.book_story.presentation.ui.isPureDark
import java.lang.reflect.Field
@ -109,10 +116,25 @@ class Activity : AppCompatActivity() {
}
}
val density = LocalDensity.current
val imeInsets = WindowInsets.ime
val focusManager = LocalFocusManager.current
val isKeyboardVisible by remember {
derivedStateOf {
imeInsets.getBottom(density) > 0
}
}
LaunchedEffect(isKeyboardVisible) {
if (!isKeyboardVisible) {
focusManager.clearFocus()
}
}
if (isLoaded) {
BooksHistoryResurrectionTheme(
BookStoryTheme(
theme = state.theme!!,
isDark = state.darkTheme!!.isDark(),
isPureDark = state.pureDark!!.isPureDark(this),
themeContrast = state.themeContrast!!
) {
NavigationHost(startScreen = Screen.LIBRARY) {

View file

@ -5,5 +5,7 @@ import kotlinx.coroutines.flow.Flow
interface DataStore {
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)
}

View file

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

View file

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

View file

@ -1,6 +1,7 @@
package ua.acclorite.book_story.domain.repository
import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.History
@ -8,6 +9,7 @@ import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.presentation.data.MainState
import java.io.File
interface BookRepository {
@ -55,6 +57,8 @@ interface BookRepository {
value: T
)
suspend fun getAllSettings(scope: CoroutineScope): MainState
suspend fun getFilesFromDevice(query: String = ""): Flow<Resource<List<File>>>
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 THEME = stringPreferencesKey("theme")
val DARK_THEME = stringPreferencesKey("dark_theme")
val PURE_DARK = stringPreferencesKey("pure_dark")
val THEME_CONTRAST = stringPreferencesKey("theme_contrast")
val SHOW_START_SCREEN = booleanPreferencesKey("guide")
val BACKGROUND_COLOR = longPreferencesKey("background_color")

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,9 +10,3 @@ fun String.removeTrailingZero(): String {
fun Double.removeDigits(digits: Int) = "%.${digits}f".format(this).replace(",", ".")
fun calculateFamiliarity(string: String, target: String): Int {
val targetCounts = target.lowercase().trim().groupingBy { it }.eachCount()
val familiarity = string.lowercase().trim().sumOf { targetCounts.getOrDefault(it, 0) }
return familiarity
}

View file

@ -4,6 +4,7 @@ sealed class MainEvent {
data class OnChangeLanguage(val lang: String) : MainEvent()
data class OnChangeTheme(val theme: String) : MainEvent()
data class OnChangeDarkTheme(val darkTheme: String) : MainEvent()
data class OnChangePureDark(val pureDark: String) : MainEvent()
data class OnChangeThemeContrast(val themeContrast: String) : MainEvent()
data class OnChangeFontFamily(val fontFamily: String) : MainEvent()
data class OnChangeFontStyle(val fontStyle: Boolean) : MainEvent()

View file

@ -1,11 +1,21 @@
package ua.acclorite.book_story.presentation.data
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.domain.util.DataStoreConstants
import ua.acclorite.book_story.presentation.ui.DarkTheme
import ua.acclorite.book_story.presentation.ui.PureDark
import ua.acclorite.book_story.presentation.ui.Theme
import ua.acclorite.book_story.presentation.ui.ThemeContrast
import ua.acclorite.book_story.presentation.ui.toDarkTheme
import ua.acclorite.book_story.presentation.ui.toPureDark
import ua.acclorite.book_story.presentation.ui.toTheme
import ua.acclorite.book_story.presentation.ui.toThemeContrast
import java.util.Locale
/**
* Main State. All app's settings are here. Wrapped in SavedStateHandle, so it won't reset.
@ -15,6 +25,7 @@ data class MainState(
val language: String? = null,
val theme: Theme? = null,
val darkTheme: DarkTheme? = null,
val pureDark: PureDark? = null,
val themeContrast: ThemeContrast? = null,
val fontFamily: String? = null,
val isItalic: Boolean? = null,
@ -34,6 +45,8 @@ data class MainState(
// String
darkTheme = DarkTheme.valueOf(parcel.readString() ?: DarkTheme.FOLLOW_SYSTEM.name),
// String
pureDark = PureDark.valueOf(parcel.readString() ?: PureDark.OFF.name),
// String
themeContrast = ThemeContrast.valueOf(parcel.readString() ?: ThemeContrast.STANDARD.name),
// String
fontFamily = parcel.readString(),
@ -64,6 +77,8 @@ data class MainState(
parcel.writeString(theme?.name)
// Dark Theme
parcel.writeString(darkTheme?.name)
// Pure Dark
parcel.writeString(pureDark?.name)
// Contrast Level
parcel.writeString(themeContrast?.name)
// Font Family
@ -106,5 +121,79 @@ data class MainState(
override fun newArray(size: Int): Array<MainState?> {
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
import android.os.Build
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@ -9,23 +7,21 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ua.acclorite.book_story.domain.use_case.ChangeLanguage
import ua.acclorite.book_story.domain.use_case.GetDatastore
import ua.acclorite.book_story.domain.use_case.GetAllSettings
import ua.acclorite.book_story.domain.use_case.SetDatastore
import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.domain.util.DataStoreConstants
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.ui.DarkTheme
import ua.acclorite.book_story.presentation.ui.Theme
import ua.acclorite.book_story.presentation.ui.ThemeContrast
import ua.acclorite.book_story.presentation.ui.toDarkTheme
import ua.acclorite.book_story.presentation.ui.toPureDark
import ua.acclorite.book_story.presentation.ui.toTheme
import ua.acclorite.book_story.presentation.ui.toThemeContrast
import java.util.Locale
import javax.inject.Inject
/**
@ -35,14 +31,17 @@ import javax.inject.Inject
class MainViewModel @Inject constructor(
private val stateHandle: SavedStateHandle,
private val getDatastore: GetDatastore,
private val setDatastore: SetDatastore,
private val changeLanguage: ChangeLanguage,
private val getAllSettings: GetAllSettings
) : ViewModel() {
private val _isReady = MutableStateFlow(false)
val isReady = _isReady.asStateFlow()
private val isSettingsReady = MutableStateFlow(false)
private val isViewModelReady = MutableStateFlow(false)
private val _state: MutableStateFlow<MainState> = MutableStateFlow(
stateHandle[Constants.MAIN_STATE] ?: MainState()
)
@ -53,7 +52,7 @@ class MainViewModel @Inject constructor(
is MainEvent.OnChangeLanguage -> {
viewModelScope.launch(Dispatchers.Main) {
changeLanguage.execute(event.lang)
_state.updateWithSavedHandle {
updateStateWithSavedHandle {
it.copy(
language = event.lang
)
@ -64,7 +63,7 @@ class MainViewModel @Inject constructor(
is MainEvent.OnChangeDarkTheme -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.DARK_THEME, event.darkTheme)
_state.updateWithSavedHandle {
updateStateWithSavedHandle {
it.copy(
darkTheme = event.darkTheme.toDarkTheme()
)
@ -72,10 +71,21 @@ class MainViewModel @Inject constructor(
}
}
is MainEvent.OnChangePureDark -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.PURE_DARK, event.pureDark)
updateStateWithSavedHandle {
it.copy(
pureDark = event.pureDark.toPureDark()
)
}
}
}
is MainEvent.OnChangeThemeContrast -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.THEME_CONTRAST, event.themeContrast)
_state.updateWithSavedHandle {
updateStateWithSavedHandle {
it.copy(
themeContrast = event.themeContrast.toThemeContrast()
)
@ -86,7 +96,7 @@ class MainViewModel @Inject constructor(
is MainEvent.OnChangeTheme -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.THEME, event.theme)
_state.updateWithSavedHandle {
updateStateWithSavedHandle {
it.copy(
theme = event.theme.toTheme()
)
@ -97,7 +107,7 @@ class MainViewModel @Inject constructor(
is MainEvent.OnChangeFontFamily -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.FONT, event.fontFamily)
_state.updateWithSavedHandle {
updateStateWithSavedHandle {
it.copy(
fontFamily = Constants.FONTS.find { font -> font.id == event.fontFamily }?.id
?: Constants.FONTS[0].id
@ -109,7 +119,7 @@ class MainViewModel @Inject constructor(
is MainEvent.OnChangeFontStyle -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.IS_ITALIC, event.fontStyle)
_state.updateWithSavedHandle {
updateStateWithSavedHandle {
it.copy(
isItalic = event.fontStyle
)
@ -120,7 +130,7 @@ class MainViewModel @Inject constructor(
is MainEvent.OnChangeFontSize -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.FONT_SIZE, event.fontSize)
_state.updateWithSavedHandle {
updateStateWithSavedHandle {
it.copy(
fontSize = event.fontSize
)
@ -131,7 +141,7 @@ class MainViewModel @Inject constructor(
is MainEvent.OnChangeLineHeight -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.LINE_HEIGHT, event.lineHeight)
_state.updateWithSavedHandle {
updateStateWithSavedHandle {
it.copy(
lineHeight = event.lineHeight
)
@ -142,7 +152,7 @@ class MainViewModel @Inject constructor(
is MainEvent.OnChangeParagraphHeight -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.PARAGRAPH_HEIGHT, event.paragraphHeight)
_state.updateWithSavedHandle {
updateStateWithSavedHandle {
it.copy(
paragraphHeight = event.paragraphHeight
)
@ -153,7 +163,7 @@ class MainViewModel @Inject constructor(
is MainEvent.OnChangeParagraphIndentation -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.PARAGRAPH_INDENTATION, event.bool)
_state.updateWithSavedHandle {
updateStateWithSavedHandle {
it.copy(
paragraphIndentation = event.bool
)
@ -164,7 +174,7 @@ class MainViewModel @Inject constructor(
is MainEvent.OnChangeBackgroundColor -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.BACKGROUND_COLOR, event.color)
_state.updateWithSavedHandle {
updateStateWithSavedHandle {
it.copy(
backgroundColor = event.color
)
@ -175,7 +185,7 @@ class MainViewModel @Inject constructor(
is MainEvent.OnChangeFontColor -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.FONT_COLOR, event.color)
_state.updateWithSavedHandle {
updateStateWithSavedHandle {
it.copy(
fontColor = event.color
)
@ -186,7 +196,7 @@ class MainViewModel @Inject constructor(
is MainEvent.OnChangeShowStartScreen -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.SHOW_START_SCREEN, event.bool)
_state.updateWithSavedHandle {
updateStateWithSavedHandle {
it.copy(
showStartScreen = event.bool
)
@ -199,204 +209,54 @@ class MainViewModel @Inject constructor(
fun init(
libraryViewModel: LibraryViewModel,
) {
val isViewModelsReady = combine(
libraryViewModel.isReady,
) { values ->
values.all { it }
viewModelScope.launch(Dispatchers.Main) {
val settings = getAllSettings.execute(viewModelScope)
// All additional execution
changeLanguage.execute(settings.language!!)
updateStateWithSavedHandle {
settings
}
isSettingsReady.update { true }
}
val isDataReady = combine(
_state
) {
val value = it.first()
return@combine value.language != null &&
value.theme != null &&
value.darkTheme != null &&
value.themeContrast != null &&
value.fontFamily != null &&
value.isItalic != null &&
value.fontSize != null &&
value.lineHeight != null &&
value.paragraphHeight != null &&
value.paragraphIndentation != null &&
value.backgroundColor != null &&
value.fontColor != null &&
value.showStartScreen != null
}
val isReady = combine(
isViewModelsReady,
isDataReady
) { values ->
values.all { it }
}
viewModelScope.launch {
isReady.collect { bool ->
if (bool) {
_isReady.update {
true
}
return@collect
viewModelScope.launch(Dispatchers.IO) {
libraryViewModel.isReady.collectLatest { ready ->
isViewModelReady.update {
ready
}
}
}
// Language
viewModelScope.launch(Dispatchers.Main) {
getDatastore
.execute(
DataStoreConstants.LANGUAGE,
if (Constants.LANGUAGES.any { Locale.getDefault().language.take(2) == it.first }) {
Locale.getDefault().language.take(2)
} else {
"en"
val isReady = combine(
isViewModelReady,
isSettingsReady
) { values ->
values.all { it }
}
viewModelScope.launch(Dispatchers.IO) {
isReady.first { bool ->
if (bool) {
_isReady.update {
true
}
)
.first {
onEvent(MainEvent.OnChangeLanguage(it))
it.isNotBlank()
}
bool
}
}
// Theme
viewModelScope.launch(Dispatchers.IO) {
getDatastore
.execute(
DataStoreConstants.THEME,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Theme.DYNAMIC.name
else Theme.BLUE.name
)
.first {
onEvent(MainEvent.OnChangeTheme(it))
it.isNotBlank()
}
}
// Dark Theme
viewModelScope.launch(Dispatchers.IO) {
getDatastore
.execute(DataStoreConstants.DARK_THEME, DarkTheme.FOLLOW_SYSTEM.name)
.first {
onEvent(MainEvent.OnChangeDarkTheme(it))
it.isNotBlank()
}
}
// Theme Contrast
viewModelScope.launch(Dispatchers.IO) {
getDatastore
.execute(DataStoreConstants.THEME_CONTRAST, ThemeContrast.STANDARD.name)
.first {
onEvent(MainEvent.OnChangeThemeContrast(it))
it.isNotBlank()
}
}
// Show Start Screen
viewModelScope.launch(Dispatchers.IO) {
getDatastore
.execute(DataStoreConstants.SHOW_START_SCREEN, true)
.first {
onEvent(MainEvent.OnChangeShowStartScreen(it))
true
}
}
// Background Color
viewModelScope.launch(Dispatchers.IO) {
getDatastore
.execute(DataStoreConstants.BACKGROUND_COLOR, Color.DarkGray.value.toLong())
.first {
onEvent(MainEvent.OnChangeBackgroundColor(it))
true
}
}
// Font Color
viewModelScope.launch(Dispatchers.IO) {
getDatastore
.execute(DataStoreConstants.FONT_COLOR, Color.LightGray.value.toLong())
.first {
onEvent(MainEvent.OnChangeFontColor(it))
true
}
}
// Font Family
viewModelScope.launch(Dispatchers.IO) {
getDatastore
.execute(DataStoreConstants.FONT, Constants.FONTS[0].id)
.first {
onEvent(MainEvent.OnChangeFontFamily(it))
true
}
}
// Font Style
viewModelScope.launch(Dispatchers.IO) {
getDatastore
.execute(DataStoreConstants.IS_ITALIC, false)
.first {
onEvent(MainEvent.OnChangeFontStyle(it))
true
}
}
// Font Size
viewModelScope.launch(Dispatchers.IO) {
getDatastore
.execute(DataStoreConstants.FONT_SIZE, 16)
.first {
onEvent(MainEvent.OnChangeFontSize(it))
it > 0
}
}
// Line Height
viewModelScope.launch(Dispatchers.IO) {
getDatastore
.execute(DataStoreConstants.LINE_HEIGHT, 4)
.first {
onEvent(MainEvent.OnChangeLineHeight(it))
true
}
}
// Paragraph Height
viewModelScope.launch(Dispatchers.IO) {
getDatastore
.execute(DataStoreConstants.PARAGRAPH_HEIGHT, 8)
.first {
onEvent(MainEvent.OnChangeParagraphHeight(it))
true
}
}
// Paragraph Indentation
viewModelScope.launch(Dispatchers.IO) {
getDatastore
.execute(DataStoreConstants.PARAGRAPH_INDENTATION, false)
.first {
onEvent(MainEvent.OnChangeParagraphIndentation(it))
true
}
}
}
/**
* Updates [MutableStateFlow] along with [SavedStateHandle].
* Updates [MainState] along with [SavedStateHandle].
*/
private fun <T> MutableStateFlow<T>.updateWithSavedHandle(
const: String = Constants.MAIN_STATE,
function: (T) -> T
private fun updateStateWithSavedHandle(
function: (MainState) -> MainState
) {
val nextValue = function(value)
update {
stateHandle[const] = nextValue
nextValue
_state.update {
stateHandle[Constants.MAIN_STATE] = function(it)
function(it)
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.CustomTooltip
import ua.acclorite.book_story.presentation.ui.SlidingTransition
@OptIn(ExperimentalLayoutApi::class)
/**
* When clicked returns a Tag from [tags] (if present in [text])
*/
@Composable
fun HelpClickableNote() {
val context = LocalContext.current
Column {
Icon(
imageVector = Icons.Outlined.Info,
contentDescription = stringResource(id = R.string.note_content_desc),
modifier = Modifier
.size(20.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(8.dp))
FlowRow(verticalArrangement = Arrangement.spacedBy(1.dp)) {
Text(
stringResource(id = R.string.clickable_note_1) + " ",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.align(Alignment.CenterVertically)
)
Text(
stringResource(id = R.string.clickable_note_2) + " ",
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.tertiary,
fun LazyItemScope.HelpClickableNote(
text: AnnotatedString,
tags: List<String> = emptyList(),
isNoteFullyShown: Boolean,
onIconClick: () -> Unit,
onTagClick: (String) -> Unit
) {
Column(Modifier.animateItem()) {
CustomTooltip(text = stringResource(R.string.note_content_desc)) {
Icon(
imageVector = Icons.Outlined.Info,
modifier = Modifier
.align(Alignment.CenterVertically)
.clickable(
interactionSource = null,
indication = null,
onClick = {
Toast
.makeText(
context,
context.getString(R.string.clickable_note_action),
Toast.LENGTH_SHORT
)
.show()
}
)
)
Text(
stringResource(id = R.string.clickable_note_3),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.align(Alignment.CenterVertically)
.size(20.dp)
.clickable(interactionSource = null, indication = null) {
onIconClick()
},
contentDescription = stringResource(R.string.note_content_desc),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
SlidingTransition(visible = isNoteFullyShown) {
Column {
Spacer(modifier = Modifier.height(8.dp))
ClickableText(
text = text,
style = MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant
)
) { offset ->
tags.forEach { tag ->
text.getStringAnnotations(tag = tag, start = offset, end = offset)
.firstOrNull()?.let {
onTagClick(tag)
}
}
}
}
}
}
}

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
sealed class HelpEvent {
data class OnSearchInWeb(
val context: Context,
val noAppsFound: () -> Unit
) : HelpEvent()
data class OnNavigateToBrowserPage(
val page: String,
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
import android.app.SearchManager
import android.content.Intent
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -13,6 +17,9 @@ class HelpViewModel @Inject constructor(
) : ViewModel() {
private val _state = MutableStateFlow(HelpState())
val state = _state.asStateFlow()
fun onEvent(event: HelpEvent) {
when (event) {
is HelpEvent.OnNavigateToBrowserPage -> {
@ -30,6 +37,40 @@ class HelpViewModel @Inject constructor(
event.noAppsFound()
}
}
is HelpEvent.OnSearchInWeb -> {
viewModelScope.launch {
if (_state.value.textFieldValue.isBlank()) {
_state.update {
it.copy(
showError = true
)
}
return@launch
}
val intent = Intent()
intent.action = Intent.ACTION_WEB_SEARCH
intent.putExtra(
SearchManager.QUERY,
"${_state.value.textFieldValue.trim()} filetype:txt OR filetype:pdf"
)
if (intent.resolveActivity(event.context.packageManager) != null) {
event.context.startActivity(intent)
return@launch
}
event.noAppsFound()
}
}
}
}
fun onUpdate(calculation: (HelpState) -> HelpState) {
_state.update {
calculation(it)
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,7 +5,9 @@ import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.slideOutVertically
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -13,20 +15,20 @@ object Transitions {
val DefaultTransitionIn = fadeIn(tween(300))
val DefaultTransitionOut = fadeOut(tween(300))
val FadeTransitionIn = fadeIn(tween(300))
val FadeTransitionOut = fadeOut(tween(50))
val FadeTransitionIn = fadeIn(tween(350))
val FadeTransitionOut = fadeOut(tween(100))
val SlidingTransitionIn = fadeIn(tween(300)) +
slideInHorizontally(tween(300)) { it / 16 }
val SlidingTransitionIn = fadeIn(tween(350)) +
slideInHorizontally(tween(350)) { it / 16 }
val BackSlidingTransitionIn = fadeIn(tween(300)) +
slideInHorizontally(tween(300)) { -it / 16 }
val BackSlidingTransitionIn = fadeIn(tween(350)) +
slideInHorizontally(tween(350)) { -it / 16 }
val SlidingTransitionOut = fadeOut(tween(200)) +
slideOutHorizontally(tween(300)) { -it / 16 }
val SlidingTransitionOut = fadeOut(tween(250)) +
slideOutHorizontally(tween(350)) { -it / 16 }
val BackSlidingTransitionOut = fadeOut(tween(200)) +
slideOutHorizontally(tween(300)) { it / 16 }
val BackSlidingTransitionOut = fadeOut(tween(250)) +
slideOutHorizontally(tween(350)) { it / 16 }
}
@Composable
@ -45,6 +47,24 @@ fun DefaultTransition(
}
}
@Composable
fun SlidingTransition(
visible: Boolean,
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
AnimatedVisibility(
visible = visible,
modifier = modifier,
enter = slideInVertically(tween(300)) { -it / 10 } +
fadeIn(tween(300)),
exit = slideOutVertically(tween(150)) { -it / 10 } +
fadeOut(tween(100)),
) {
content()
}
}

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">
<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="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>

View file

@ -1,7 +1,12 @@
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Basic Strings -->
<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 -->
<string name="library_screen">Бібліотека</string>
<string name="history_screen">Історія</string>
@ -108,7 +113,7 @@
<string name="continue_read">Продовжити</string>
<string name="never">Ніколи</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="undo">Скасувати</string>
<string name="copy">Копіювати</string>
@ -160,6 +165,7 @@
<!-- Settings options -->
<string name="language_option">Мова застосунка</string>
<string name="dark_theme_option">Темна тема</string>
<string name="pure_dark_option">Суцільна темрява (ОЛЕД)</string>
<string name="theme_contrast_option">Контрастність теми</string>
<string name="app_theme_option">Тема застосунка</string>
<string name="font_family_option">Сімейство шрифтів</string>
@ -181,6 +187,11 @@
<string name="dark_theme_on">Ввімкнена</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 -->
<string name="theme_contrast_standard">Типова</string>
<string name="theme_contrast_medium">Середня</string>
@ -257,10 +268,106 @@
<string name="slava_ukraini_option_desc">Героям Слава! 🐉</string>
<!-- Help -->
<string name="clickable_note_1">Будь ласка, зверніть увагу, що на</string>
<string name="clickable_note_2">такий виділений текст</string>
<string name="clickable_note_3">можна натиснути.</string>
<string name="clickable_note_action">На мене можна натиснути!</string>
<string name="help_clickable_note_1">Будь ласка, зверніть увагу, що на</string>
<string name="help_clickable_note_2">такий виділений текст</string>
<string name="help_clickable_note_3">
можна натиснути.
Також ви можете приховати цю примітку, натиснувши на іконку, спробуйте!
</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 -->
<string name="default_font">За замовчуванням</string>
@ -294,6 +401,11 @@
<string name="app_icon_content_desc">Іконка застосунку</string>
<string name="note_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>

View file

@ -1,7 +1,9 @@
<resources>
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="InconsistentArrays">
<!-- 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>
<!-- About sites -->
<string name="releases_page" translatable="false">
https://github.com/Acclorite/book-story/releases/latest
</string>
@ -18,6 +20,13 @@
https://www.buymeacoffee.com/acclorite
</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 -->
<string name="library_screen">Library</string>
<string name="history_screen">History</string>
@ -123,7 +132,7 @@
<string name="continue_read">Continue</string>
<string name="never">Never</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="undo">Undo</string>
<string name="copy">Copy</string>
@ -175,6 +184,7 @@
<!-- Settings options -->
<string name="language_option">App language</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="app_theme_option">App theme</string>
<string name="font_family_option">Font family</string>
@ -196,6 +206,11 @@
<string name="dark_theme_on">Enabled</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 -->
<string name="theme_contrast_standard">Standard</string>
<string name="theme_contrast_medium">Medium</string>
@ -272,10 +287,105 @@
<string name="slava_ukraini_option_desc">Heroyam Slava! 🐉</string>
<!-- Help -->
<string name="clickable_note_1">Please note, that</string>
<string name="clickable_note_2">such highlighted text</string>
<string name="clickable_note_3">can be clicked.</string>
<string name="clickable_note_action">I\'m clickable!</string>
<string name="help_clickable_note_1">Please note, that</string>
<string name="help_clickable_note_2">such highlighted text</string>
<string name="help_clickable_note_3">
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 -->
<string name="default_font">Default</string>
@ -309,5 +419,10 @@
<string name="app_icon_content_desc">App icon</string>
<string name="note_content_desc">Note</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>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<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:windowContentTransitions">false</item>
@ -12,7 +12,7 @@
<style name="Theme.Start.Splash" parent="Theme.SplashScreen">
<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="windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>