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

@ -165,4 +165,7 @@ dependencies {
// About open source libraries // About open source libraries
implementation("com.mikepenz:aboutlibraries-core:latest.release") implementation("com.mikepenz:aboutlibraries-core:latest.release")
implementation("com.mikepenz:aboutlibraries-compose-m3:latest.release") implementation("com.mikepenz:aboutlibraries-compose-m3:latest.release")
// Drag & Drop
implementation("sh.calvin.reorderable:reorderable:2.2.0")
} }

View file

@ -0,0 +1,176 @@
{
"formatVersion": 1,
"database": {
"version": 5,
"identityHash": "b91d8ccd2316f8f11c90b0118512208b",
"entities": [
{
"tableName": "BookEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `author` TEXT, `description` TEXT, `textPath` TEXT NOT NULL, `filePath` TEXT NOT NULL, `scrollIndex` INTEGER NOT NULL, `scrollOffset` INTEGER NOT NULL, `progress` REAL NOT NULL, `image` TEXT, `category` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "author",
"columnName": "author",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "textPath",
"columnName": "textPath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "filePath",
"columnName": "filePath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "scrollIndex",
"columnName": "scrollIndex",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "scrollOffset",
"columnName": "scrollOffset",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "progress",
"columnName": "progress",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "image",
"columnName": "image",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "category",
"columnName": "category",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "HistoryEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bookId` INTEGER NOT NULL, `time` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bookId",
"columnName": "bookId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "ColorPresetEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT, `backgroundColor` INTEGER NOT NULL, `fontColor` INTEGER NOT NULL, `isSelected` INTEGER NOT NULL, `order` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "backgroundColor",
"columnName": "backgroundColor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "fontColor",
"columnName": "fontColor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isSelected",
"columnName": "isSelected",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b91d8ccd2316f8f11c90b0118512208b')"
]
}
}

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

View file

@ -43,6 +43,7 @@ object AppModule {
) )
.addMigrations( .addMigrations(
DatabaseHelper.MIGRATION_2_3, // creates LanguageHistoryEntity table(if does not exist) DatabaseHelper.MIGRATION_2_3, // creates LanguageHistoryEntity table(if does not exist)
DatabaseHelper.MIGRATION_4_5, // creates ColorPresetEntity table(if does not exist)
) )
.allowMainThreadQueries() .allowMainThreadQueries()
.build() .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.local.notification.UpdatesNotificationServiceImpl
import ua.acclorite.book_story.data.mapper.book.BookMapper 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.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.HistoryMapper
import ua.acclorite.book_story.data.mapper.history.HistoryMapperImpl import ua.acclorite.book_story.data.mapper.history.HistoryMapperImpl
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
@ -47,6 +49,12 @@ abstract class RepositoryModule {
historyMapperImpl: HistoryMapperImpl historyMapperImpl: HistoryMapperImpl
): HistoryMapper ): HistoryMapper
@Binds
@Singleton
abstract fun bindColorPresetMapper(
colorPresetMapperImpl: ColorPresetMapperImpl
): ColorPresetMapper
@Binds @Binds
@Singleton @Singleton
abstract fun bindFileParser( 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.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.Update 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.BookEntity
import ua.acclorite.book_story.data.local.dto.ColorPresetEntity
import ua.acclorite.book_story.data.local.dto.HistoryEntity import ua.acclorite.book_story.data.local.dto.HistoryEntity
@Dao @Dao
@ -57,4 +59,23 @@ interface BookDao {
@Delete @Delete
suspend fun deleteHistory(history: List<HistoryEntity>) 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.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import ua.acclorite.book_story.data.local.dto.BookEntity 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 import ua.acclorite.book_story.data.local.dto.HistoryEntity
@Database( @Database(
entities = [ entities = [
BookEntity::class, BookEntity::class,
HistoryEntity::class HistoryEntity::class,
ColorPresetEntity::class,
], ],
version = 4, version = 5,
autoMigrations = [ autoMigrations = [
AutoMigration(1, 2), AutoMigration(1, 2),
AutoMigration(2, 3), AutoMigration(2, 3),
AutoMigration(3, 4, spec = DatabaseHelper.MIGRATION_3_4::class), AutoMigration(3, 4, spec = DatabaseHelper.MIGRATION_3_4::class),
AutoMigration(4, 5),
], ],
exportSchema = true exportSchema = true
) )
@ -50,4 +53,19 @@ object DatabaseHelper {
@DeleteColumn("BookEntity", "translateWhenOpen") @DeleteColumn("BookEntity", "translateWhenOpen")
@DeleteTable("LanguageHistoryEntity") @DeleteTable("LanguageHistoryEntity")
class MIGRATION_3_4 : AutoMigrationSpec 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.notification.UpdatesNotificationService
import ua.acclorite.book_story.data.local.room.BookDao 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.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.mapper.history.HistoryMapper
import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.FileParser
import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.data.parser.TextParser
import ua.acclorite.book_story.data.remote.GithubAPI import ua.acclorite.book_story.data.remote.GithubAPI
import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo
import ua.acclorite.book_story.domain.model.Book 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.History
import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.repository.BookRepository
@ -55,6 +57,7 @@ class BookRepositoryImpl @Inject constructor(
private val bookMapper: BookMapper, private val bookMapper: BookMapper,
private val historyMapper: HistoryMapper, private val historyMapper: HistoryMapper,
private val colorPresetMapper: ColorPresetMapper,
private val fileParser: FileParser, private val fileParser: FileParser,
private val textParser: TextParser 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 kotlinx.coroutines.flow.Flow
import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo
import ua.acclorite.book_story.domain.model.Book 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.History
import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.domain.util.CoverImage import ua.acclorite.book_story.domain.util.CoverImage
@ -89,5 +90,17 @@ interface BookRepository {
history: List<History> history: List<History>
) )
suspend fun checkForUpdates(postNotification: Boolean): LatestReleaseInfo? 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.Icons
import androidx.compose.material.icons.filled.Person 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.Font
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle 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.Badge
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category 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.Credit
import ua.acclorite.book_story.domain.model.FontWithName import ua.acclorite.book_story.domain.model.FontWithName
import ua.acclorite.book_story.domain.model.NavigationItem import ua.acclorite.book_story.domain.model.NavigationItem
@ -295,4 +297,12 @@ object Constants {
lastOpened = null, lastOpened = null,
category = Category.READING 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.booleanPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
object DataStoreConstants { object DataStoreConstants {
@ -13,8 +12,6 @@ object DataStoreConstants {
val PURE_DARK = stringPreferencesKey("pure_dark") val PURE_DARK = stringPreferencesKey("pure_dark")
val THEME_CONTRAST = stringPreferencesKey("theme_contrast") val THEME_CONTRAST = stringPreferencesKey("theme_contrast")
val SHOW_START_SCREEN = booleanPreferencesKey("guide") val SHOW_START_SCREEN = booleanPreferencesKey("guide")
val BACKGROUND_COLOR = longPreferencesKey("background_color")
val FONT_COLOR = longPreferencesKey("font_color")
val FONT = stringPreferencesKey("font") val FONT = stringPreferencesKey("font")
val IS_ITALIC = booleanPreferencesKey("font_style") val IS_ITALIC = booleanPreferencesKey("font_style")
val FONT_SIZE = intPreferencesKey("font_size") val FONT_SIZE = intPreferencesKey("font_size")
@ -24,4 +21,5 @@ object DataStoreConstants {
val CHECK_FOR_UPDATES = booleanPreferencesKey("check_for_updates") val CHECK_FOR_UPDATES = booleanPreferencesKey("check_for_updates")
val SIDE_PADDING = intPreferencesKey("side_padding") val SIDE_PADDING = intPreferencesKey("side_padding")
val DOUBLE_CLICK_TRANSLATION = booleanPreferencesKey("double_click_translation") 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( fun <T> LazyListScope.customItemsIndexed(
items: List<T>, items: List<T>,
key: (T) -> Any?, key: (T, Int) -> Any?,
content: @Composable LazyItemScope.(Int, T) -> Unit content: @Composable LazyItemScope.(Int, T) -> Unit
) { ) {
for ((index, item) in items.withIndex()) { for ((index, item) in items.withIndex()) {
item(key = key(item)) { item(key = key(item, index)) {
content(index, item) content(index, item)
} }
} }

View file

@ -12,10 +12,9 @@ sealed class MainEvent {
data class OnChangeLineHeight(val lineHeight: Int) : MainEvent() data class OnChangeLineHeight(val lineHeight: Int) : MainEvent()
data class OnChangeParagraphHeight(val paragraphHeight: Int) : MainEvent() data class OnChangeParagraphHeight(val paragraphHeight: Int) : MainEvent()
data class OnChangeParagraphIndentation(val bool: Boolean) : 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 OnChangeShowStartScreen(val bool: Boolean) : MainEvent()
data class OnChangeCheckForUpdates(val bool: Boolean) : MainEvent() data class OnChangeCheckForUpdates(val bool: Boolean) : MainEvent()
data class OnChangeSidePadding(val sidePadding: Int) : MainEvent() data class OnChangeSidePadding(val sidePadding: Int) : MainEvent()
data class OnChangeDoubleClickTranslation(val bool: Boolean) : 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 android.os.Parcelable
import androidx.annotation.Keep import androidx.annotation.Keep
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import ua.acclorite.book_story.domain.util.Constants import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.domain.util.DataStoreConstants import ua.acclorite.book_story.domain.util.DataStoreConstants
@ -38,12 +37,11 @@ data class MainState(
val lineHeight: Int? = null, val lineHeight: Int? = null,
val paragraphHeight: Int? = null, val paragraphHeight: Int? = null,
val paragraphIndentation: Boolean? = null, val paragraphIndentation: Boolean? = null,
val backgroundColor: Long? = null,
val fontColor: Long? = null,
val showStartScreen: Boolean? = null, val showStartScreen: Boolean? = null,
val checkForUpdates: Boolean? = null, val checkForUpdates: Boolean? = null,
val sidePadding: Int? = null, val sidePadding: Int? = null,
val doubleClickTranslation: Boolean? = null val doubleClickTranslation: Boolean? = null,
val fastColorPresetChange: Boolean? = null,
) : Parcelable { ) : Parcelable {
companion object { companion object {
/** /**
@ -75,12 +73,6 @@ data class MainState(
val showStartScreen: Boolean = data[SHOW_START_SCREEN.name] as? Boolean val showStartScreen: Boolean = data[SHOW_START_SCREEN.name] as? Boolean
?: true ?: 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 val fontFamily: String = data[FONT.name] as? String
?: Constants.FONTS[0].id ?: Constants.FONTS[0].id
@ -109,6 +101,10 @@ data class MainState(
data[DOUBLE_CLICK_TRANSLATION.name] as? Boolean data[DOUBLE_CLICK_TRANSLATION.name] as? Boolean
?: false ?: false
val fastColorPresetChange: Boolean =
data[FAST_COLOR_PRESET_CHANGE.name] as? Boolean
?: true
return MainState( return MainState(
language = language, language = language,
theme = theme.toTheme(), theme = theme.toTheme(),
@ -116,8 +112,6 @@ data class MainState(
pureDark = pureDark.toPureDark(), pureDark = pureDark.toPureDark(),
themeContrast = themeContrast.toThemeContrast(), themeContrast = themeContrast.toThemeContrast(),
showStartScreen = showStartScreen, showStartScreen = showStartScreen,
backgroundColor = backgroundColor,
fontColor = fontColor,
fontFamily = fontFamily, fontFamily = fontFamily,
isItalic = isItalic, isItalic = isItalic,
fontSize = fontSize, fontSize = fontSize,
@ -126,7 +120,8 @@ data class MainState(
paragraphIndentation = paragraphIndentation, paragraphIndentation = paragraphIndentation,
checkForUpdates = checkForUpdates, checkForUpdates = checkForUpdates,
sidePadding = sidePadding, 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.Constants
import ua.acclorite.book_story.domain.util.DataStoreConstants import ua.acclorite.book_story.domain.util.DataStoreConstants
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.screens.settings.data.SettingsViewModel
import ua.acclorite.book_story.presentation.ui.toDarkTheme import ua.acclorite.book_story.presentation.ui.toDarkTheme
import ua.acclorite.book_story.presentation.ui.toPureDark import ua.acclorite.book_story.presentation.ui.toPureDark
import ua.acclorite.book_story.presentation.ui.toTheme 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 -> { is MainEvent.OnChangeShowStartScreen -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.SHOW_START_SCREEN, event.bool) setDatastore.execute(DataStoreConstants.SHOW_START_SCREEN, event.bool)
@ -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( fun init(
libraryViewModel: LibraryViewModel libraryViewModel: LibraryViewModel,
settingsViewModel: SettingsViewModel,
) { ) {
viewModelScope.launch(Dispatchers.Main) { viewModelScope.launch(Dispatchers.Main) {
val settings = getAllSettings.execute(viewModelScope) val settings = getAllSettings.execute(viewModelScope)
@ -265,7 +256,12 @@ class MainViewModel @Inject constructor(
} }
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
libraryViewModel.isReady.collectLatest { ready -> combine(
libraryViewModel.isReady,
settingsViewModel.isReady
) { (libraryViewModelReady, settingsViewModelReady) ->
libraryViewModelReady && settingsViewModelReady
}.collectLatest { ready ->
isViewModelReady.update { isViewModelReady.update {
ready ready
} }

View file

@ -5,6 +5,7 @@ import android.os.Build
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -34,7 +35,6 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource 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.ReaderTextParagraph
import ua.acclorite.book_story.presentation.screens.reader.components.app_bar.ReaderBottomBar 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.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.settings_bottom_sheet.ReaderSettingsBottomSheet
import ua.acclorite.book_story.presentation.screens.reader.components.start_item.ReaderStartItem 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.ReaderEvent
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderState 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.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 @Composable
fun ReaderScreenRoot(screen: Screen.Reader) { fun ReaderScreenRoot(screen: Screen.Reader) {
@ -82,9 +86,11 @@ fun ReaderScreenRoot(screen: Screen.Reader) {
val mainViewModel: MainViewModel = hiltViewModel() val mainViewModel: MainViewModel = hiltViewModel()
val libraryViewModel: LibraryViewModel = hiltViewModel() val libraryViewModel: LibraryViewModel = hiltViewModel()
val historyViewModel: HistoryViewModel = hiltViewModel() val historyViewModel: HistoryViewModel = hiltViewModel()
val settingsViewModel: SettingsViewModel = hiltViewModel()
val state = viewModel.state.collectAsState() val state = viewModel.state.collectAsState()
val mainState = mainViewModel.state.collectAsState() val mainState = mainViewModel.state.collectAsState()
val settingsState = settingsViewModel.state.collectAsState()
val lazyListState = rememberSaveable( val lazyListState = rememberSaveable(
state.value.listState, state.value.listState,
@ -137,10 +143,12 @@ fun ReaderScreenRoot(screen: Screen.Reader) {
ReaderScreen( ReaderScreen(
state = state, state = state,
mainState = mainState, mainState = mainState,
settingsState = settingsState,
lazyListState = lazyListState, lazyListState = lazyListState,
onNavigate = { navigator.it() }, onNavigate = { navigator.it() },
onEvent = viewModel::onEvent, onEvent = viewModel::onEvent,
onMainEvent = mainViewModel::onEvent, onMainEvent = mainViewModel::onEvent,
onSettingsEvent = settingsViewModel::onEvent,
onLibraryEvent = libraryViewModel::onEvent, onLibraryEvent = libraryViewModel::onEvent,
onHistoryEvent = historyViewModel::onEvent onHistoryEvent = historyViewModel::onEvent
) )
@ -151,10 +159,12 @@ fun ReaderScreenRoot(screen: Screen.Reader) {
private fun ReaderScreen( private fun ReaderScreen(
state: State<ReaderState>, state: State<ReaderState>,
mainState: State<MainState>, mainState: State<MainState>,
settingsState: State<SettingsState>,
lazyListState: LazyListState, lazyListState: LazyListState,
onNavigate: OnNavigate, onNavigate: OnNavigate,
onEvent: (ReaderEvent) -> Unit, onEvent: (ReaderEvent) -> Unit,
onMainEvent: (MainEvent) -> Unit, onMainEvent: (MainEvent) -> Unit,
onSettingsEvent: (SettingsEvent) -> Unit,
onLibraryEvent: (LibraryEvent) -> Unit, onLibraryEvent: (LibraryEvent) -> Unit,
onHistoryEvent: (HistoryEvent) -> Unit, onHistoryEvent: (HistoryEvent) -> Unit,
) { ) {
@ -186,12 +196,14 @@ private fun ReaderScreen(
it.id == mainState.value.fontFamily it.id == mainState.value.fontFamily
} ?: Constants.FONTS[0] } ?: Constants.FONTS[0]
} }
val fontColor = remember(mainState.value.fontColor) { val backgroundColor = animateColorAsState(
Color(mainState.value.fontColor!!.toULong()) settingsState.value.selectedColorPreset.backgroundColor,
} label = ""
val backgroundColor = remember(mainState.value.backgroundColor) { )
Color(mainState.value.backgroundColor!!.toULong()) val fontColor = animateColorAsState(
} settingsState.value.selectedColorPreset.fontColor,
label = ""
)
val lineHeight = val lineHeight =
remember(mainState.value.fontSize, mainState.value.lineHeight) { remember(mainState.value.fontSize, mainState.value.lineHeight) {
(mainState.value.fontSize!! + mainState.value.lineHeight!!).sp (mainState.value.fontSize!! + mainState.value.lineHeight!!).sp
@ -212,8 +224,10 @@ private fun ReaderScreen(
if (state.value.showSettingsBottomSheet) { if (state.value.showSettingsBottomSheet) {
ReaderSettingsBottomSheet( ReaderSettingsBottomSheet(
mainState = mainState, mainState = mainState,
settingsState = settingsState,
onEvent = onEvent, onEvent = onEvent,
onMainEvent = onMainEvent onMainEvent = onMainEvent,
onSettingsEvent = onSettingsEvent
) )
} }
@ -298,7 +312,7 @@ private fun ReaderScreen(
state = lazyListState, state = lazyListState,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(backgroundColor) .background(backgroundColor.value)
.then( .then(
if (!state.value.loading && toolbarHidden) { if (!state.value.loading && toolbarHidden) {
Modifier Modifier
@ -314,6 +328,25 @@ private fun ReaderScreen(
} else { } else {
Modifier 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) verticalArrangement = Arrangement.spacedBy(paragraphHeight)
) { ) {
@ -332,7 +365,7 @@ private fun ReaderScreen(
line = line.second, line = line.second,
context = context, context = context,
fontFamily = fontFamily, fontFamily = fontFamily,
fontColor = fontColor, fontColor = fontColor.value,
lineHeight = lineHeight, lineHeight = lineHeight,
fontStyle = fontStyle, fontStyle = fontStyle,
fontSize = mainState.value.fontSize!!.sp, 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.MainEvent
import ua.acclorite.book_story.presentation.data.MainState 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.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.appearance.components.subcategories.ColorsSubcategory
import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.ReaderSettingsCategory 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. * Settings bottom sheet. Has General and Colors categories.
* *
* @param mainState [MainState]. * @param mainState [MainState].
* @param settingsState [SettingsState].
* @param onEvent [ReaderEvent] callback. * @param onEvent [ReaderEvent] callback.
* @param onMainEvent [MainEvent] callback. * @param onMainEvent [MainEvent] callback.
* @param onSettingsEvent [SettingsEvent] callback.
*/ */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ReaderSettingsBottomSheet( fun ReaderSettingsBottomSheet(
mainState: State<MainState>, mainState: State<MainState>,
settingsState: State<SettingsState>,
onEvent: (ReaderEvent) -> Unit, onEvent: (ReaderEvent) -> Unit,
onMainEvent: (MainEvent) -> Unit onMainEvent: (MainEvent) -> Unit,
onSettingsEvent: (SettingsEvent) -> Unit
) { ) {
val pagerState = rememberPagerState { 2 } val pagerState = rememberPagerState { 2 }
val currentPage by remember { derivedStateOf { pagerState.currentPage } } val currentPage by remember { derivedStateOf { pagerState.currentPage } }
@ -57,7 +63,7 @@ fun ReaderSettingsBottomSheet(
) )
val height = remember(currentPage) { val height = remember(currentPage) {
if (currentPage == 1) 0.5f else 0.7f if (currentPage == 1) 0.6f else 0.7f
} }
val animatedHeight by animateFloatAsState( val animatedHeight by animateFloatAsState(
targetValue = height, targetValue = height,
@ -107,8 +113,11 @@ fun ReaderSettingsBottomSheet(
if (page == 1) { if (page == 1) {
ColorsSubcategory( ColorsSubcategory(
state = mainState, state = mainState,
settingsState = settingsState,
onMainEvent = onMainEvent, onMainEvent = onMainEvent,
onSettingsEvent = onSettingsEvent,
showTitle = false, showTitle = false,
backgroundColor = { MaterialTheme.colorScheme.surfaceContainer },
topPadding = 16.dp, topPadding = 16.dp,
bottomPadding = 8.dp + it 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 package ua.acclorite.book_story.presentation.screens.settings.components
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer 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.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.History import androidx.compose.material.icons.filled.History
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import 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.R
import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.components.CustomIconButton import ua.acclorite.book_story.presentation.components.CustomIconButton
@ -33,22 +38,33 @@ import ua.acclorite.book_story.presentation.components.CustomIconButton
* *
* @param modifier Modifier. * @param modifier Modifier.
* @param value Target [Color]. * @param value Target [Color].
* @param presetId Current Color Preset id(to trigger initial value change).
* @param title Title. * @param title Title.
* @param horizontalPadding Horizontal item padding. * @param horizontalPadding Horizontal item padding.
* @param verticalPadding Vertical item padding. * @param verticalPadding Vertical item padding.
* @param onValueChange Callback when target color changes. * @param onValueChange Callback when target color changes.
*/ */
@OptIn(FlowPreview::class)
@Composable @Composable
fun ColorPickerWithTitle( fun ColorPickerWithTitle(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
value: Color, value: Color,
presetId: Int,
title: String, title: String,
horizontalPadding: Dp = 18.dp, horizontalPadding: Dp = 18.dp,
verticalPadding: Dp = 8.dp, verticalPadding: Dp = 8.dp,
onValueChange: (Color) -> Unit onValueChange: (Color) -> Unit
) { ) {
val initialValue = rememberSaveable { value.value.toString() } val initialValue = rememberSaveable(presetId) { value.value.toString() }
var color by remember { mutableStateOf(value) } var color by remember(value) { mutableStateOf(value) }
LaunchedEffect(color) {
snapshotFlow {
color
}.debounce(50).collectLatest {
onValueChange(it)
}
}
Column( Column(
modifier modifier
@ -66,7 +82,6 @@ fun ColorPickerWithTitle(
title = stringResource(id = R.string.red_color), title = stringResource(id = R.string.red_color),
onValueChange = { onValueChange = {
color = color.copy(red = it) color = color.copy(red = it)
onValueChange(color)
} }
) )
RevertibleSlider( RevertibleSlider(
@ -75,7 +90,6 @@ fun ColorPickerWithTitle(
title = stringResource(id = R.string.green_color), title = stringResource(id = R.string.green_color),
onValueChange = { onValueChange = {
color = color.copy(green = it) color = color.copy(green = it)
onValueChange(color)
} }
) )
RevertibleSlider( RevertibleSlider(
@ -84,7 +98,6 @@ fun ColorPickerWithTitle(
title = stringResource(id = R.string.blue_color), title = stringResource(id = R.string.blue_color),
onValueChange = { onValueChange = {
color = color.copy(blue = it) color = color.copy(blue = it)
onValueChange(color)
} }
) )
} }
@ -108,7 +121,7 @@ private fun RevertibleSlider(
) )
) { ) {
SliderWithTitle( SliderWithTitle(
modifier = Modifier.weight(0.85f), modifier = Modifier.weight(1f),
value = value, value = value,
title = title, title = title,
toValue = 255, toValue = 255,
@ -118,18 +131,18 @@ private fun RevertibleSlider(
horizontalPadding = 0.dp, horizontalPadding = 0.dp,
verticalPadding = 0.dp verticalPadding = 0.dp
) )
Box(modifier = Modifier.weight(0.15f), contentAlignment = Alignment.CenterEnd) {
CustomIconButton( Spacer(modifier = Modifier.width(10.dp))
modifier = Modifier.size(28.dp), CustomIconButton(
icon = Icons.Default.History, modifier = Modifier.size(28.dp),
contentDescription = R.string.revert_content_desc, icon = Icons.Default.History,
disableOnClick = false, contentDescription = R.string.revert_content_desc,
enabled = initialValue != value.first, disableOnClick = false,
color = if (initialValue == value.first) MaterialTheme.colorScheme.onSurfaceVariant enabled = initialValue != value.first,
else MaterialTheme.colorScheme.onSurface color = if (initialValue == value.first) MaterialTheme.colorScheme.onSurfaceVariant
) { else MaterialTheme.colorScheme.onSurface
onValueChange(initialValue) ) {
} onValueChange(initialValue)
} }
} }
} }

View file

@ -4,8 +4,11 @@ package ua.acclorite.book_story.presentation.screens.settings.data
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState import com.google.accompanist.permissions.PermissionState
import ua.acclorite.book_story.domain.util.ID
import ua.acclorite.book_story.domain.util.UIText
@Immutable @Immutable
sealed class SettingsEvent { sealed class SettingsEvent {
@ -15,4 +18,27 @@ sealed class SettingsEvent {
val notificationsPermissionState: PermissionState, val notificationsPermissionState: PermissionState,
val onChangeCheckForUpdates: (Boolean) -> Unit val onChangeCheckForUpdates: (Boolean) -> Unit
) : SettingsEvent() ) : 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.isGranted
import com.google.accompanist.permissions.shouldShowRationale import com.google.accompanist.permissions.shouldShowRationale
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay 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.launch
import kotlinx.coroutines.yield 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 ua.acclorite.book_story.presentation.data.launchActivity
import javax.inject.Inject import javax.inject.Inject
import kotlin.random.Random
@OptIn(ExperimentalPermissionsApi::class) @OptIn(ExperimentalPermissionsApi::class)
@HiltViewModel @HiltViewModel
class SettingsViewModel @Inject constructor( 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() { ) : 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) { fun onEvent(event: SettingsEvent) {
when (event) { when (event) {
is SettingsEvent.OnGeneralChangeCheckForUpdates -> { is SettingsEvent.OnGeneralChangeCheckForUpdates -> {
if (!event.enable) { if (!event.enable) {
event.onChangeCheckForUpdates(false) 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.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
@ -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.MainEvent
import ua.acclorite.book_story.presentation.data.MainState import ua.acclorite.book_story.presentation.data.MainState
import ua.acclorite.book_story.presentation.data.MainViewModel 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 import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.AppearanceSettingsCategory
@Composable @Composable
fun AppearanceSettingsRoot() { fun AppearanceSettingsRoot() {
val navigator = LocalNavigator.current val navigator = LocalNavigator.current
val mainViewModel: MainViewModel = hiltViewModel() val mainViewModel: MainViewModel = hiltViewModel()
val settingsViewModel: SettingsViewModel = hiltViewModel()
val state = mainViewModel.state.collectAsState() val state = mainViewModel.state.collectAsState()
val settingsState = settingsViewModel.state.collectAsState()
AppearanceSettings( AppearanceSettings(
state = state, state = state,
settingsState = settingsState,
onNavigate = { navigator.it() }, onNavigate = { navigator.it() },
onMainEvent = mainViewModel::onEvent onMainEvent = mainViewModel::onEvent,
onSettingsEvent = settingsViewModel::onEvent
) )
} }
@ -47,8 +55,10 @@ fun AppearanceSettingsRoot() {
@Composable @Composable
private fun AppearanceSettings( private fun AppearanceSettings(
state: State<MainState>, state: State<MainState>,
settingsState: State<SettingsState>,
onNavigate: OnNavigate, onNavigate: OnNavigate,
onMainEvent: (MainEvent) -> Unit onMainEvent: (MainEvent) -> Unit,
onSettingsEvent: (SettingsEvent) -> Unit
) { ) {
val scrollState = TopAppBarDefaults.collapsibleUntilExitScrollBehaviorWithLazyListState() val scrollState = TopAppBarDefaults.collapsibleUntilExitScrollBehaviorWithLazyListState()
@ -56,7 +66,8 @@ private fun AppearanceSettings(
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.nestedScroll(scrollState.first.nestedScrollConnection) .nestedScroll(scrollState.first.nestedScrollConnection)
.windowInsetsPadding(WindowInsets.navigationBars), .windowInsetsPadding(WindowInsets.navigationBars)
.imePadding(),
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
topBar = { topBar = {
LargeTopAppBar( LargeTopAppBar(
@ -82,7 +93,9 @@ private fun AppearanceSettings(
) { ) {
AppearanceSettingsCategory( AppearanceSettingsCategory(
state = state, 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 androidx.compose.ui.unit.dp
import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainEvent
import ua.acclorite.book_story.presentation.data.MainState 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.ColorsSubcategory
import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.subcategories.ThemePreferencesSubcategory 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 state [MainState] instance.
* @param onMainEvent [MainEvent] callback. * @param onMainEvent [MainEvent] callback.
* @param onSettingsEvent [SettingsEvent] callback.
* @param titleColor Title's color. * @param titleColor Title's color.
* @param topPadding Top padding to be applied. * @param topPadding Top padding to be applied.
* @param bottomPadding Bottom padding to be applied. * @param bottomPadding Bottom padding to be applied.
*/ */
fun LazyListScope.AppearanceSettingsCategory( fun LazyListScope.AppearanceSettingsCategory(
state: State<MainState>, state: State<MainState>,
settingsState: State<SettingsState>,
onMainEvent: (MainEvent) -> Unit, onMainEvent: (MainEvent) -> Unit,
onSettingsEvent: (SettingsEvent) -> Unit,
titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary },
topPadding: Dp = 16.dp, topPadding: Dp = 16.dp,
bottomPadding: Dp = 48.dp bottomPadding: Dp = 48.dp
@ -39,8 +44,10 @@ fun LazyListScope.AppearanceSettingsCategory(
bottomPadding = 0.dp bottomPadding = 0.dp
) )
ColorsSubcategory( ColorsSubcategory(
state, state = state,
settingsState = settingsState,
onMainEvent = onMainEvent, onMainEvent = onMainEvent,
onSettingsEvent = onSettingsEvent,
titleColor = titleColor, titleColor = titleColor,
topPadding = 22.dp, topPadding = 22.dp,
bottomPadding = bottomPadding 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.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth 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.domain.util.UIText
import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility 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.MainEvent
import ua.acclorite.book_story.presentation.data.MainState import ua.acclorite.book_story.presentation.data.MainState
import ua.acclorite.book_story.presentation.ui.Theme import ua.acclorite.book_story.presentation.ui.Theme
@ -79,13 +78,19 @@ fun LazyItemScope.ThemeSetting(
title = stringResource(id = R.string.app_theme_option), title = stringResource(id = R.string.app_theme_option),
padding = horizontalPadding padding = horizontalPadding
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(10.dp))
LazyRow( LazyRow(
Modifier.fillMaxWidth(), Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)
contentPadding = PaddingValues(horizontal = horizontalPadding)
) { ) {
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( ThemeSettingItem(
theme = themeEntry, theme = themeEntry,
darkTheme = state.value.darkTheme!!.isDark(), darkTheme = state.value.darkTheme!!.isDark(),
@ -95,6 +100,10 @@ fun LazyItemScope.ThemeSetting(
) { ) {
onMainEvent(MainEvent.OnChangeTheme(themeEntry.first.toString())) 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.components.CategoryTitle
import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainEvent
import ua.acclorite.book_story.presentation.data.MainState 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. * Colors subcategory.
@ -27,9 +30,12 @@ import ua.acclorite.book_story.presentation.screens.settings.components.ColorPic
*/ */
fun LazyListScope.ColorsSubcategory( fun LazyListScope.ColorsSubcategory(
state: State<MainState>, state: State<MainState>,
settingsState: State<SettingsState>,
onMainEvent: (MainEvent) -> Unit, onMainEvent: (MainEvent) -> Unit,
onSettingsEvent: (SettingsEvent) -> Unit,
titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary },
title: @Composable () -> String = { stringResource(id = R.string.colors_appearance_settings) }, title: @Composable () -> String = { stringResource(id = R.string.colors_appearance_settings) },
backgroundColor: @Composable () -> Color = { MaterialTheme.colorScheme.surfaceContainerLow },
showTitle: Boolean = true, showTitle: Boolean = true,
topPadding: Dp, topPadding: Dp,
bottomPadding: Dp bottomPadding: Dp
@ -55,36 +61,20 @@ fun LazyListScope.ColorsSubcategory(
} }
} }
// todo: This will be replaced with ColorPresets
item { item {
ColorPickerWithTitle( ColorPresetSetting(
modifier = Modifier.animateItem(), state = settingsState,
value = Color(state.value.backgroundColor!!.toULong()), backgroundColor = backgroundColor.invoke(),
title = stringResource(id = R.string.background_color_option), onEvent = onSettingsEvent
onValueChange = {
onMainEvent(
MainEvent.OnChangeBackgroundColor(
it.value.toLong()
)
)
}
) )
} }
item { item {
ColorPickerWithTitle( FastColorPresetChangeSetting(
modifier = Modifier.animateItem(), state = state,
value = Color(state.value.fontColor!!.toULong()), onMainEvent = onMainEvent
title = stringResource(id = R.string.font_color_option),
onValueChange = {
onMainEvent(
MainEvent.OnChangeFontColor(
it.value.toLong()
)
)
}
) )
} }
// ---------
item { item {
Spacer( Spacer(

View file

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

View file

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

View file

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

View file

@ -139,6 +139,8 @@
<string name="read_query">%1$s read</string> <string name="read_query">%1$s read</string>
<string name="file_path_query">File path: %1$s</string> <string name="file_path_query">File path: %1$s</string>
<string name="update_query">Update %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 --> <!-- Action Texts -->
<string name="cancel">Cancel</string> <string name="cancel">Cancel</string>
@ -220,18 +222,22 @@
<string name="line_height_option">Line height</string> <string name="line_height_option">Line height</string>
<string name="paragraph_height_option">Paragraph height</string> <string name="paragraph_height_option">Paragraph height</string>
<string name="paragraph_indentation_option">Paragraph indentation</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="background_color_option">Background color</string>
<string name="font_color_option">Font 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">Check for updates</string>
<string name="check_for_updates_option_desc">Check for newer app version on startup</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="side_padding_option">Side padding</string>
<string name="translator_double_click_to_translate_option">Double click translation</string> <string name="double_click_translation_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_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="red_color">Red</string>
<string name="green_color">Green</string> <string name="green_color">Green</string>
<string name="blue_color">Blue</string> <string name="blue_color">Blue</string>
<string name="color_preset_placeholder">Name a preset…</string>
<!-- Dark Theme properties --> <!-- Dark Theme properties -->
<string name="dark_theme_off">Disabled</string> <string name="dark_theme_off">Disabled</string>
@ -500,10 +506,14 @@
<string name="reset_start_content_desc">Reset start guide</string> <string name="reset_start_content_desc">Reset start guide</string>
<string name="exit_editing_content_desc">Exit editing mode</string> <string name="exit_editing_content_desc">Exit editing mode</string>
<string name="open_in_web_content_desc">Open in web</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="x_content_desc" translatable="false">X</string>
<string name="reddit_content_desc" translatable="false">Reddit</string> <string name="reddit_content_desc" translatable="false">Reddit</string>
<string name="tryzub_content_desc">Tryzub</string> <string name="tryzub_content_desc">Tryzub</string>
<string name="github_project_content_desc">GitHub Project</string> <string name="github_project_content_desc">GitHub Project</string>
<string name="github_profile_content_desc">GitHub Profile</string> <string name="github_profile_content_desc">GitHub Profile</string>
<string name="drag_content_desc">Drag</string>
</resources> </resources>