ColorPresets + Room Migration 4 → 5.

This commit is contained in:
acclorite 2024-07-27 16:58:25 +03:00
parent 945634c653
commit 5b8ddb4dfb
41 changed files with 1694 additions and 140 deletions

View file

@ -38,6 +38,7 @@ import ua.acclorite.book_story.presentation.screens.library.LibraryScreenRoot
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.screens.reader.ReaderScreenRoot
import ua.acclorite.book_story.presentation.screens.settings.SettingsScreenRoot
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsViewModel
import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.AppearanceSettingsRoot
import ua.acclorite.book_story.presentation.screens.settings.nested.general.GeneralSettingsRoot
import ua.acclorite.book_story.presentation.screens.settings.nested.reader.ReaderSettingsRoot
@ -53,11 +54,12 @@ import java.lang.reflect.Field
@SuppressLint("DiscouragedPrivateApi")
@AndroidEntryPoint
class Activity : AppCompatActivity() {
// Initializing all viewModels on app startup
// Initializing all required viewModels on app startup
private val mainViewModel: MainViewModel by viewModels()
private val libraryViewModel: LibraryViewModel by viewModels()
private val historyViewModel: HistoryViewModel by viewModels()
private val browseViewModel: BrowseViewModel by viewModels()
private val settingsViewModel: SettingsViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -72,7 +74,7 @@ class Activity : AppCompatActivity() {
}
// Initializing all variables
mainViewModel.init(libraryViewModel)
mainViewModel.init(libraryViewModel, settingsViewModel)
// Splash screen
installSplashScreen().apply {

View file

@ -43,6 +43,7 @@ object AppModule {
)
.addMigrations(
DatabaseHelper.MIGRATION_2_3, // creates LanguageHistoryEntity table(if does not exist)
DatabaseHelper.MIGRATION_4_5, // creates ColorPresetEntity table(if does not exist)
)
.allowMainThreadQueries()
.build()

View file

@ -10,6 +10,8 @@ import ua.acclorite.book_story.data.local.notification.UpdatesNotificationServic
import ua.acclorite.book_story.data.local.notification.UpdatesNotificationServiceImpl
import ua.acclorite.book_story.data.mapper.book.BookMapper
import ua.acclorite.book_story.data.mapper.book.BookMapperImpl
import ua.acclorite.book_story.data.mapper.color_preset.ColorPresetMapper
import ua.acclorite.book_story.data.mapper.color_preset.ColorPresetMapperImpl
import ua.acclorite.book_story.data.mapper.history.HistoryMapper
import ua.acclorite.book_story.data.mapper.history.HistoryMapperImpl
import ua.acclorite.book_story.data.parser.FileParser
@ -47,6 +49,12 @@ abstract class RepositoryModule {
historyMapperImpl: HistoryMapperImpl
): HistoryMapper
@Binds
@Singleton
abstract fun bindColorPresetMapper(
colorPresetMapperImpl: ColorPresetMapperImpl
): ColorPresetMapper
@Binds
@Singleton
abstract fun bindFileParser(

View file

@ -0,0 +1,15 @@
package ua.acclorite.book_story.data.local.dto
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class ColorPresetEntity(
@PrimaryKey(true)
val id: Int? = null,
val name: String?,
val backgroundColor: Long,
val fontColor: Long,
val isSelected: Boolean,
val order: Int
)

View file

@ -6,7 +6,9 @@ import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import androidx.room.Upsert
import ua.acclorite.book_story.data.local.dto.BookEntity
import ua.acclorite.book_story.data.local.dto.ColorPresetEntity
import ua.acclorite.book_story.data.local.dto.HistoryEntity
@Dao
@ -57,4 +59,23 @@ interface BookDao {
@Delete
suspend fun deleteHistory(history: List<HistoryEntity>)
@Upsert
suspend fun updateColorPreset(colorPreset: ColorPresetEntity)
@Query("SELECT `order` FROM colorpresetentity WHERE :id=id")
suspend fun getColorPresetOrder(id: Int): Int
@Query("SELECT COUNT(*) FROM colorpresetentity")
suspend fun getColorPresetsSize(): Int
@Query("SELECT * FROM colorpresetentity")
suspend fun getColorPresets(): List<ColorPresetEntity>
@Delete
suspend fun deleteColorPreset(colorPreset: ColorPresetEntity)
@Query("DELETE FROM colorpresetentity")
suspend fun deleteColorPresets()
}

View file

@ -9,18 +9,21 @@ import androidx.room.migration.AutoMigrationSpec
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import ua.acclorite.book_story.data.local.dto.BookEntity
import ua.acclorite.book_story.data.local.dto.ColorPresetEntity
import ua.acclorite.book_story.data.local.dto.HistoryEntity
@Database(
entities = [
BookEntity::class,
HistoryEntity::class
HistoryEntity::class,
ColorPresetEntity::class,
],
version = 4,
version = 5,
autoMigrations = [
AutoMigration(1, 2),
AutoMigration(2, 3),
AutoMigration(3, 4, spec = DatabaseHelper.MIGRATION_3_4::class),
AutoMigration(4, 5),
],
exportSchema = true
)
@ -50,4 +53,19 @@ object DatabaseHelper {
@DeleteColumn("BookEntity", "translateWhenOpen")
@DeleteTable("LanguageHistoryEntity")
class MIGRATION_3_4 : AutoMigrationSpec
val MIGRATION_4_5 = object : Migration(4, 5) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"CREATE TABLE IF NOT EXISTS `ColorPresetEntity` (" +
"`id` INTEGER PRIMARY KEY AUTOINCREMENT, " +
"`name` TEXT, " +
"`backgroundColor` INTEGER NOT NULL, " +
"`fontColor` INTEGER NOT NULL, " +
"`isSelected` INTEGER NOT NULL, " +
"`order` INTEGER NOT NULL" +
")"
)
}
}
}

View file

@ -0,0 +1,10 @@
package ua.acclorite.book_story.data.mapper.color_preset
import ua.acclorite.book_story.data.local.dto.ColorPresetEntity
import ua.acclorite.book_story.domain.model.ColorPreset
interface ColorPresetMapper {
suspend fun toColorPresetEntity(colorPreset: ColorPreset, order: Int): ColorPresetEntity
suspend fun toColorPreset(colorPresetEntity: ColorPresetEntity): ColorPreset
}

View file

@ -0,0 +1,34 @@
package ua.acclorite.book_story.data.mapper.color_preset
import androidx.compose.ui.graphics.Color
import ua.acclorite.book_story.data.local.dto.ColorPresetEntity
import ua.acclorite.book_story.domain.model.ColorPreset
import javax.inject.Inject
class ColorPresetMapperImpl @Inject constructor() : ColorPresetMapper {
override suspend fun toColorPresetEntity(
colorPreset: ColorPreset,
order: Int
): ColorPresetEntity {
return ColorPresetEntity(
id = if (colorPreset.id != -1) colorPreset.id
else null,
name = colorPreset.name,
backgroundColor = colorPreset.backgroundColor.value.toLong(),
fontColor = colorPreset.fontColor.value.toLong(),
isSelected = colorPreset.isSelected,
order = order
)
}
override suspend fun toColorPreset(colorPresetEntity: ColorPresetEntity): ColorPreset {
return ColorPreset(
id = colorPresetEntity.id!!,
name = colorPresetEntity.name,
backgroundColor = Color(colorPresetEntity.backgroundColor.toULong()),
fontColor = Color(colorPresetEntity.fontColor.toULong()),
isSelected = colorPresetEntity.isSelected
)
}
}

View file

@ -22,12 +22,14 @@ import ua.acclorite.book_story.data.local.data_store.DataStore
import ua.acclorite.book_story.data.local.notification.UpdatesNotificationService
import ua.acclorite.book_story.data.local.room.BookDao
import ua.acclorite.book_story.data.mapper.book.BookMapper
import ua.acclorite.book_story.data.mapper.color_preset.ColorPresetMapper
import ua.acclorite.book_story.data.mapper.history.HistoryMapper
import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.data.remote.GithubAPI
import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.ColorPreset
import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.domain.repository.BookRepository
@ -55,6 +57,7 @@ class BookRepositoryImpl @Inject constructor(
private val bookMapper: BookMapper,
private val historyMapper: HistoryMapper,
private val colorPresetMapper: ColorPresetMapper,
private val fileParser: FileParser,
private val textParser: TextParser
@ -667,6 +670,50 @@ class BookRepositoryImpl @Inject constructor(
}
}
}
override suspend fun updateColorPreset(colorPreset: ColorPreset) {
database.updateColorPreset(
colorPresetMapper.toColorPresetEntity(
colorPreset,
if (colorPreset.id != -1) database.getColorPresetOrder(colorPreset.id)
else database.getColorPresetsSize()
)
)
}
override suspend fun selectColorPreset(colorPreset: ColorPreset) {
database.getColorPresets().map {
it.copy(
isSelected = it.id == colorPreset.id
)
}.forEach {
database.updateColorPreset(it)
}
}
override suspend fun getColorPresets(): List<ColorPreset> {
return database.getColorPresets()
.sortedBy { it.order }
.map { colorPresetMapper.toColorPreset(it) }
}
override suspend fun reorderColorPresets(orderedColorPresets: List<ColorPreset>) {
database.deleteColorPresets()
orderedColorPresets.forEachIndexed { index, colorPreset ->
database.updateColorPreset(
colorPresetMapper.toColorPresetEntity(colorPreset, order = index)
)
}
}
override suspend fun deleteColorPreset(colorPreset: ColorPreset) {
database.deleteColorPreset(
colorPresetMapper.toColorPresetEntity(
colorPreset, -1
)
)
}
}

View file

@ -0,0 +1,14 @@
package ua.acclorite.book_story.domain.model
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import ua.acclorite.book_story.domain.util.Selected
@Immutable
data class ColorPreset(
val id: Int,
val name: String?,
val backgroundColor: Color,
val fontColor: Color,
val isSelected: Selected
)

View file

@ -5,6 +5,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.ColorPreset
import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.domain.util.CoverImage
@ -89,5 +90,17 @@ interface BookRepository {
history: List<History>
)
suspend fun checkForUpdates(postNotification: Boolean): LatestReleaseInfo?
suspend fun updateColorPreset(colorPreset: ColorPreset)
suspend fun selectColorPreset(colorPreset: ColorPreset)
suspend fun getColorPresets(): List<ColorPreset>
suspend fun reorderColorPresets(orderedColorPresets: List<ColorPreset>)
suspend fun deleteColorPreset(colorPreset: ColorPreset)
}

View file

@ -0,0 +1,12 @@
package ua.acclorite.book_story.domain.use_case
import ua.acclorite.book_story.domain.model.ColorPreset
import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Inject
class DeleteColorPreset @Inject constructor(private val repository: BookRepository) {
suspend fun execute(colorPreset: ColorPreset) {
repository.deleteColorPreset(colorPreset)
}
}

View file

@ -0,0 +1,12 @@
package ua.acclorite.book_story.domain.use_case
import ua.acclorite.book_story.domain.model.ColorPreset
import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Inject
class GetColorPresets @Inject constructor(private val repository: BookRepository) {
suspend fun execute(): List<ColorPreset> {
return repository.getColorPresets()
}
}

View file

@ -0,0 +1,12 @@
package ua.acclorite.book_story.domain.use_case
import ua.acclorite.book_story.domain.model.ColorPreset
import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Inject
class ReorderColorPresets @Inject constructor(private val repository: BookRepository) {
suspend fun execute(reorderedColorPresets: List<ColorPreset>) {
repository.reorderColorPresets(reorderedColorPresets)
}
}

View file

@ -0,0 +1,12 @@
package ua.acclorite.book_story.domain.use_case
import ua.acclorite.book_story.domain.model.ColorPreset
import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Inject
class SelectColorPreset @Inject constructor(private val repository: BookRepository) {
suspend fun execute(colorPreset: ColorPreset) {
repository.selectColorPreset(colorPreset)
}
}

View file

@ -0,0 +1,12 @@
package ua.acclorite.book_story.domain.use_case
import ua.acclorite.book_story.domain.model.ColorPreset
import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Inject
class UpdateColorPreset @Inject constructor(private val repository: BookRepository) {
suspend fun execute(colorPreset: ColorPreset) {
repository.updateColorPreset(colorPreset)
}
}

View file

@ -2,6 +2,7 @@ package ua.acclorite.book_story.domain.util
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
@ -10,6 +11,7 @@ import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Badge
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.model.ColorPreset
import ua.acclorite.book_story.domain.model.Credit
import ua.acclorite.book_story.domain.model.FontWithName
import ua.acclorite.book_story.domain.model.NavigationItem
@ -295,4 +297,12 @@ object Constants {
lastOpened = null,
category = Category.READING
)
val DEFAULT_COLOR_PRESET = ColorPreset(
id = -1,
name = null,
backgroundColor = Color.DarkGray,
fontColor = Color.LightGray,
isSelected = false
)
}

View file

@ -2,7 +2,6 @@ package ua.acclorite.book_story.domain.util
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
object DataStoreConstants {
@ -13,8 +12,6 @@ object DataStoreConstants {
val PURE_DARK = stringPreferencesKey("pure_dark")
val THEME_CONTRAST = stringPreferencesKey("theme_contrast")
val SHOW_START_SCREEN = booleanPreferencesKey("guide")
val BACKGROUND_COLOR = longPreferencesKey("background_color")
val FONT_COLOR = longPreferencesKey("font_color")
val FONT = stringPreferencesKey("font")
val IS_ITALIC = booleanPreferencesKey("font_style")
val FONT_SIZE = intPreferencesKey("font_size")
@ -24,4 +21,5 @@ object DataStoreConstants {
val CHECK_FOR_UPDATES = booleanPreferencesKey("check_for_updates")
val SIDE_PADDING = intPreferencesKey("side_padding")
val DOUBLE_CLICK_TRANSLATION = booleanPreferencesKey("double_click_translation")
val FAST_COLOR_PRESET_CHANGE = booleanPreferencesKey("fast_color_preset_change")
}

View file

@ -20,11 +20,11 @@ fun <T> LazyListScope.customItems(
fun <T> LazyListScope.customItemsIndexed(
items: List<T>,
key: (T) -> Any?,
key: (T, Int) -> Any?,
content: @Composable LazyItemScope.(Int, T) -> Unit
) {
for ((index, item) in items.withIndex()) {
item(key = key(item)) {
item(key = key(item, index)) {
content(index, item)
}
}

View file

@ -12,10 +12,9 @@ sealed class MainEvent {
data class OnChangeLineHeight(val lineHeight: Int) : MainEvent()
data class OnChangeParagraphHeight(val paragraphHeight: Int) : MainEvent()
data class OnChangeParagraphIndentation(val bool: Boolean) : MainEvent()
data class OnChangeBackgroundColor(val color: Long) : MainEvent()
data class OnChangeFontColor(val color: Long) : MainEvent()
data class OnChangeShowStartScreen(val bool: Boolean) : MainEvent()
data class OnChangeCheckForUpdates(val bool: Boolean) : MainEvent()
data class OnChangeSidePadding(val sidePadding: Int) : MainEvent()
data class OnChangeDoubleClickTranslation(val bool: Boolean) : MainEvent()
data class OnChangeFastColorPresetChange(val bool: Boolean) : MainEvent()
}

View file

@ -4,7 +4,6 @@ import android.os.Build
import android.os.Parcelable
import androidx.annotation.Keep
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import kotlinx.parcelize.Parcelize
import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.domain.util.DataStoreConstants
@ -38,12 +37,11 @@ data class MainState(
val lineHeight: Int? = null,
val paragraphHeight: Int? = null,
val paragraphIndentation: Boolean? = null,
val backgroundColor: Long? = null,
val fontColor: Long? = null,
val showStartScreen: Boolean? = null,
val checkForUpdates: Boolean? = null,
val sidePadding: Int? = null,
val doubleClickTranslation: Boolean? = null
val doubleClickTranslation: Boolean? = null,
val fastColorPresetChange: Boolean? = null,
) : Parcelable {
companion object {
/**
@ -75,12 +73,6 @@ data class MainState(
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
@ -109,6 +101,10 @@ data class MainState(
data[DOUBLE_CLICK_TRANSLATION.name] as? Boolean
?: false
val fastColorPresetChange: Boolean =
data[FAST_COLOR_PRESET_CHANGE.name] as? Boolean
?: true
return MainState(
language = language,
theme = theme.toTheme(),
@ -116,8 +112,6 @@ data class MainState(
pureDark = pureDark.toPureDark(),
themeContrast = themeContrast.toThemeContrast(),
showStartScreen = showStartScreen,
backgroundColor = backgroundColor,
fontColor = fontColor,
fontFamily = fontFamily,
isItalic = isItalic,
fontSize = fontSize,
@ -126,7 +120,8 @@ data class MainState(
paragraphIndentation = paragraphIndentation,
checkForUpdates = checkForUpdates,
sidePadding = sidePadding,
doubleClickTranslation = doubleClickTranslation
doubleClickTranslation = doubleClickTranslation,
fastColorPresetChange = fastColorPresetChange
)
}
}

View file

@ -19,6 +19,7 @@ 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.screens.settings.data.SettingsViewModel
import ua.acclorite.book_story.presentation.ui.toDarkTheme
import ua.acclorite.book_story.presentation.ui.toPureDark
import ua.acclorite.book_story.presentation.ui.toTheme
@ -173,28 +174,6 @@ class MainViewModel @Inject constructor(
}
}
is MainEvent.OnChangeBackgroundColor -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.BACKGROUND_COLOR, event.color)
updateStateWithSavedHandle {
it.copy(
backgroundColor = event.color
)
}
}
}
is MainEvent.OnChangeFontColor -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.FONT_COLOR, event.color)
updateStateWithSavedHandle {
it.copy(
fontColor = event.color
)
}
}
}
is MainEvent.OnChangeShowStartScreen -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.SHOW_START_SCREEN, event.bool)
@ -238,11 +217,23 @@ class MainViewModel @Inject constructor(
}
}
}
is MainEvent.OnChangeFastColorPresetChange -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.FAST_COLOR_PRESET_CHANGE, event.bool)
updateStateWithSavedHandle {
it.copy(
fastColorPresetChange = event.bool
)
}
}
}
}
}
fun init(
libraryViewModel: LibraryViewModel
libraryViewModel: LibraryViewModel,
settingsViewModel: SettingsViewModel,
) {
viewModelScope.launch(Dispatchers.Main) {
val settings = getAllSettings.execute(viewModelScope)
@ -265,7 +256,12 @@ class MainViewModel @Inject constructor(
}
viewModelScope.launch(Dispatchers.IO) {
libraryViewModel.isReady.collectLatest { ready ->
combine(
libraryViewModel.isReady,
settingsViewModel.isReady
) { (libraryViewModelReady, settingsViewModelReady) ->
libraryViewModelReady && settingsViewModelReady
}.collectLatest { ready ->
isViewModelReady.update {
ready
}

View file

@ -5,6 +5,7 @@ import android.os.Build
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
@ -34,7 +35,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
@ -67,11 +67,15 @@ import ua.acclorite.book_story.presentation.screens.reader.components.ReaderEndI
import ua.acclorite.book_story.presentation.screens.reader.components.ReaderTextParagraph
import ua.acclorite.book_story.presentation.screens.reader.components.app_bar.ReaderBottomBar
import ua.acclorite.book_story.presentation.screens.reader.components.app_bar.ReaderTopBar
import ua.acclorite.book_story.presentation.screens.reader.components.readerFastColorPresetChange
import ua.acclorite.book_story.presentation.screens.reader.components.settings_bottom_sheet.ReaderSettingsBottomSheet
import ua.acclorite.book_story.presentation.screens.reader.components.start_item.ReaderStartItem
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderState
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsEvent
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsState
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsViewModel
@Composable
fun ReaderScreenRoot(screen: Screen.Reader) {
@ -82,9 +86,11 @@ fun ReaderScreenRoot(screen: Screen.Reader) {
val mainViewModel: MainViewModel = hiltViewModel()
val libraryViewModel: LibraryViewModel = hiltViewModel()
val historyViewModel: HistoryViewModel = hiltViewModel()
val settingsViewModel: SettingsViewModel = hiltViewModel()
val state = viewModel.state.collectAsState()
val mainState = mainViewModel.state.collectAsState()
val settingsState = settingsViewModel.state.collectAsState()
val lazyListState = rememberSaveable(
state.value.listState,
@ -137,10 +143,12 @@ fun ReaderScreenRoot(screen: Screen.Reader) {
ReaderScreen(
state = state,
mainState = mainState,
settingsState = settingsState,
lazyListState = lazyListState,
onNavigate = { navigator.it() },
onEvent = viewModel::onEvent,
onMainEvent = mainViewModel::onEvent,
onSettingsEvent = settingsViewModel::onEvent,
onLibraryEvent = libraryViewModel::onEvent,
onHistoryEvent = historyViewModel::onEvent
)
@ -151,10 +159,12 @@ fun ReaderScreenRoot(screen: Screen.Reader) {
private fun ReaderScreen(
state: State<ReaderState>,
mainState: State<MainState>,
settingsState: State<SettingsState>,
lazyListState: LazyListState,
onNavigate: OnNavigate,
onEvent: (ReaderEvent) -> Unit,
onMainEvent: (MainEvent) -> Unit,
onSettingsEvent: (SettingsEvent) -> Unit,
onLibraryEvent: (LibraryEvent) -> Unit,
onHistoryEvent: (HistoryEvent) -> Unit,
) {
@ -186,12 +196,14 @@ private fun ReaderScreen(
it.id == mainState.value.fontFamily
} ?: Constants.FONTS[0]
}
val fontColor = remember(mainState.value.fontColor) {
Color(mainState.value.fontColor!!.toULong())
}
val backgroundColor = remember(mainState.value.backgroundColor) {
Color(mainState.value.backgroundColor!!.toULong())
}
val backgroundColor = animateColorAsState(
settingsState.value.selectedColorPreset.backgroundColor,
label = ""
)
val fontColor = animateColorAsState(
settingsState.value.selectedColorPreset.fontColor,
label = ""
)
val lineHeight =
remember(mainState.value.fontSize, mainState.value.lineHeight) {
(mainState.value.fontSize!! + mainState.value.lineHeight!!).sp
@ -212,8 +224,10 @@ private fun ReaderScreen(
if (state.value.showSettingsBottomSheet) {
ReaderSettingsBottomSheet(
mainState = mainState,
settingsState = settingsState,
onEvent = onEvent,
onMainEvent = onMainEvent
onMainEvent = onMainEvent,
onSettingsEvent = onSettingsEvent
)
}
@ -298,7 +312,7 @@ private fun ReaderScreen(
state = lazyListState,
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
.background(backgroundColor.value)
.then(
if (!state.value.loading && toolbarHidden) {
Modifier
@ -314,6 +328,25 @@ private fun ReaderScreen(
} else {
Modifier
}
)
.readerFastColorPresetChange(
fastColorPresetChangeEnabled = mainState.value.fastColorPresetChange!!,
toolbarHidden = toolbarHidden,
onSettingsEvent = onSettingsEvent,
presetChanged = {
Toast
.makeText(
context,
context.getString(
R.string.color_preset_selected_query,
it
.asString(context)
.trim()
),
Toast.LENGTH_SHORT
)
.show()
}
),
verticalArrangement = Arrangement.spacedBy(paragraphHeight)
) {
@ -332,7 +365,7 @@ private fun ReaderScreen(
line = line.second,
context = context,
fontFamily = fontFamily,
fontColor = fontColor,
fontColor = fontColor.value,
lineHeight = lineHeight,
fontStyle = fontStyle,
fontSize = mainState.value.fontSize!!.sp,

View file

@ -0,0 +1,61 @@
package ua.acclorite.book_story.presentation.screens.reader.components
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import ua.acclorite.book_story.domain.util.UIText
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsEvent
/**
* Reader Fast Color Preset Change.
* Detects horizontal swipe gestures.
*
* @param fastColorPresetChangeEnabled Whether this is enabled.
* @param toolbarHidden Whether toolbar(selection) is hidden.
* @param onSettingsEvent [SettingsEvent] callback.
* @param presetChanged Callback when Color Preset was changed.
*/
@Composable
fun Modifier.readerFastColorPresetChange(
fastColorPresetChangeEnabled: Boolean,
toolbarHidden: Boolean,
onSettingsEvent: (SettingsEvent) -> Unit,
presetChanged: (UIText) -> Unit
): Modifier {
val offset = remember { mutableFloatStateOf(0f) }
return this.then(
if (fastColorPresetChangeEnabled && toolbarHidden) {
Modifier.pointerInput(Unit) {
detectHorizontalDragGestures(
onDragStart = { offset.floatValue = 0f },
onDragEnd = {
when {
offset.floatValue > 200 -> {
onSettingsEvent(
SettingsEvent.OnSelectPreviousPreset {
presetChanged(it)
}
)
}
offset.floatValue < -200 -> {
onSettingsEvent(
SettingsEvent.OnSelectNextPreset {
presetChanged(it)
}
)
}
}
}
) { _, dragAmount ->
offset.floatValue += dragAmount
}
}
} else {
Modifier
}
)
}

View file

@ -26,6 +26,8 @@ import ua.acclorite.book_story.presentation.components.CustomBottomSheet
import ua.acclorite.book_story.presentation.data.MainEvent
import ua.acclorite.book_story.presentation.data.MainState
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsEvent
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsState
import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.subcategories.ColorsSubcategory
import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.ReaderSettingsCategory
@ -33,15 +35,19 @@ import ua.acclorite.book_story.presentation.screens.settings.nested.reader.compo
* Settings bottom sheet. Has General and Colors categories.
*
* @param mainState [MainState].
* @param settingsState [SettingsState].
* @param onEvent [ReaderEvent] callback.
* @param onMainEvent [MainEvent] callback.
* @param onSettingsEvent [SettingsEvent] callback.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReaderSettingsBottomSheet(
mainState: State<MainState>,
settingsState: State<SettingsState>,
onEvent: (ReaderEvent) -> Unit,
onMainEvent: (MainEvent) -> Unit
onMainEvent: (MainEvent) -> Unit,
onSettingsEvent: (SettingsEvent) -> Unit
) {
val pagerState = rememberPagerState { 2 }
val currentPage by remember { derivedStateOf { pagerState.currentPage } }
@ -57,7 +63,7 @@ fun ReaderSettingsBottomSheet(
)
val height = remember(currentPage) {
if (currentPage == 1) 0.5f else 0.7f
if (currentPage == 1) 0.6f else 0.7f
}
val animatedHeight by animateFloatAsState(
targetValue = height,
@ -107,8 +113,11 @@ fun ReaderSettingsBottomSheet(
if (page == 1) {
ColorsSubcategory(
state = mainState,
settingsState = settingsState,
onMainEvent = onMainEvent,
onSettingsEvent = onSettingsEvent,
showTitle = false,
backgroundColor = { MaterialTheme.colorScheme.surfaceContainer },
topPadding = 16.dp,
bottomPadding = 8.dp + it
)
@ -116,15 +125,4 @@ fun ReaderSettingsBottomSheet(
}
}
}
}
}

View file

@ -1,7 +1,6 @@
package ua.acclorite.book_story.presentation.screens.settings.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@ -9,21 +8,27 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.History
import androidx.compose.material3.MaterialTheme
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.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.components.CustomIconButton
@ -33,22 +38,33 @@ import ua.acclorite.book_story.presentation.components.CustomIconButton
*
* @param modifier Modifier.
* @param value Target [Color].
* @param presetId Current Color Preset id(to trigger initial value change).
* @param title Title.
* @param horizontalPadding Horizontal item padding.
* @param verticalPadding Vertical item padding.
* @param onValueChange Callback when target color changes.
*/
@OptIn(FlowPreview::class)
@Composable
fun ColorPickerWithTitle(
modifier: Modifier = Modifier,
value: Color,
presetId: Int,
title: String,
horizontalPadding: Dp = 18.dp,
verticalPadding: Dp = 8.dp,
onValueChange: (Color) -> Unit
) {
val initialValue = rememberSaveable { value.value.toString() }
var color by remember { mutableStateOf(value) }
val initialValue = rememberSaveable(presetId) { value.value.toString() }
var color by remember(value) { mutableStateOf(value) }
LaunchedEffect(color) {
snapshotFlow {
color
}.debounce(50).collectLatest {
onValueChange(it)
}
}
Column(
modifier
@ -66,7 +82,6 @@ fun ColorPickerWithTitle(
title = stringResource(id = R.string.red_color),
onValueChange = {
color = color.copy(red = it)
onValueChange(color)
}
)
RevertibleSlider(
@ -75,7 +90,6 @@ fun ColorPickerWithTitle(
title = stringResource(id = R.string.green_color),
onValueChange = {
color = color.copy(green = it)
onValueChange(color)
}
)
RevertibleSlider(
@ -84,7 +98,6 @@ fun ColorPickerWithTitle(
title = stringResource(id = R.string.blue_color),
onValueChange = {
color = color.copy(blue = it)
onValueChange(color)
}
)
}
@ -108,7 +121,7 @@ private fun RevertibleSlider(
)
) {
SliderWithTitle(
modifier = Modifier.weight(0.85f),
modifier = Modifier.weight(1f),
value = value,
title = title,
toValue = 255,
@ -118,18 +131,18 @@ private fun RevertibleSlider(
horizontalPadding = 0.dp,
verticalPadding = 0.dp
)
Box(modifier = Modifier.weight(0.15f), contentAlignment = Alignment.CenterEnd) {
CustomIconButton(
modifier = Modifier.size(28.dp),
icon = Icons.Default.History,
contentDescription = R.string.revert_content_desc,
disableOnClick = false,
enabled = initialValue != value.first,
color = if (initialValue == value.first) MaterialTheme.colorScheme.onSurfaceVariant
else MaterialTheme.colorScheme.onSurface
) {
onValueChange(initialValue)
}
Spacer(modifier = Modifier.width(10.dp))
CustomIconButton(
modifier = Modifier.size(28.dp),
icon = Icons.Default.History,
contentDescription = R.string.revert_content_desc,
disableOnClick = false,
enabled = initialValue != value.first,
color = if (initialValue == value.first) MaterialTheme.colorScheme.onSurfaceVariant
else MaterialTheme.colorScheme.onSurface
) {
onValueChange(initialValue)
}
}
}

View file

@ -4,8 +4,11 @@ package ua.acclorite.book_story.presentation.screens.settings.data
import androidx.activity.ComponentActivity
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import ua.acclorite.book_story.domain.util.ID
import ua.acclorite.book_story.domain.util.UIText
@Immutable
sealed class SettingsEvent {
@ -15,4 +18,27 @@ sealed class SettingsEvent {
val notificationsPermissionState: PermissionState,
val onChangeCheckForUpdates: (Boolean) -> Unit
) : SettingsEvent()
data class OnSelectPreviousPreset(
val onSelected: suspend (name: UIText) -> Unit
) : SettingsEvent()
data class OnSelectNextPreset(
val onSelected: suspend (name: UIText) -> Unit
) : SettingsEvent()
data class OnSelectColorPreset(val id: ID) : SettingsEvent()
data class OnDeleteColorPreset(val id: ID) : SettingsEvent()
data class OnScrollToColorPreset(val scrollTo: Int) : SettingsEvent()
data class OnUpdateColorPresetTitle(val id: ID, val title: String) : SettingsEvent()
data class OnShuffleColorPreset(val id: ID) : SettingsEvent()
data class OnUpdateColorPresetColor(
val id: ID,
val backgroundColor: Color?,
val fontColor: Color?
) : SettingsEvent()
data object OnAddColorPreset : SettingsEvent()
data class OnReorderColorPresets(val from: Int, val to: Int) : SettingsEvent()
data object OnConfirmReorderColorPresets : SettingsEvent()
}

View file

@ -0,0 +1,14 @@
package ua.acclorite.book_story.presentation.screens.settings.data
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.model.ColorPreset
import ua.acclorite.book_story.domain.util.Constants
@Immutable
data class SettingsState(
val colorPresets: List<ColorPreset> = emptyList(),
val selectedColorPreset: ColorPreset = Constants.DEFAULT_COLOR_PRESET,
val animateColorPreset: Boolean = false,
val colorPresetsListState: LazyListState = LazyListState()
)

View file

@ -9,24 +9,92 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.shouldShowRationale
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.ColorPreset
import ua.acclorite.book_story.domain.use_case.DeleteColorPreset
import ua.acclorite.book_story.domain.use_case.GetColorPresets
import ua.acclorite.book_story.domain.use_case.ReorderColorPresets
import ua.acclorite.book_story.domain.use_case.SelectColorPreset
import ua.acclorite.book_story.domain.use_case.UpdateColorPreset
import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.domain.util.ID
import ua.acclorite.book_story.domain.util.UIText
import ua.acclorite.book_story.presentation.data.launchActivity
import javax.inject.Inject
import kotlin.random.Random
@OptIn(ExperimentalPermissionsApi::class)
@HiltViewModel
class SettingsViewModel @Inject constructor(
private val getColorPresets: GetColorPresets,
private val updateColorPreset: UpdateColorPreset,
private val deleteColorPreset: DeleteColorPreset,
private val selectColorPreset: SelectColorPreset,
private val reorderColorPresets: ReorderColorPresets
) : ViewModel() {
private var notificationsPermissionJob: Job? = null
private val _state = MutableStateFlow(SettingsState())
val state = _state.asStateFlow()
private val _isReady = MutableStateFlow(false)
val isReady = _isReady.asStateFlow()
private var notificationsPermissionJob: Job? = null
private var selectColorPresetJob: Job? = null
private var addColorPresetJob: Job? = null
private var deleteColorPresetJob: Job? = null
private var updateColorColorPresetJob: Job? = null
private var updateTitleColorPresetJob: Job? = null
private var shuffleColorPresetJob: Job? = null
init {
viewModelScope.launch(Dispatchers.IO) {
var colorPresets = getColorPresets.execute()
if (colorPresets.isEmpty()) {
updateColorPreset.execute(Constants.DEFAULT_COLOR_PRESET)
onSelectColorPreset(getColorPresets.execute().first())
colorPresets = getColorPresets.execute()
}
val scrollIndex = colorPresets.indexOfFirst {
it.isSelected
}
if (scrollIndex != -1) {
launch(Dispatchers.Main) {
try {
_state.value.colorPresetsListState.requestScrollToItem(
index = scrollIndex
)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
_state.update {
it.copy(
selectedColorPreset = selectedColorPreset(colorPresets),
colorPresets = colorPresets
)
}
_isReady.update {
true
}
}
}
@Suppress("LABEL_NAME_CLASH")
fun onEvent(event: SettingsEvent) {
when (event) {
is SettingsEvent.OnGeneralChangeCheckForUpdates -> {
if (!event.enable) {
event.onChangeCheckForUpdates(false)
@ -75,6 +143,385 @@ class SettingsViewModel @Inject constructor(
}
}
}
is SettingsEvent.OnSelectPreviousPreset -> {
viewModelScope.launch {
cancelColorPresetJobs()
selectColorPresetJob = launch {
val colorPresets = _state.value.colorPresets
val selectedPreset = _state.value.selectedColorPreset
if (colorPresets.size == 1) {
return@launch
}
val selectedPresetIndex = colorPresets.indexOf(selectedPreset)
if (selectedPresetIndex == -1) {
return@launch
}
val previousColorPresetIndex = when (selectedPresetIndex) {
0 -> {
colorPresets.lastIndex
}
else -> {
selectedPresetIndex - 1
}
}
val previousColorPreset = colorPresets.getOrNull(previousColorPresetIndex)
?: return@launch
yield()
onSelectColorPreset(previousColorPreset)
val updatedColorPresets = _state.value.colorPresets.map {
it.copy(isSelected = previousColorPreset.id == it.id)
}
_state.update {
it.copy(
selectedColorPreset = selectedColorPreset(updatedColorPresets),
colorPresets = updatedColorPresets
)
}
event.onSelected(
if ((previousColorPreset.name ?: "").isBlank()) {
UIText.StringResource(
R.string.color_preset_query,
previousColorPreset.id.toString()
)
} else {
UIText.StringValue(previousColorPreset.name!!)
}
)
}
}
}
is SettingsEvent.OnSelectNextPreset -> {
viewModelScope.launch {
cancelColorPresetJobs()
selectColorPresetJob = launch {
val colorPresets = _state.value.colorPresets
val selectedPreset = _state.value.selectedColorPreset
if (colorPresets.size == 1) {
return@launch
}
val selectedPresetIndex = colorPresets.indexOf(selectedPreset)
if (selectedPresetIndex == -1) {
return@launch
}
val nextColorPresetIndex = when (selectedPresetIndex) {
colorPresets.lastIndex -> {
0
}
else -> {
selectedPresetIndex + 1
}
}
val nextColorPreset = colorPresets.getOrNull(nextColorPresetIndex)
?: return@launch
yield()
onSelectColorPreset(nextColorPreset)
val updatedColorPresets = _state.value.colorPresets.map {
it.copy(isSelected = nextColorPreset.id == it.id)
}
_state.update {
it.copy(
selectedColorPreset = selectedColorPreset(updatedColorPresets),
colorPresets = updatedColorPresets
)
}
event.onSelected(
if ((nextColorPreset.name ?: "").isBlank()) {
UIText.StringResource(
R.string.color_preset_query,
nextColorPreset.id.toString()
)
} else {
UIText.StringValue(nextColorPreset.name!!)
}
)
}
}
}
is SettingsEvent.OnSelectColorPreset -> {
viewModelScope.launch {
cancelColorPresetJobs()
selectColorPresetJob = launch {
val colorPreset = getColorPresetById(event.id) ?: return@launch
yield()
onSelectColorPreset(colorPreset, animate = true)
val colorPresets = _state.value.colorPresets.map {
it.copy(isSelected = colorPreset.id == it.id)
}
_state.update {
it.copy(
selectedColorPreset = selectedColorPreset(colorPresets),
colorPresets = colorPresets
)
}
}
}
}
is SettingsEvent.OnDeleteColorPreset -> {
viewModelScope.launch {
cancelColorPresetJobs()
deleteColorPresetJob = launch {
val colorPreset = getColorPresetById(event.id) ?: return@launch
yield()
val position = _state.value.colorPresets.indexOf(colorPreset)
if (position == -1) {
return@launch
}
val nextPosition = if (position == _state.value.colorPresets.lastIndex) {
position - 1
} else {
position
}
yield()
deleteColorPreset.execute(colorPreset)
onSelectColorPreset(getColorPresets.execute()[nextPosition])
val colorPresets = getColorPresets.execute()
_state.update {
it.copy(
selectedColorPreset = selectedColorPreset(colorPresets),
colorPresets = colorPresets
)
}
onEvent(SettingsEvent.OnScrollToColorPreset(nextPosition))
}
}
}
is SettingsEvent.OnScrollToColorPreset -> {
viewModelScope.launch {
try {
_state.value.colorPresetsListState.requestScrollToItem(
event.scrollTo
)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
is SettingsEvent.OnUpdateColorPresetTitle -> {
viewModelScope.launch {
cancelColorPresetJobs()
updateTitleColorPresetJob = launch {
val colorPreset = getColorPresetById(event.id) ?: return@launch
yield()
val updatedColorPreset = colorPreset.copy(
name = event.title
)
yield()
updateColorPreset.execute(updatedColorPreset)
_state.update {
it.copy(
selectedColorPreset = updatedColorPreset,
colorPresets = it.colorPresets.updateColorPreset(
updatedColorPreset
)
)
}
}
}
}
is SettingsEvent.OnShuffleColorPreset -> {
viewModelScope.launch {
cancelColorPresetJobs()
shuffleColorPresetJob = launch {
val colorPreset = getColorPresetById(event.id) ?: return@launch
yield()
val shuffledColorPreset = colorPreset.copy(
backgroundColor = colorPreset.backgroundColor.copy(
red = Random.nextFloat(),
green = Random.nextFloat(),
blue = Random.nextFloat()
),
fontColor = colorPreset.fontColor.copy(
red = Random.nextFloat(),
green = Random.nextFloat(),
blue = Random.nextFloat()
)
)
yield()
updateColorPreset.execute(shuffledColorPreset)
_state.update {
it.copy(
selectedColorPreset = shuffledColorPreset,
colorPresets = it.colorPresets.updateColorPreset(
shuffledColorPreset
)
)
}
}
}
}
is SettingsEvent.OnAddColorPreset -> {
viewModelScope.launch {
cancelColorPresetJobs()
addColorPresetJob = launch {
yield()
val newColorPreset = Constants.DEFAULT_COLOR_PRESET
updateColorPreset.execute(newColorPreset)
onSelectColorPreset(getColorPresets.execute().last())
val colorPresets = getColorPresets.execute()
_state.update {
it.copy(
selectedColorPreset = selectedColorPreset(colorPresets),
colorPresets = colorPresets
)
}
onEvent(SettingsEvent.OnScrollToColorPreset(colorPresets.lastIndex))
}
}
}
is SettingsEvent.OnUpdateColorPresetColor -> {
viewModelScope.launch {
cancelColorPresetJobs()
updateColorColorPresetJob = launch {
val colorPreset = getColorPresetById(event.id) ?: return@launch
yield()
val updatedColorPreset = colorPreset.copy(
backgroundColor = event.backgroundColor
?: colorPreset.backgroundColor,
fontColor = event.fontColor
?: colorPreset.fontColor
)
yield()
updateColorPreset.execute(updatedColorPreset)
_state.update {
it.copy(
selectedColorPreset = updatedColorPreset,
colorPresets = it.colorPresets.updateColorPreset(
updatedColorPreset
)
)
}
}
}
}
is SettingsEvent.OnReorderColorPresets -> {
viewModelScope.launch {
cancelColorPresetJobs()
launch {
val reorderedColorPresets =
_state.value.colorPresets.toMutableList().apply {
add(event.to, removeAt(event.from))
}
_state.update {
it.copy(
colorPresets = reorderedColorPresets
)
}
}
}
}
is SettingsEvent.OnConfirmReorderColorPresets -> {
viewModelScope.launch {
cancelColorPresetJobs()
launch {
reorderColorPresets.execute(_state.value.colorPresets)
}
}
}
}
}
private fun cancelColorPresetJobs() {
selectColorPresetJob?.cancel()
addColorPresetJob?.cancel()
updateTitleColorPresetJob?.cancel()
shuffleColorPresetJob?.cancel()
updateColorColorPresetJob?.cancel()
deleteColorPresetJob?.cancel()
}
private suspend fun onSelectColorPreset(colorPreset: ColorPreset, animate: Boolean = false) {
selectColorPreset.execute(colorPreset)
_state.update {
it.copy(
animateColorPreset = animate
)
}
}
private fun getColorPresetById(id: ID): ColorPreset? {
return _state.value.colorPresets.firstOrNull {
it.id == id
}
}
private fun selectedColorPreset(colorPresets: List<ColorPreset>? = null): ColorPreset {
val presets = colorPresets ?: _state.value.colorPresets
if (presets.size == 1) {
return presets.first()
}
val selectedPreset = presets.firstOrNull { it.isSelected }
if (selectedPreset == null) {
return Constants.DEFAULT_COLOR_PRESET
}
return selectedPreset
}
private fun List<ColorPreset>.updateColorPreset(colorPreset: ColorPreset): List<ColorPreset> {
if (size == 1) {
return listOf(colorPreset)
}
return this.map {
if (it.isSelected) {
colorPreset
} else {
it
}
}
}
}

View file

@ -2,6 +2,7 @@ package ua.acclorite.book_story.presentation.screens.settings.nested.appearance
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
@ -27,19 +28,26 @@ import ua.acclorite.book_story.presentation.data.LocalNavigator
import ua.acclorite.book_story.presentation.data.MainEvent
import ua.acclorite.book_story.presentation.data.MainState
import ua.acclorite.book_story.presentation.data.MainViewModel
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsEvent
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsState
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsViewModel
import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.AppearanceSettingsCategory
@Composable
fun AppearanceSettingsRoot() {
val navigator = LocalNavigator.current
val mainViewModel: MainViewModel = hiltViewModel()
val settingsViewModel: SettingsViewModel = hiltViewModel()
val state = mainViewModel.state.collectAsState()
val settingsState = settingsViewModel.state.collectAsState()
AppearanceSettings(
state = state,
settingsState = settingsState,
onNavigate = { navigator.it() },
onMainEvent = mainViewModel::onEvent
onMainEvent = mainViewModel::onEvent,
onSettingsEvent = settingsViewModel::onEvent
)
}
@ -47,8 +55,10 @@ fun AppearanceSettingsRoot() {
@Composable
private fun AppearanceSettings(
state: State<MainState>,
settingsState: State<SettingsState>,
onNavigate: OnNavigate,
onMainEvent: (MainEvent) -> Unit
onMainEvent: (MainEvent) -> Unit,
onSettingsEvent: (SettingsEvent) -> Unit
) {
val scrollState = TopAppBarDefaults.collapsibleUntilExitScrollBehaviorWithLazyListState()
@ -56,7 +66,8 @@ private fun AppearanceSettings(
Modifier
.fillMaxSize()
.nestedScroll(scrollState.first.nestedScrollConnection)
.windowInsetsPadding(WindowInsets.navigationBars),
.windowInsetsPadding(WindowInsets.navigationBars)
.imePadding(),
containerColor = MaterialTheme.colorScheme.surface,
topBar = {
LargeTopAppBar(
@ -82,7 +93,9 @@ private fun AppearanceSettings(
) {
AppearanceSettingsCategory(
state = state,
onMainEvent = onMainEvent
settingsState = settingsState,
onMainEvent = onMainEvent,
onSettingsEvent = onSettingsEvent
)
}
}

View file

@ -11,6 +11,8 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.presentation.data.MainEvent
import ua.acclorite.book_story.presentation.data.MainState
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsEvent
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsState
import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.subcategories.ColorsSubcategory
import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.subcategories.ThemePreferencesSubcategory
@ -20,13 +22,16 @@ import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.c
*
* @param state [MainState] instance.
* @param onMainEvent [MainEvent] callback.
* @param onSettingsEvent [SettingsEvent] callback.
* @param titleColor Title's color.
* @param topPadding Top padding to be applied.
* @param bottomPadding Bottom padding to be applied.
*/
fun LazyListScope.AppearanceSettingsCategory(
state: State<MainState>,
settingsState: State<SettingsState>,
onMainEvent: (MainEvent) -> Unit,
onSettingsEvent: (SettingsEvent) -> Unit,
titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary },
topPadding: Dp = 16.dp,
bottomPadding: Dp = 48.dp
@ -39,8 +44,10 @@ fun LazyListScope.AppearanceSettingsCategory(
bottomPadding = 0.dp
)
ColorsSubcategory(
state,
state = state,
settingsState = settingsState,
onMainEvent = onMainEvent,
onSettingsEvent = onSettingsEvent,
titleColor = titleColor,
topPadding = 22.dp,
bottomPadding = bottomPadding

View file

@ -0,0 +1,467 @@
package ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.settings
import android.content.Context
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.expandHorizontally
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkHorizontally
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.DeleteOutline
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.filled.Shuffle
import androidx.compose.material.icons.rounded.DragHandle
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import sh.calvin.reorderable.ReorderableCollectionItemScope
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.ColorPreset
import ua.acclorite.book_story.domain.util.Selected
import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
import ua.acclorite.book_story.presentation.components.CustomIconButton
import ua.acclorite.book_story.presentation.components.customItemsIndexed
import ua.acclorite.book_story.presentation.screens.settings.components.ColorPickerWithTitle
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsEvent
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsState
import ua.acclorite.book_story.presentation.ui.DefaultTransition
import ua.acclorite.book_story.presentation.ui.Transitions
/**
* Color Preset setting.
* Lets user create and use custom color preset.
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LazyItemScope.ColorPresetSetting(
state: State<SettingsState>,
onEvent: (SettingsEvent) -> Unit,
backgroundColor: Color,
verticalPadding: Dp = 8.dp,
horizontalPadding: Dp = 18.dp
) {
val context = LocalContext.current
val reorderableListState = rememberReorderableLazyListState(
lazyListState = state.value.colorPresetsListState
) { from, to ->
onEvent(SettingsEvent.OnReorderColorPresets(from.index, to.index))
}
Column(
Modifier
.animateItem()
.fillMaxWidth()
.padding(vertical = verticalPadding)
) {
CategoryTitle(
title = stringResource(id = R.string.color_preset_option),
padding = horizontalPadding
)
Spacer(modifier = Modifier.height(10.dp))
LazyRow(
modifier = Modifier.fillMaxWidth(),
state = state.value.colorPresetsListState,
verticalAlignment = Alignment.CenterVertically
) {
customItemsIndexed(
state.value.colorPresets,
key = { item, _ -> item.id }
) { index, colorPreset ->
ReorderableItem(
state = reorderableListState,
animateItemModifier = Modifier,
key = colorPreset.id
) {
Row {
if (index == 0) {
Spacer(modifier = Modifier.width(horizontalPadding))
} else {
Spacer(modifier = Modifier.width(2.dp))
}
ColorPresetSettingItem(
colorPreset = colorPreset,
isSelected = remember(
colorPreset.isSelected,
state.value.colorPresets.size
) {
if (state.value.colorPresets.size > 1) {
colorPreset.isSelected
} else true
},
enableAnimation = state.value.animateColorPreset,
context = context,
canDrag = remember(state.value.colorPresets.size) {
state.value.colorPresets.size > 1
},
onDragStopped = {
onEvent(SettingsEvent.OnConfirmReorderColorPresets)
},
onClick = {
onEvent(
SettingsEvent.OnSelectColorPreset(
id = colorPreset.id
)
)
}
)
if (index == state.value.colorPresets.lastIndex) {
Spacer(modifier = Modifier.width(horizontalPadding))
} else {
Spacer(modifier = Modifier.width(2.dp))
}
}
}
}
}
Spacer(modifier = Modifier.height(12.dp))
Column(
Modifier
.fillMaxWidth()
.padding(horizontal = horizontalPadding)
.clip(MaterialTheme.shapes.large)
.background(
backgroundColor,
MaterialTheme.shapes.large
)
) {
Spacer(modifier = Modifier.height(18.dp))
ColorPresetSettingConfigurationItem(
selectedColorPreset = state.value.selectedColorPreset,
canDelete = state.value.colorPresets.size > 1,
onDelete = {
onEvent(
SettingsEvent.OnDeleteColorPreset(
id = state.value.selectedColorPreset.id
)
)
},
onTitleChange = {
onEvent(
SettingsEvent.OnUpdateColorPresetTitle(
id = state.value.selectedColorPreset.id,
title = it
)
)
},
onShuffle = {
onEvent(
SettingsEvent.OnShuffleColorPreset(
id = state.value.selectedColorPreset.id
)
)
},
onAdd = {
onEvent(SettingsEvent.OnAddColorPreset)
}
)
Spacer(modifier = Modifier.height(8.dp))
ColorPickerWithTitle(
value = state.value.selectedColorPreset.backgroundColor,
presetId = state.value.selectedColorPreset.id,
title = stringResource(id = R.string.background_color_option),
onValueChange = {
onEvent(
SettingsEvent.OnUpdateColorPresetColor(
id = state.value.selectedColorPreset.id,
backgroundColor = it,
fontColor = null
)
)
}
)
ColorPickerWithTitle(
value = state.value.selectedColorPreset.fontColor,
presetId = state.value.selectedColorPreset.id,
title = stringResource(id = R.string.font_color_option),
onValueChange = {
onEvent(
SettingsEvent.OnUpdateColorPresetColor(
id = state.value.selectedColorPreset.id,
backgroundColor = null,
fontColor = it
)
)
}
)
Spacer(modifier = Modifier.height(10.dp))
}
}
}
/**
* Color Preset Setting Item.
* Lets user select color preset and shows little preview of the preset.
*
* @param colorPreset [ColorPreset] to select.
* @param isSelected Whether [ColorPreset] is selected.
* @param canDrag Whether user can drag [ColorPreset].
* @param enableAnimation Whether there is appearing/disappearing animation.
* @param context [Context].
* @param onDragStopped Callback when user stops dragging [ColorPreset].
* @param onClick OnClick callback.
*/
@Composable
private fun ReorderableCollectionItemScope.ColorPresetSettingItem(
colorPreset: ColorPreset,
isSelected: Selected,
canDrag: Boolean,
enableAnimation: Boolean,
context: Context,
onDragStopped: () -> Unit,
onClick: () -> Unit
) {
val title = remember(colorPreset) {
if ((colorPreset.name ?: "").isBlank()) {
return@remember context.getString(
R.string.color_preset_query,
colorPreset.id.toString()
)
}
colorPreset.name!!
}
val borderColor = remember(isSelected, colorPreset.fontColor) {
if (!isSelected) colorPreset.fontColor.copy(0.3f)
else colorPreset.fontColor
}
val animatedBorderColor = animateColorAsState(
borderColor,
label = ""
)
val animatedBackgroundColor = animateColorAsState(
colorPreset.backgroundColor,
label = ""
)
val animatedFontColor = animateColorAsState(
colorPreset.fontColor,
label = ""
)
Row(
Modifier
.clip(CircleShape)
.border(
width = 2.dp,
color = if (enableAnimation) animatedBorderColor.value
else borderColor,
shape = CircleShape
)
.padding(2.dp)
.background(animatedBackgroundColor.value, CircleShape)
.clickable(enabled = !isSelected) {
onClick()
}
.padding(horizontal = 16.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
CustomAnimatedVisibility(
visible = isSelected,
enter = if (enableAnimation) expandHorizontally() + fadeIn()
else Transitions.NoEnterAnimation,
exit = if (enableAnimation) shrinkHorizontally() + fadeOut()
else Transitions.NoExitAnimation
) {
Icon(
imageVector = Icons.Default.Done,
contentDescription = stringResource(id = R.string.selected_content_desc),
modifier = Modifier
.padding(end = 8.dp)
.size(18.dp),
tint = colorPreset.fontColor
)
}
Text(
title.trim(),
color = animatedFontColor.value,
style = MaterialTheme.typography.labelLarge,
overflow = TextOverflow.Ellipsis,
maxLines = 1
)
CustomAnimatedVisibility(
visible = isSelected && canDrag,
enter = if (enableAnimation) expandHorizontally() + fadeIn()
else Transitions.NoEnterAnimation,
exit = if (enableAnimation) shrinkHorizontally() + fadeOut()
else Transitions.NoExitAnimation
) {
Icon(
imageVector = Icons.Rounded.DragHandle,
contentDescription = stringResource(id = R.string.drag_content_desc),
modifier = Modifier
.padding(start = 8.dp)
.size(18.dp)
.longPressDraggableHandle(
onDragStopped = onDragStopped
),
tint = colorPreset.fontColor
)
}
}
}
/**
* Color Preset Settings Configuration Item.
* Lets user configure Title, Shuffle colors, Delete presets and Add presets.
*
* @param selectedColorPreset Currently selected [ColorPreset].
* @param canDelete Whether this [ColorPreset] can be deleted.
* @param onDelete Delete callback.
* @param onShuffle Shuffle callback.
* @param onTitleChange TitleChange callback.
* @param onAdd Add callback.
*/
@OptIn(FlowPreview::class)
@Composable
private fun ColorPresetSettingConfigurationItem(
selectedColorPreset: ColorPreset,
canDelete: Boolean,
onDelete: () -> Unit,
onShuffle: () -> Unit,
onTitleChange: (String) -> Unit,
onAdd: () -> Unit
) {
val title = remember(selectedColorPreset.id) {
mutableStateOf(selectedColorPreset.name ?: "")
}
LaunchedEffect(title) {
snapshotFlow {
title.value
}.debounce(50).collectLatest {
onTitleChange(it)
}
}
Row(
Modifier.padding(horizontal = 18.dp),
verticalAlignment = Alignment.CenterVertically
) {
BasicTextField(
value = title.value,
singleLine = true,
modifier = Modifier.weight(1f),
textStyle = TextStyle(
color = MaterialTheme.colorScheme.onSurface,
fontSize = MaterialTheme.typography.titleLarge.fontSize,
lineHeight = MaterialTheme.typography.titleLarge.lineHeight,
fontFamily = MaterialTheme.typography.titleLarge.fontFamily
),
onValueChange = {
if (it.length < 40 || it.length <= title.value.length) {
title.value = it
}
},
keyboardOptions = KeyboardOptions(
KeyboardCapitalization.Sentences
),
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant)
) { innerText ->
Box(
modifier = Modifier.fillMaxHeight(),
contentAlignment = Alignment.CenterStart
) {
if (title.value.isEmpty()) {
Text(
stringResource(id = R.string.color_preset_placeholder),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.titleLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
innerText()
}
}
Spacer(modifier = Modifier.width(24.dp))
DefaultTransition(visible = canDelete) {
CustomIconButton(
modifier = Modifier.size(24.dp),
icon = Icons.Default.DeleteOutline,
contentDescription = R.string.delete_color_preset_content_desc,
disableOnClick = false,
enabled = canDelete,
color = MaterialTheme.colorScheme.onSurfaceVariant
) {
onDelete()
}
}
CustomIconButton(
modifier = Modifier.size(24.dp),
icon = Icons.Default.Shuffle,
contentDescription = R.string.shuffle_color_preset_content_desc,
disableOnClick = false,
color = MaterialTheme.colorScheme.onSurfaceVariant
) {
onShuffle()
}
CustomIconButton(
modifier = Modifier.size(24.dp),
icon = Icons.Default.Add,
contentDescription = R.string.create_color_preset_content_desc,
disableOnClick = false,
color = MaterialTheme.colorScheme.onSurfaceVariant
) {
onAdd()
}
}
}

View file

@ -0,0 +1,35 @@
package ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.settings
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.data.MainEvent
import ua.acclorite.book_story.presentation.data.MainState
import ua.acclorite.book_story.presentation.screens.settings.components.SwitchWithTitle
/**
* Fast Color Preset Change setting.
* If true, user can fast change color presets in Reader with swipes.
*/
@Composable
fun LazyItemScope.FastColorPresetChangeSetting(
state: State<MainState>,
onMainEvent: (MainEvent) -> Unit
) {
SwitchWithTitle(
selected = state.value.fastColorPresetChange!!,
modifier = Modifier.animateItem(),
title = stringResource(id = R.string.fast_color_preset_change_option),
description = stringResource(id = R.string.fast_color_preset_change_option_desc),
onClick = {
onMainEvent(
MainEvent.OnChangeFastColorPresetChange(
!state.value.fastColorPresetChange!!
)
)
}
)
}

View file

@ -11,7 +11,6 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@ -44,7 +43,7 @@ import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.domain.util.UIText
import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility
import ua.acclorite.book_story.presentation.components.customItems
import ua.acclorite.book_story.presentation.components.customItemsIndexed
import ua.acclorite.book_story.presentation.data.MainEvent
import ua.acclorite.book_story.presentation.data.MainState
import ua.acclorite.book_story.presentation.ui.Theme
@ -79,13 +78,19 @@ fun LazyItemScope.ThemeSetting(
title = stringResource(id = R.string.app_theme_option),
padding = horizontalPadding
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(10.dp))
LazyRow(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(horizontal = horizontalPadding)
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
customItems(themes, key = { it.first.name }) { themeEntry ->
customItemsIndexed(
themes,
key = { item, _ -> item.first.name }
) { index, themeEntry ->
if (index == 0) {
Spacer(modifier = Modifier.width(horizontalPadding))
}
ThemeSettingItem(
theme = themeEntry,
darkTheme = state.value.darkTheme!!.isDark(),
@ -95,6 +100,10 @@ fun LazyItemScope.ThemeSetting(
) {
onMainEvent(MainEvent.OnChangeTheme(themeEntry.first.toString()))
}
if (index == themes.lastIndex) {
Spacer(modifier = Modifier.width(horizontalPadding))
}
}
}
}

View file

@ -19,7 +19,10 @@ import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.data.MainEvent
import ua.acclorite.book_story.presentation.data.MainState
import ua.acclorite.book_story.presentation.screens.settings.components.ColorPickerWithTitle
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsEvent
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsState
import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.settings.ColorPresetSetting
import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.settings.FastColorPresetChangeSetting
/**
* Colors subcategory.
@ -27,9 +30,12 @@ import ua.acclorite.book_story.presentation.screens.settings.components.ColorPic
*/
fun LazyListScope.ColorsSubcategory(
state: State<MainState>,
settingsState: State<SettingsState>,
onMainEvent: (MainEvent) -> Unit,
onSettingsEvent: (SettingsEvent) -> Unit,
titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary },
title: @Composable () -> String = { stringResource(id = R.string.colors_appearance_settings) },
backgroundColor: @Composable () -> Color = { MaterialTheme.colorScheme.surfaceContainerLow },
showTitle: Boolean = true,
topPadding: Dp,
bottomPadding: Dp
@ -55,36 +61,20 @@ fun LazyListScope.ColorsSubcategory(
}
}
// todo: This will be replaced with ColorPresets
item {
ColorPickerWithTitle(
modifier = Modifier.animateItem(),
value = Color(state.value.backgroundColor!!.toULong()),
title = stringResource(id = R.string.background_color_option),
onValueChange = {
onMainEvent(
MainEvent.OnChangeBackgroundColor(
it.value.toLong()
)
)
}
ColorPresetSetting(
state = settingsState,
backgroundColor = backgroundColor.invoke(),
onEvent = onSettingsEvent
)
}
item {
ColorPickerWithTitle(
modifier = Modifier.animateItem(),
value = Color(state.value.fontColor!!.toULong()),
title = stringResource(id = R.string.font_color_option),
onValueChange = {
onMainEvent(
MainEvent.OnChangeFontColor(
it.value.toLong()
)
)
}
FastColorPresetChangeSetting(
state = state,
onMainEvent = onMainEvent
)
}
// ---------
item {
Spacer(

View file

@ -22,8 +22,8 @@ fun LazyItemScope.DoubleClickTranslationSetting(
SwitchWithTitle(
selected = state.value.doubleClickTranslation!!,
modifier = Modifier.animateItem(),
title = stringResource(id = R.string.translator_double_click_to_translate_option),
description = stringResource(id = R.string.translator_double_click_to_translate_option_desc),
title = stringResource(id = R.string.double_click_translation_option),
description = stringResource(id = R.string.double_click_translation_option_desc),
onClick = {
onMainEvent(
MainEvent.OnChangeDoubleClickTranslation(

View file

@ -24,15 +24,16 @@ object Transitions {
val SlidingTransitionIn = fadeIn(tween(350)) +
slideInHorizontally(tween(350)) { it / 16 }
val BackSlidingTransitionIn = fadeIn(tween(350)) +
slideInHorizontally(tween(350)) { -it / 16 }
val SlidingTransitionOut = fadeOut(tween(250)) +
slideOutHorizontally(tween(350)) { -it / 16 }
val BackSlidingTransitionOut = fadeOut(tween(250)) +
slideOutHorizontally(tween(350)) { it / 16 }
val NoEnterAnimation = fadeIn(tween(0))
val NoExitAnimation = fadeOut(tween(0))
}
@Composable

View file

@ -124,6 +124,8 @@
<string name="read_query">%1$s прочитано</string>
<string name="file_path_query">Шлях до файлу: %1$s</string>
<string name="update_query">Оновлення %1$s</string>
<string name="color_preset_query">Пресет %1$s</string>
<string name="color_preset_selected_query">Вибрано «%1$s»</string>
<!-- Action Texts -->
<string name="cancel">Відмінити</string>
@ -205,18 +207,22 @@
<string name="line_height_option">Висота рядка</string>
<string name="paragraph_height_option">Висота абзаца</string>
<string name="paragraph_indentation_option">Абзацний відступ</string>
<string name="color_preset_option">Пресет кольорів</string>
<string name="background_color_option">Колір фона</string>
<string name="font_color_option">Колір шрифта</string>
<string name="check_for_updates_option">Перевіряти на оновлення</string>
<string name="check_for_updates_option_desc">Перевіряти на новішу версію застосунку при запуску</string>
<string name="side_padding_option">Бокові відступи</string>
<string name="translator_double_click_to_translate_option">Переклад подвійним кліком</string>
<string name="translator_double_click_to_translate_option_desc">Двічі клацніть на абзац, щоб перекласти його</string>
<string name="double_click_translation_option">Переклад подвійним кліком</string>
<string name="double_click_translation_option_desc">Двічі клацніть на абзац, щоб перекласти його</string>
<string name="fast_color_preset_change_option">Швидка зміна пресета кольорів</string>
<string name="fast_color_preset_change_option_desc">Проведіть вліво або вправо, щоб змінити пресет кольорів</string>
<!-- Colors -->
<!-- Color presets -->
<string name="red_color">Червоний</string>
<string name="green_color">Зелений</string>
<string name="blue_color">Синій</string>
<string name="color_preset_placeholder">Назвіть пресет…</string>
<!-- Dark Theme properties -->
<string name="dark_theme_off">Вимкнена</string>
@ -484,8 +490,12 @@
<string name="reset_start_content_desc">Скинути початковий посібник</string>
<string name="exit_editing_content_desc">Вийти з режиму редагування</string>
<string name="open_in_web_content_desc">Відкрити в браузері</string>
<string name="delete_color_preset_content_desc">Видалити пресет кольорів</string>
<string name="create_color_preset_content_desc">Створити новий пресет кольорів</string>
<string name="shuffle_color_preset_content_desc">Перетасувати кольори</string>
<string name="tryzub_content_desc">Тризуб</string>
<string name="github_project_content_desc">GitHub Проєкт</string>
<string name="github_profile_content_desc">GitHub Профіль</string>
<string name="drag_content_desc">Тягнути</string>
</resources>

View file

@ -139,6 +139,8 @@
<string name="read_query">%1$s read</string>
<string name="file_path_query">File path: %1$s</string>
<string name="update_query">Update %1$s</string>
<string name="color_preset_query">Preset %1$s</string>
<string name="color_preset_selected_query">Selected «%1$s»</string>
<!-- Action Texts -->
<string name="cancel">Cancel</string>
@ -220,18 +222,22 @@
<string name="line_height_option">Line height</string>
<string name="paragraph_height_option">Paragraph height</string>
<string name="paragraph_indentation_option">Paragraph indentation</string>
<string name="color_preset_option">Color preset</string>
<string name="background_color_option">Background color</string>
<string name="font_color_option">Font color</string>
<string name="check_for_updates_option">Check for updates</string>
<string name="check_for_updates_option_desc">Check for newer app version on startup</string>
<string name="side_padding_option">Side padding</string>
<string name="translator_double_click_to_translate_option">Double click translation</string>
<string name="translator_double_click_to_translate_option_desc">Double click on paragraph to translate it</string>
<string name="double_click_translation_option">Double click translation</string>
<string name="double_click_translation_option_desc">Double click on paragraph to translate it</string>
<string name="fast_color_preset_change_option">Fast color preset change</string>
<string name="fast_color_preset_change_option_desc">Swipe left or right to change color preset</string>
<!-- Colors -->
<!-- Color presets -->
<string name="red_color">Red</string>
<string name="green_color">Green</string>
<string name="blue_color">Blue</string>
<string name="color_preset_placeholder">Name a preset…</string>
<!-- Dark Theme properties -->
<string name="dark_theme_off">Disabled</string>
@ -500,10 +506,14 @@
<string name="reset_start_content_desc">Reset start guide</string>
<string name="exit_editing_content_desc">Exit editing mode</string>
<string name="open_in_web_content_desc">Open in web</string>
<string name="delete_color_preset_content_desc">Delete color preset</string>
<string name="create_color_preset_content_desc">Create new color preset</string>
<string name="shuffle_color_preset_content_desc">Shuffle colors</string>
<string name="x_content_desc" translatable="false">X</string>
<string name="reddit_content_desc" translatable="false">Reddit</string>
<string name="tryzub_content_desc">Tryzub</string>
<string name="github_project_content_desc">GitHub Project</string>
<string name="github_profile_content_desc">GitHub Profile</string>
<string name="drag_content_desc">Drag</string>
</resources>