diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 99dac196..06355c6e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -165,4 +165,7 @@ dependencies { // About open source libraries implementation("com.mikepenz:aboutlibraries-core:latest.release") implementation("com.mikepenz:aboutlibraries-compose-m3:latest.release") + + // Drag & Drop + implementation("sh.calvin.reorderable:reorderable:2.2.0") } \ No newline at end of file diff --git a/app/schemas/ua.acclorite.book_story.data.local.room.BookDatabase/5.json b/app/schemas/ua.acclorite.book_story.data.local.room.BookDatabase/5.json new file mode 100644 index 00000000..8e4af1d0 --- /dev/null +++ b/app/schemas/ua.acclorite.book_story.data.local.room.BookDatabase/5.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/Activity.kt b/app/src/main/java/ua/acclorite/book_story/Activity.kt index e52a08aa..52a14baf 100644 --- a/app/src/main/java/ua/acclorite/book_story/Activity.kt +++ b/app/src/main/java/ua/acclorite/book_story/Activity.kt @@ -38,6 +38,7 @@ import ua.acclorite.book_story.presentation.screens.library.LibraryScreenRoot import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.presentation.screens.reader.ReaderScreenRoot import ua.acclorite.book_story.presentation.screens.settings.SettingsScreenRoot +import ua.acclorite.book_story.presentation.screens.settings.data.SettingsViewModel import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.AppearanceSettingsRoot import ua.acclorite.book_story.presentation.screens.settings.nested.general.GeneralSettingsRoot import ua.acclorite.book_story.presentation.screens.settings.nested.reader.ReaderSettingsRoot @@ -53,11 +54,12 @@ import java.lang.reflect.Field @SuppressLint("DiscouragedPrivateApi") @AndroidEntryPoint class Activity : AppCompatActivity() { - // Initializing all viewModels on app startup + // Initializing all required viewModels on app startup private val mainViewModel: MainViewModel by viewModels() private val libraryViewModel: LibraryViewModel by viewModels() private val historyViewModel: HistoryViewModel by viewModels() private val browseViewModel: BrowseViewModel by viewModels() + private val settingsViewModel: SettingsViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -72,7 +74,7 @@ class Activity : AppCompatActivity() { } // Initializing all variables - mainViewModel.init(libraryViewModel) + mainViewModel.init(libraryViewModel, settingsViewModel) // Splash screen installSplashScreen().apply { diff --git a/app/src/main/java/ua/acclorite/book_story/data/di/AppModule.kt b/app/src/main/java/ua/acclorite/book_story/data/di/AppModule.kt index c1e6b42d..c8d667c2 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/di/AppModule.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/di/AppModule.kt @@ -43,6 +43,7 @@ object AppModule { ) .addMigrations( DatabaseHelper.MIGRATION_2_3, // creates LanguageHistoryEntity table(if does not exist) + DatabaseHelper.MIGRATION_4_5, // creates ColorPresetEntity table(if does not exist) ) .allowMainThreadQueries() .build() diff --git a/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt b/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt index 71921e85..71f89f64 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/di/RepositoryModule.kt @@ -10,6 +10,8 @@ import ua.acclorite.book_story.data.local.notification.UpdatesNotificationServic import ua.acclorite.book_story.data.local.notification.UpdatesNotificationServiceImpl import ua.acclorite.book_story.data.mapper.book.BookMapper import ua.acclorite.book_story.data.mapper.book.BookMapperImpl +import ua.acclorite.book_story.data.mapper.color_preset.ColorPresetMapper +import ua.acclorite.book_story.data.mapper.color_preset.ColorPresetMapperImpl import ua.acclorite.book_story.data.mapper.history.HistoryMapper import ua.acclorite.book_story.data.mapper.history.HistoryMapperImpl import ua.acclorite.book_story.data.parser.FileParser @@ -47,6 +49,12 @@ abstract class RepositoryModule { historyMapperImpl: HistoryMapperImpl ): HistoryMapper + @Binds + @Singleton + abstract fun bindColorPresetMapper( + colorPresetMapperImpl: ColorPresetMapperImpl + ): ColorPresetMapper + @Binds @Singleton abstract fun bindFileParser( diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/dto/ColorPresetEntity.kt b/app/src/main/java/ua/acclorite/book_story/data/local/dto/ColorPresetEntity.kt new file mode 100644 index 00000000..8f3abfa1 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/data/local/dto/ColorPresetEntity.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDao.kt b/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDao.kt index 64ca103c..1ae70f93 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDao.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDao.kt @@ -6,7 +6,9 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Update +import androidx.room.Upsert import ua.acclorite.book_story.data.local.dto.BookEntity +import ua.acclorite.book_story.data.local.dto.ColorPresetEntity import ua.acclorite.book_story.data.local.dto.HistoryEntity @Dao @@ -57,4 +59,23 @@ interface BookDao { @Delete suspend fun deleteHistory(history: List) + + + @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 + + @Delete + suspend fun deleteColorPreset(colorPreset: ColorPresetEntity) + + @Query("DELETE FROM colorpresetentity") + suspend fun deleteColorPresets() } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDatabase.kt b/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDatabase.kt index d32dfc60..ad2ec727 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDatabase.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/local/room/BookDatabase.kt @@ -9,18 +9,21 @@ import androidx.room.migration.AutoMigrationSpec import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import ua.acclorite.book_story.data.local.dto.BookEntity +import ua.acclorite.book_story.data.local.dto.ColorPresetEntity import ua.acclorite.book_story.data.local.dto.HistoryEntity @Database( entities = [ BookEntity::class, - HistoryEntity::class + HistoryEntity::class, + ColorPresetEntity::class, ], - version = 4, + version = 5, autoMigrations = [ AutoMigration(1, 2), AutoMigration(2, 3), AutoMigration(3, 4, spec = DatabaseHelper.MIGRATION_3_4::class), + AutoMigration(4, 5), ], exportSchema = true ) @@ -50,4 +53,19 @@ object DatabaseHelper { @DeleteColumn("BookEntity", "translateWhenOpen") @DeleteTable("LanguageHistoryEntity") class MIGRATION_3_4 : AutoMigrationSpec + + val MIGRATION_4_5 = object : Migration(4, 5) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + "CREATE TABLE IF NOT EXISTS `ColorPresetEntity` (" + + "`id` INTEGER PRIMARY KEY AUTOINCREMENT, " + + "`name` TEXT, " + + "`backgroundColor` INTEGER NOT NULL, " + + "`fontColor` INTEGER NOT NULL, " + + "`isSelected` INTEGER NOT NULL, " + + "`order` INTEGER NOT NULL" + + ")" + ) + } + } } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/color_preset/ColorPresetMapper.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/color_preset/ColorPresetMapper.kt new file mode 100644 index 00000000..5f552807 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/color_preset/ColorPresetMapper.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/mapper/color_preset/ColorPresetMapperImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/mapper/color_preset/ColorPresetMapperImpl.kt new file mode 100644 index 00000000..d11cbded --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/data/mapper/color_preset/ColorPresetMapperImpl.kt @@ -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 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt b/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt index f022f72f..d524098c 100644 --- a/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt +++ b/app/src/main/java/ua/acclorite/book_story/data/repository/BookRepositoryImpl.kt @@ -22,12 +22,14 @@ import ua.acclorite.book_story.data.local.data_store.DataStore import ua.acclorite.book_story.data.local.notification.UpdatesNotificationService import ua.acclorite.book_story.data.local.room.BookDao import ua.acclorite.book_story.data.mapper.book.BookMapper +import ua.acclorite.book_story.data.mapper.color_preset.ColorPresetMapper import ua.acclorite.book_story.data.mapper.history.HistoryMapper import ua.acclorite.book_story.data.parser.FileParser import ua.acclorite.book_story.data.parser.TextParser import ua.acclorite.book_story.data.remote.GithubAPI import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo import ua.acclorite.book_story.domain.model.Book +import ua.acclorite.book_story.domain.model.ColorPreset import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.repository.BookRepository @@ -55,6 +57,7 @@ class BookRepositoryImpl @Inject constructor( private val bookMapper: BookMapper, private val historyMapper: HistoryMapper, + private val colorPresetMapper: ColorPresetMapper, private val fileParser: FileParser, private val textParser: TextParser @@ -667,6 +670,50 @@ class BookRepositoryImpl @Inject constructor( } } } + + override suspend fun updateColorPreset(colorPreset: ColorPreset) { + database.updateColorPreset( + colorPresetMapper.toColorPresetEntity( + colorPreset, + if (colorPreset.id != -1) database.getColorPresetOrder(colorPreset.id) + else database.getColorPresetsSize() + ) + ) + } + + override suspend fun selectColorPreset(colorPreset: ColorPreset) { + database.getColorPresets().map { + it.copy( + isSelected = it.id == colorPreset.id + ) + }.forEach { + database.updateColorPreset(it) + } + } + + override suspend fun getColorPresets(): List { + return database.getColorPresets() + .sortedBy { it.order } + .map { colorPresetMapper.toColorPreset(it) } + } + + override suspend fun reorderColorPresets(orderedColorPresets: List) { + 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 + ) + ) + } } diff --git a/app/src/main/java/ua/acclorite/book_story/domain/model/ColorPreset.kt b/app/src/main/java/ua/acclorite/book_story/domain/model/ColorPreset.kt new file mode 100644 index 00000000..484c46b7 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/model/ColorPreset.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt b/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt index 8015a2b9..ff6c45cd 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/repository/BookRepository.kt @@ -5,6 +5,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import ua.acclorite.book_story.data.remote.dto.LatestReleaseInfo import ua.acclorite.book_story.domain.model.Book +import ua.acclorite.book_story.domain.model.ColorPreset import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.util.CoverImage @@ -89,5 +90,17 @@ interface BookRepository { history: List ) + suspend fun checkForUpdates(postNotification: Boolean): LatestReleaseInfo? + + + suspend fun updateColorPreset(colorPreset: ColorPreset) + + suspend fun selectColorPreset(colorPreset: ColorPreset) + + suspend fun getColorPresets(): List + + suspend fun reorderColorPresets(orderedColorPresets: List) + + suspend fun deleteColorPreset(colorPreset: ColorPreset) } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/DeleteColorPreset.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/DeleteColorPreset.kt new file mode 100644 index 00000000..59eaf6b0 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/DeleteColorPreset.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetColorPresets.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetColorPresets.kt new file mode 100644 index 00000000..e828a64b --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/GetColorPresets.kt @@ -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 { + return repository.getColorPresets() + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/ReorderColorPresets.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/ReorderColorPresets.kt new file mode 100644 index 00000000..f146282c --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/ReorderColorPresets.kt @@ -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) { + repository.reorderColorPresets(reorderedColorPresets) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/SelectColorPreset.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/SelectColorPreset.kt new file mode 100644 index 00000000..ad818039 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/SelectColorPreset.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/use_case/UpdateColorPreset.kt b/app/src/main/java/ua/acclorite/book_story/domain/use_case/UpdateColorPreset.kt new file mode 100644 index 00000000..484f4121 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/domain/use_case/UpdateColorPreset.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/util/Constants.kt b/app/src/main/java/ua/acclorite/book_story/domain/util/Constants.kt index bb6e9577..110f9de4 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/util/Constants.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/util/Constants.kt @@ -2,6 +2,7 @@ package ua.acclorite.book_story.domain.util import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Person +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle @@ -10,6 +11,7 @@ import ua.acclorite.book_story.R import ua.acclorite.book_story.domain.model.Badge import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Category +import ua.acclorite.book_story.domain.model.ColorPreset import ua.acclorite.book_story.domain.model.Credit import ua.acclorite.book_story.domain.model.FontWithName import ua.acclorite.book_story.domain.model.NavigationItem @@ -295,4 +297,12 @@ object Constants { lastOpened = null, category = Category.READING ) + + val DEFAULT_COLOR_PRESET = ColorPreset( + id = -1, + name = null, + backgroundColor = Color.DarkGray, + fontColor = Color.LightGray, + isSelected = false + ) } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/domain/util/DataStoreConstants.kt b/app/src/main/java/ua/acclorite/book_story/domain/util/DataStoreConstants.kt index f0f7b8c3..3fa68fb0 100644 --- a/app/src/main/java/ua/acclorite/book_story/domain/util/DataStoreConstants.kt +++ b/app/src/main/java/ua/acclorite/book_story/domain/util/DataStoreConstants.kt @@ -2,7 +2,6 @@ package ua.acclorite.book_story.domain.util import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey -import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey object DataStoreConstants { @@ -13,8 +12,6 @@ object DataStoreConstants { val PURE_DARK = stringPreferencesKey("pure_dark") val THEME_CONTRAST = stringPreferencesKey("theme_contrast") val SHOW_START_SCREEN = booleanPreferencesKey("guide") - val BACKGROUND_COLOR = longPreferencesKey("background_color") - val FONT_COLOR = longPreferencesKey("font_color") val FONT = stringPreferencesKey("font") val IS_ITALIC = booleanPreferencesKey("font_style") val FONT_SIZE = intPreferencesKey("font_size") @@ -24,4 +21,5 @@ object DataStoreConstants { val CHECK_FOR_UPDATES = booleanPreferencesKey("check_for_updates") val SIDE_PADDING = intPreferencesKey("side_padding") val DOUBLE_CLICK_TRANSLATION = booleanPreferencesKey("double_click_translation") + val FAST_COLOR_PRESET_CHANGE = booleanPreferencesKey("fast_color_preset_change") } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/components/LazyListItem.kt b/app/src/main/java/ua/acclorite/book_story/presentation/components/LazyListItem.kt index 7925c378..e67339ec 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/components/LazyListItem.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/components/LazyListItem.kt @@ -20,11 +20,11 @@ fun LazyListScope.customItems( fun LazyListScope.customItemsIndexed( items: List, - key: (T) -> Any?, + key: (T, Int) -> Any?, content: @Composable LazyItemScope.(Int, T) -> Unit ) { for ((index, item) in items.withIndex()) { - item(key = key(item)) { + item(key = key(item, index)) { content(index, item) } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainEvent.kt index f914b000..f4184c51 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainEvent.kt @@ -12,10 +12,9 @@ sealed class MainEvent { data class OnChangeLineHeight(val lineHeight: Int) : MainEvent() data class OnChangeParagraphHeight(val paragraphHeight: Int) : MainEvent() data class OnChangeParagraphIndentation(val bool: Boolean) : MainEvent() - data class OnChangeBackgroundColor(val color: Long) : MainEvent() - data class OnChangeFontColor(val color: Long) : MainEvent() data class OnChangeShowStartScreen(val bool: Boolean) : MainEvent() data class OnChangeCheckForUpdates(val bool: Boolean) : MainEvent() data class OnChangeSidePadding(val sidePadding: Int) : MainEvent() data class OnChangeDoubleClickTranslation(val bool: Boolean) : MainEvent() + data class OnChangeFastColorPresetChange(val bool: Boolean) : MainEvent() } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainState.kt index 3b7013ac..cbaa72a5 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainState.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainState.kt @@ -4,7 +4,6 @@ import android.os.Build import android.os.Parcelable import androidx.annotation.Keep import androidx.compose.runtime.Immutable -import androidx.compose.ui.graphics.Color import kotlinx.parcelize.Parcelize import ua.acclorite.book_story.domain.util.Constants import ua.acclorite.book_story.domain.util.DataStoreConstants @@ -38,12 +37,11 @@ data class MainState( val lineHeight: Int? = null, val paragraphHeight: Int? = null, val paragraphIndentation: Boolean? = null, - val backgroundColor: Long? = null, - val fontColor: Long? = null, val showStartScreen: Boolean? = null, val checkForUpdates: Boolean? = null, val sidePadding: Int? = null, - val doubleClickTranslation: Boolean? = null + val doubleClickTranslation: Boolean? = null, + val fastColorPresetChange: Boolean? = null, ) : Parcelable { companion object { /** @@ -75,12 +73,6 @@ data class MainState( val showStartScreen: Boolean = data[SHOW_START_SCREEN.name] as? Boolean ?: true - val backgroundColor: Long = data[BACKGROUND_COLOR.name] as? Long - ?: Color.DarkGray.value.toLong() - - val fontColor: Long = data[FONT_COLOR.name] as? Long - ?: Color.LightGray.value.toLong() - val fontFamily: String = data[FONT.name] as? String ?: Constants.FONTS[0].id @@ -109,6 +101,10 @@ data class MainState( data[DOUBLE_CLICK_TRANSLATION.name] as? Boolean ?: false + val fastColorPresetChange: Boolean = + data[FAST_COLOR_PRESET_CHANGE.name] as? Boolean + ?: true + return MainState( language = language, theme = theme.toTheme(), @@ -116,8 +112,6 @@ data class MainState( pureDark = pureDark.toPureDark(), themeContrast = themeContrast.toThemeContrast(), showStartScreen = showStartScreen, - backgroundColor = backgroundColor, - fontColor = fontColor, fontFamily = fontFamily, isItalic = isItalic, fontSize = fontSize, @@ -126,7 +120,8 @@ data class MainState( paragraphIndentation = paragraphIndentation, checkForUpdates = checkForUpdates, sidePadding = sidePadding, - doubleClickTranslation = doubleClickTranslation + doubleClickTranslation = doubleClickTranslation, + fastColorPresetChange = fastColorPresetChange ) } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt index 11f10914..fc8c2837 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/data/MainViewModel.kt @@ -19,6 +19,7 @@ import ua.acclorite.book_story.domain.use_case.SetDatastore import ua.acclorite.book_story.domain.util.Constants import ua.acclorite.book_story.domain.util.DataStoreConstants import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel +import ua.acclorite.book_story.presentation.screens.settings.data.SettingsViewModel import ua.acclorite.book_story.presentation.ui.toDarkTheme import ua.acclorite.book_story.presentation.ui.toPureDark import ua.acclorite.book_story.presentation.ui.toTheme @@ -173,28 +174,6 @@ class MainViewModel @Inject constructor( } } - is MainEvent.OnChangeBackgroundColor -> { - viewModelScope.launch(Dispatchers.IO) { - setDatastore.execute(DataStoreConstants.BACKGROUND_COLOR, event.color) - updateStateWithSavedHandle { - it.copy( - backgroundColor = event.color - ) - } - } - } - - is MainEvent.OnChangeFontColor -> { - viewModelScope.launch(Dispatchers.IO) { - setDatastore.execute(DataStoreConstants.FONT_COLOR, event.color) - updateStateWithSavedHandle { - it.copy( - fontColor = event.color - ) - } - } - } - is MainEvent.OnChangeShowStartScreen -> { viewModelScope.launch(Dispatchers.IO) { setDatastore.execute(DataStoreConstants.SHOW_START_SCREEN, event.bool) @@ -238,11 +217,23 @@ class MainViewModel @Inject constructor( } } } + + is MainEvent.OnChangeFastColorPresetChange -> { + viewModelScope.launch(Dispatchers.IO) { + setDatastore.execute(DataStoreConstants.FAST_COLOR_PRESET_CHANGE, event.bool) + updateStateWithSavedHandle { + it.copy( + fastColorPresetChange = event.bool + ) + } + } + } } } fun init( - libraryViewModel: LibraryViewModel + libraryViewModel: LibraryViewModel, + settingsViewModel: SettingsViewModel, ) { viewModelScope.launch(Dispatchers.Main) { val settings = getAllSettings.execute(viewModelScope) @@ -265,7 +256,12 @@ class MainViewModel @Inject constructor( } viewModelScope.launch(Dispatchers.IO) { - libraryViewModel.isReady.collectLatest { ready -> + combine( + libraryViewModel.isReady, + settingsViewModel.isReady + ) { (libraryViewModelReady, settingsViewModelReady) -> + libraryViewModelReady && settingsViewModelReady + }.collectLatest { ready -> isViewModelReady.update { ready } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt index e7af3f1b..8d6a84a5 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/ReaderScreen.kt @@ -5,6 +5,7 @@ import android.os.Build import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler +import androidx.compose.animation.animateColorAsState import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background @@ -34,7 +35,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource @@ -67,11 +67,15 @@ import ua.acclorite.book_story.presentation.screens.reader.components.ReaderEndI import ua.acclorite.book_story.presentation.screens.reader.components.ReaderTextParagraph import ua.acclorite.book_story.presentation.screens.reader.components.app_bar.ReaderBottomBar import ua.acclorite.book_story.presentation.screens.reader.components.app_bar.ReaderTopBar +import ua.acclorite.book_story.presentation.screens.reader.components.readerFastColorPresetChange import ua.acclorite.book_story.presentation.screens.reader.components.settings_bottom_sheet.ReaderSettingsBottomSheet import ua.acclorite.book_story.presentation.screens.reader.components.start_item.ReaderStartItem import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent import ua.acclorite.book_story.presentation.screens.reader.data.ReaderState import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel +import ua.acclorite.book_story.presentation.screens.settings.data.SettingsEvent +import ua.acclorite.book_story.presentation.screens.settings.data.SettingsState +import ua.acclorite.book_story.presentation.screens.settings.data.SettingsViewModel @Composable fun ReaderScreenRoot(screen: Screen.Reader) { @@ -82,9 +86,11 @@ fun ReaderScreenRoot(screen: Screen.Reader) { val mainViewModel: MainViewModel = hiltViewModel() val libraryViewModel: LibraryViewModel = hiltViewModel() val historyViewModel: HistoryViewModel = hiltViewModel() + val settingsViewModel: SettingsViewModel = hiltViewModel() val state = viewModel.state.collectAsState() val mainState = mainViewModel.state.collectAsState() + val settingsState = settingsViewModel.state.collectAsState() val lazyListState = rememberSaveable( state.value.listState, @@ -137,10 +143,12 @@ fun ReaderScreenRoot(screen: Screen.Reader) { ReaderScreen( state = state, mainState = mainState, + settingsState = settingsState, lazyListState = lazyListState, onNavigate = { navigator.it() }, onEvent = viewModel::onEvent, onMainEvent = mainViewModel::onEvent, + onSettingsEvent = settingsViewModel::onEvent, onLibraryEvent = libraryViewModel::onEvent, onHistoryEvent = historyViewModel::onEvent ) @@ -151,10 +159,12 @@ fun ReaderScreenRoot(screen: Screen.Reader) { private fun ReaderScreen( state: State, mainState: State, + settingsState: State, lazyListState: LazyListState, onNavigate: OnNavigate, onEvent: (ReaderEvent) -> Unit, onMainEvent: (MainEvent) -> Unit, + onSettingsEvent: (SettingsEvent) -> Unit, onLibraryEvent: (LibraryEvent) -> Unit, onHistoryEvent: (HistoryEvent) -> Unit, ) { @@ -186,12 +196,14 @@ private fun ReaderScreen( it.id == mainState.value.fontFamily } ?: Constants.FONTS[0] } - val fontColor = remember(mainState.value.fontColor) { - Color(mainState.value.fontColor!!.toULong()) - } - val backgroundColor = remember(mainState.value.backgroundColor) { - Color(mainState.value.backgroundColor!!.toULong()) - } + val backgroundColor = animateColorAsState( + settingsState.value.selectedColorPreset.backgroundColor, + label = "" + ) + val fontColor = animateColorAsState( + settingsState.value.selectedColorPreset.fontColor, + label = "" + ) val lineHeight = remember(mainState.value.fontSize, mainState.value.lineHeight) { (mainState.value.fontSize!! + mainState.value.lineHeight!!).sp @@ -212,8 +224,10 @@ private fun ReaderScreen( if (state.value.showSettingsBottomSheet) { ReaderSettingsBottomSheet( mainState = mainState, + settingsState = settingsState, onEvent = onEvent, - onMainEvent = onMainEvent + onMainEvent = onMainEvent, + onSettingsEvent = onSettingsEvent ) } @@ -298,7 +312,7 @@ private fun ReaderScreen( state = lazyListState, modifier = Modifier .fillMaxSize() - .background(backgroundColor) + .background(backgroundColor.value) .then( if (!state.value.loading && toolbarHidden) { Modifier @@ -314,6 +328,25 @@ private fun ReaderScreen( } else { Modifier } + ) + .readerFastColorPresetChange( + fastColorPresetChangeEnabled = mainState.value.fastColorPresetChange!!, + toolbarHidden = toolbarHidden, + onSettingsEvent = onSettingsEvent, + presetChanged = { + Toast + .makeText( + context, + context.getString( + R.string.color_preset_selected_query, + it + .asString(context) + .trim() + ), + Toast.LENGTH_SHORT + ) + .show() + } ), verticalArrangement = Arrangement.spacedBy(paragraphHeight) ) { @@ -332,7 +365,7 @@ private fun ReaderScreen( line = line.second, context = context, fontFamily = fontFamily, - fontColor = fontColor, + fontColor = fontColor.value, lineHeight = lineHeight, fontStyle = fontStyle, fontSize = mainState.value.fontSize!!.sp, diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderFastColorPresetChange.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderFastColorPresetChange.kt new file mode 100644 index 00000000..88585ed1 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/ReaderFastColorPresetChange.kt @@ -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 + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheet.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheet.kt index d50144d7..21c94108 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheet.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/reader/components/settings_bottom_sheet/ReaderSettingsBottomSheet.kt @@ -26,6 +26,8 @@ import ua.acclorite.book_story.presentation.components.CustomBottomSheet import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainState import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent +import ua.acclorite.book_story.presentation.screens.settings.data.SettingsEvent +import ua.acclorite.book_story.presentation.screens.settings.data.SettingsState import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.subcategories.ColorsSubcategory import ua.acclorite.book_story.presentation.screens.settings.nested.reader.components.ReaderSettingsCategory @@ -33,15 +35,19 @@ import ua.acclorite.book_story.presentation.screens.settings.nested.reader.compo * Settings bottom sheet. Has General and Colors categories. * * @param mainState [MainState]. + * @param settingsState [SettingsState]. * @param onEvent [ReaderEvent] callback. * @param onMainEvent [MainEvent] callback. + * @param onSettingsEvent [SettingsEvent] callback. */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun ReaderSettingsBottomSheet( mainState: State, + settingsState: State, onEvent: (ReaderEvent) -> Unit, - onMainEvent: (MainEvent) -> Unit + onMainEvent: (MainEvent) -> Unit, + onSettingsEvent: (SettingsEvent) -> Unit ) { val pagerState = rememberPagerState { 2 } val currentPage by remember { derivedStateOf { pagerState.currentPage } } @@ -57,7 +63,7 @@ fun ReaderSettingsBottomSheet( ) val height = remember(currentPage) { - if (currentPage == 1) 0.5f else 0.7f + if (currentPage == 1) 0.6f else 0.7f } val animatedHeight by animateFloatAsState( targetValue = height, @@ -107,8 +113,11 @@ fun ReaderSettingsBottomSheet( if (page == 1) { ColorsSubcategory( state = mainState, + settingsState = settingsState, onMainEvent = onMainEvent, + onSettingsEvent = onSettingsEvent, showTitle = false, + backgroundColor = { MaterialTheme.colorScheme.surfaceContainer }, topPadding = 16.dp, bottomPadding = 8.dp + it ) @@ -116,15 +125,4 @@ fun ReaderSettingsBottomSheet( } } } -} - - - - - - - - - - - +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ColorPickerWithTitle.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ColorPickerWithTitle.kt index 05580a87..4228b375 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ColorPickerWithTitle.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/components/ColorPickerWithTitle.kt @@ -1,7 +1,6 @@ package ua.acclorite.book_story.presentation.screens.settings.components import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -9,21 +8,27 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.History import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.debounce import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.components.CustomIconButton @@ -33,22 +38,33 @@ import ua.acclorite.book_story.presentation.components.CustomIconButton * * @param modifier Modifier. * @param value Target [Color]. + * @param presetId Current Color Preset id(to trigger initial value change). * @param title Title. * @param horizontalPadding Horizontal item padding. * @param verticalPadding Vertical item padding. * @param onValueChange Callback when target color changes. */ +@OptIn(FlowPreview::class) @Composable fun ColorPickerWithTitle( modifier: Modifier = Modifier, value: Color, + presetId: Int, title: String, horizontalPadding: Dp = 18.dp, verticalPadding: Dp = 8.dp, onValueChange: (Color) -> Unit ) { - val initialValue = rememberSaveable { value.value.toString() } - var color by remember { mutableStateOf(value) } + val initialValue = rememberSaveable(presetId) { value.value.toString() } + var color by remember(value) { mutableStateOf(value) } + + LaunchedEffect(color) { + snapshotFlow { + color + }.debounce(50).collectLatest { + onValueChange(it) + } + } Column( modifier @@ -66,7 +82,6 @@ fun ColorPickerWithTitle( title = stringResource(id = R.string.red_color), onValueChange = { color = color.copy(red = it) - onValueChange(color) } ) RevertibleSlider( @@ -75,7 +90,6 @@ fun ColorPickerWithTitle( title = stringResource(id = R.string.green_color), onValueChange = { color = color.copy(green = it) - onValueChange(color) } ) RevertibleSlider( @@ -84,7 +98,6 @@ fun ColorPickerWithTitle( title = stringResource(id = R.string.blue_color), onValueChange = { color = color.copy(blue = it) - onValueChange(color) } ) } @@ -108,7 +121,7 @@ private fun RevertibleSlider( ) ) { SliderWithTitle( - modifier = Modifier.weight(0.85f), + modifier = Modifier.weight(1f), value = value, title = title, toValue = 255, @@ -118,18 +131,18 @@ private fun RevertibleSlider( horizontalPadding = 0.dp, verticalPadding = 0.dp ) - Box(modifier = Modifier.weight(0.15f), contentAlignment = Alignment.CenterEnd) { - CustomIconButton( - modifier = Modifier.size(28.dp), - icon = Icons.Default.History, - contentDescription = R.string.revert_content_desc, - disableOnClick = false, - enabled = initialValue != value.first, - color = if (initialValue == value.first) MaterialTheme.colorScheme.onSurfaceVariant - else MaterialTheme.colorScheme.onSurface - ) { - onValueChange(initialValue) - } + + Spacer(modifier = Modifier.width(10.dp)) + CustomIconButton( + modifier = Modifier.size(28.dp), + icon = Icons.Default.History, + contentDescription = R.string.revert_content_desc, + disableOnClick = false, + enabled = initialValue != value.first, + color = if (initialValue == value.first) MaterialTheme.colorScheme.onSurfaceVariant + else MaterialTheme.colorScheme.onSurface + ) { + onValueChange(initialValue) } } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsEvent.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsEvent.kt index c9bfa87f..562464b9 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsEvent.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsEvent.kt @@ -4,8 +4,11 @@ package ua.acclorite.book_story.presentation.screens.settings.data import androidx.activity.ComponentActivity import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionState +import ua.acclorite.book_story.domain.util.ID +import ua.acclorite.book_story.domain.util.UIText @Immutable sealed class SettingsEvent { @@ -15,4 +18,27 @@ sealed class SettingsEvent { val notificationsPermissionState: PermissionState, val onChangeCheckForUpdates: (Boolean) -> Unit ) : SettingsEvent() + + data class OnSelectPreviousPreset( + val onSelected: suspend (name: UIText) -> Unit + ) : SettingsEvent() + + data class OnSelectNextPreset( + val onSelected: suspend (name: UIText) -> Unit + ) : SettingsEvent() + + data class OnSelectColorPreset(val id: ID) : SettingsEvent() + data class OnDeleteColorPreset(val id: ID) : SettingsEvent() + data class OnScrollToColorPreset(val scrollTo: Int) : SettingsEvent() + data class OnUpdateColorPresetTitle(val id: ID, val title: String) : SettingsEvent() + data class OnShuffleColorPreset(val id: ID) : SettingsEvent() + data class OnUpdateColorPresetColor( + val id: ID, + val backgroundColor: Color?, + val fontColor: Color? + ) : SettingsEvent() + + data object OnAddColorPreset : SettingsEvent() + data class OnReorderColorPresets(val from: Int, val to: Int) : SettingsEvent() + data object OnConfirmReorderColorPresets : SettingsEvent() } \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsState.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsState.kt new file mode 100644 index 00000000..06e643dd --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsState.kt @@ -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 = emptyList(), + val selectedColorPreset: ColorPreset = Constants.DEFAULT_COLOR_PRESET, + val animateColorPreset: Boolean = false, + val colorPresetsListState: LazyListState = LazyListState() +) \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsViewModel.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsViewModel.kt index d2367409..36e5a939 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsViewModel.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/data/SettingsViewModel.kt @@ -9,24 +9,92 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.shouldShowRationale import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.yield +import ua.acclorite.book_story.R +import ua.acclorite.book_story.domain.model.ColorPreset +import ua.acclorite.book_story.domain.use_case.DeleteColorPreset +import ua.acclorite.book_story.domain.use_case.GetColorPresets +import ua.acclorite.book_story.domain.use_case.ReorderColorPresets +import ua.acclorite.book_story.domain.use_case.SelectColorPreset +import ua.acclorite.book_story.domain.use_case.UpdateColorPreset +import ua.acclorite.book_story.domain.util.Constants +import ua.acclorite.book_story.domain.util.ID +import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.presentation.data.launchActivity import javax.inject.Inject +import kotlin.random.Random @OptIn(ExperimentalPermissionsApi::class) @HiltViewModel class SettingsViewModel @Inject constructor( - + private val getColorPresets: GetColorPresets, + private val updateColorPreset: UpdateColorPreset, + private val deleteColorPreset: DeleteColorPreset, + private val selectColorPreset: SelectColorPreset, + private val reorderColorPresets: ReorderColorPresets ) : ViewModel() { - private var notificationsPermissionJob: Job? = null + private val _state = MutableStateFlow(SettingsState()) + val state = _state.asStateFlow() + private val _isReady = MutableStateFlow(false) + val isReady = _isReady.asStateFlow() + + private var notificationsPermissionJob: Job? = null + private var selectColorPresetJob: Job? = null + private var addColorPresetJob: Job? = null + private var deleteColorPresetJob: Job? = null + private var updateColorColorPresetJob: Job? = null + private var updateTitleColorPresetJob: Job? = null + private var shuffleColorPresetJob: Job? = null + + init { + viewModelScope.launch(Dispatchers.IO) { + var colorPresets = getColorPresets.execute() + + if (colorPresets.isEmpty()) { + updateColorPreset.execute(Constants.DEFAULT_COLOR_PRESET) + onSelectColorPreset(getColorPresets.execute().first()) + colorPresets = getColorPresets.execute() + } + + val scrollIndex = colorPresets.indexOfFirst { + it.isSelected + } + if (scrollIndex != -1) { + launch(Dispatchers.Main) { + try { + _state.value.colorPresetsListState.requestScrollToItem( + index = scrollIndex + ) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + _state.update { + it.copy( + selectedColorPreset = selectedColorPreset(colorPresets), + colorPresets = colorPresets + ) + } + _isReady.update { + true + } + } + } + + @Suppress("LABEL_NAME_CLASH") fun onEvent(event: SettingsEvent) { when (event) { - is SettingsEvent.OnGeneralChangeCheckForUpdates -> { if (!event.enable) { event.onChangeCheckForUpdates(false) @@ -75,6 +143,385 @@ class SettingsViewModel @Inject constructor( } } } + + is SettingsEvent.OnSelectPreviousPreset -> { + viewModelScope.launch { + cancelColorPresetJobs() + selectColorPresetJob = launch { + val colorPresets = _state.value.colorPresets + val selectedPreset = _state.value.selectedColorPreset + + if (colorPresets.size == 1) { + return@launch + } + + val selectedPresetIndex = colorPresets.indexOf(selectedPreset) + if (selectedPresetIndex == -1) { + return@launch + } + + val previousColorPresetIndex = when (selectedPresetIndex) { + 0 -> { + colorPresets.lastIndex + } + + else -> { + selectedPresetIndex - 1 + } + } + val previousColorPreset = colorPresets.getOrNull(previousColorPresetIndex) + ?: return@launch + + yield() + + onSelectColorPreset(previousColorPreset) + val updatedColorPresets = _state.value.colorPresets.map { + it.copy(isSelected = previousColorPreset.id == it.id) + } + _state.update { + it.copy( + selectedColorPreset = selectedColorPreset(updatedColorPresets), + colorPresets = updatedColorPresets + ) + } + + event.onSelected( + if ((previousColorPreset.name ?: "").isBlank()) { + UIText.StringResource( + R.string.color_preset_query, + previousColorPreset.id.toString() + ) + } else { + UIText.StringValue(previousColorPreset.name!!) + } + ) + } + } + } + + is SettingsEvent.OnSelectNextPreset -> { + viewModelScope.launch { + cancelColorPresetJobs() + selectColorPresetJob = launch { + val colorPresets = _state.value.colorPresets + val selectedPreset = _state.value.selectedColorPreset + + if (colorPresets.size == 1) { + return@launch + } + + val selectedPresetIndex = colorPresets.indexOf(selectedPreset) + if (selectedPresetIndex == -1) { + return@launch + } + + val nextColorPresetIndex = when (selectedPresetIndex) { + colorPresets.lastIndex -> { + 0 + } + + else -> { + selectedPresetIndex + 1 + } + } + val nextColorPreset = colorPresets.getOrNull(nextColorPresetIndex) + ?: return@launch + + yield() + + onSelectColorPreset(nextColorPreset) + val updatedColorPresets = _state.value.colorPresets.map { + it.copy(isSelected = nextColorPreset.id == it.id) + } + _state.update { + it.copy( + selectedColorPreset = selectedColorPreset(updatedColorPresets), + colorPresets = updatedColorPresets + ) + } + + event.onSelected( + if ((nextColorPreset.name ?: "").isBlank()) { + UIText.StringResource( + R.string.color_preset_query, + nextColorPreset.id.toString() + ) + } else { + UIText.StringValue(nextColorPreset.name!!) + } + ) + } + } + } + + is SettingsEvent.OnSelectColorPreset -> { + viewModelScope.launch { + cancelColorPresetJobs() + selectColorPresetJob = launch { + val colorPreset = getColorPresetById(event.id) ?: return@launch + + yield() + + onSelectColorPreset(colorPreset, animate = true) + val colorPresets = _state.value.colorPresets.map { + it.copy(isSelected = colorPreset.id == it.id) + } + _state.update { + it.copy( + selectedColorPreset = selectedColorPreset(colorPresets), + colorPresets = colorPresets + ) + } + } + } + } + + is SettingsEvent.OnDeleteColorPreset -> { + viewModelScope.launch { + cancelColorPresetJobs() + deleteColorPresetJob = launch { + val colorPreset = getColorPresetById(event.id) ?: return@launch + + yield() + + val position = _state.value.colorPresets.indexOf(colorPreset) + if (position == -1) { + return@launch + } + + val nextPosition = if (position == _state.value.colorPresets.lastIndex) { + position - 1 + } else { + position + } + + yield() + + deleteColorPreset.execute(colorPreset) + onSelectColorPreset(getColorPresets.execute()[nextPosition]) + val colorPresets = getColorPresets.execute() + + _state.update { + it.copy( + selectedColorPreset = selectedColorPreset(colorPresets), + colorPresets = colorPresets + ) + } + + onEvent(SettingsEvent.OnScrollToColorPreset(nextPosition)) + } + } + } + + is SettingsEvent.OnScrollToColorPreset -> { + viewModelScope.launch { + try { + _state.value.colorPresetsListState.requestScrollToItem( + event.scrollTo + ) + } catch (e: Exception) { + e.printStackTrace() + } + } + } + + is SettingsEvent.OnUpdateColorPresetTitle -> { + viewModelScope.launch { + cancelColorPresetJobs() + updateTitleColorPresetJob = launch { + val colorPreset = getColorPresetById(event.id) ?: return@launch + + yield() + + val updatedColorPreset = colorPreset.copy( + name = event.title + ) + + yield() + + updateColorPreset.execute(updatedColorPreset) + _state.update { + it.copy( + selectedColorPreset = updatedColorPreset, + colorPresets = it.colorPresets.updateColorPreset( + updatedColorPreset + ) + ) + } + } + } + } + + is SettingsEvent.OnShuffleColorPreset -> { + viewModelScope.launch { + cancelColorPresetJobs() + shuffleColorPresetJob = launch { + val colorPreset = getColorPresetById(event.id) ?: return@launch + + yield() + + val shuffledColorPreset = colorPreset.copy( + backgroundColor = colorPreset.backgroundColor.copy( + red = Random.nextFloat(), + green = Random.nextFloat(), + blue = Random.nextFloat() + ), + fontColor = colorPreset.fontColor.copy( + red = Random.nextFloat(), + green = Random.nextFloat(), + blue = Random.nextFloat() + ) + ) + + yield() + + updateColorPreset.execute(shuffledColorPreset) + _state.update { + it.copy( + selectedColorPreset = shuffledColorPreset, + colorPresets = it.colorPresets.updateColorPreset( + shuffledColorPreset + ) + ) + } + } + } + } + + is SettingsEvent.OnAddColorPreset -> { + viewModelScope.launch { + cancelColorPresetJobs() + addColorPresetJob = launch { + yield() + + val newColorPreset = Constants.DEFAULT_COLOR_PRESET + updateColorPreset.execute(newColorPreset) + + onSelectColorPreset(getColorPresets.execute().last()) + val colorPresets = getColorPresets.execute() + + _state.update { + it.copy( + selectedColorPreset = selectedColorPreset(colorPresets), + colorPresets = colorPresets + ) + } + + onEvent(SettingsEvent.OnScrollToColorPreset(colorPresets.lastIndex)) + } + } + } + + is SettingsEvent.OnUpdateColorPresetColor -> { + viewModelScope.launch { + cancelColorPresetJobs() + updateColorColorPresetJob = launch { + val colorPreset = getColorPresetById(event.id) ?: return@launch + + yield() + + val updatedColorPreset = colorPreset.copy( + backgroundColor = event.backgroundColor + ?: colorPreset.backgroundColor, + fontColor = event.fontColor + ?: colorPreset.fontColor + ) + + yield() + + updateColorPreset.execute(updatedColorPreset) + _state.update { + it.copy( + selectedColorPreset = updatedColorPreset, + colorPresets = it.colorPresets.updateColorPreset( + updatedColorPreset + ) + ) + } + } + } + } + + is SettingsEvent.OnReorderColorPresets -> { + viewModelScope.launch { + cancelColorPresetJobs() + launch { + val reorderedColorPresets = + _state.value.colorPresets.toMutableList().apply { + add(event.to, removeAt(event.from)) + } + + _state.update { + it.copy( + colorPresets = reorderedColorPresets + ) + } + } + } + } + + is SettingsEvent.OnConfirmReorderColorPresets -> { + viewModelScope.launch { + cancelColorPresetJobs() + launch { + reorderColorPresets.execute(_state.value.colorPresets) + } + } + } + } + } + + private fun cancelColorPresetJobs() { + selectColorPresetJob?.cancel() + addColorPresetJob?.cancel() + updateTitleColorPresetJob?.cancel() + shuffleColorPresetJob?.cancel() + updateColorColorPresetJob?.cancel() + deleteColorPresetJob?.cancel() + } + + private suspend fun onSelectColorPreset(colorPreset: ColorPreset, animate: Boolean = false) { + selectColorPreset.execute(colorPreset) + _state.update { + it.copy( + animateColorPreset = animate + ) + } + } + + private fun getColorPresetById(id: ID): ColorPreset? { + return _state.value.colorPresets.firstOrNull { + it.id == id + } + } + + private fun selectedColorPreset(colorPresets: List? = 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.updateColorPreset(colorPreset: ColorPreset): List { + if (size == 1) { + return listOf(colorPreset) + } + + return this.map { + if (it.isSelected) { + colorPreset + } else { + it + } } } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/AppearanceSettings.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/AppearanceSettings.kt index 17b25d80..b922e69b 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/AppearanceSettings.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/AppearanceSettings.kt @@ -2,6 +2,7 @@ package ua.acclorite.book_story.presentation.screens.settings.nested.appearance import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding @@ -27,19 +28,26 @@ import ua.acclorite.book_story.presentation.data.LocalNavigator import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainState import ua.acclorite.book_story.presentation.data.MainViewModel +import ua.acclorite.book_story.presentation.screens.settings.data.SettingsEvent +import ua.acclorite.book_story.presentation.screens.settings.data.SettingsState +import ua.acclorite.book_story.presentation.screens.settings.data.SettingsViewModel import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.AppearanceSettingsCategory @Composable fun AppearanceSettingsRoot() { val navigator = LocalNavigator.current val mainViewModel: MainViewModel = hiltViewModel() + val settingsViewModel: SettingsViewModel = hiltViewModel() val state = mainViewModel.state.collectAsState() + val settingsState = settingsViewModel.state.collectAsState() AppearanceSettings( state = state, + settingsState = settingsState, onNavigate = { navigator.it() }, - onMainEvent = mainViewModel::onEvent + onMainEvent = mainViewModel::onEvent, + onSettingsEvent = settingsViewModel::onEvent ) } @@ -47,8 +55,10 @@ fun AppearanceSettingsRoot() { @Composable private fun AppearanceSettings( state: State, + settingsState: State, onNavigate: OnNavigate, - onMainEvent: (MainEvent) -> Unit + onMainEvent: (MainEvent) -> Unit, + onSettingsEvent: (SettingsEvent) -> Unit ) { val scrollState = TopAppBarDefaults.collapsibleUntilExitScrollBehaviorWithLazyListState() @@ -56,7 +66,8 @@ private fun AppearanceSettings( Modifier .fillMaxSize() .nestedScroll(scrollState.first.nestedScrollConnection) - .windowInsetsPadding(WindowInsets.navigationBars), + .windowInsetsPadding(WindowInsets.navigationBars) + .imePadding(), containerColor = MaterialTheme.colorScheme.surface, topBar = { LargeTopAppBar( @@ -82,7 +93,9 @@ private fun AppearanceSettings( ) { AppearanceSettingsCategory( state = state, - onMainEvent = onMainEvent + settingsState = settingsState, + onMainEvent = onMainEvent, + onSettingsEvent = onSettingsEvent ) } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/AppearanceSettingsCategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/AppearanceSettingsCategory.kt index 68e5e907..c9300fc9 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/AppearanceSettingsCategory.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/AppearanceSettingsCategory.kt @@ -11,6 +11,8 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainState +import ua.acclorite.book_story.presentation.screens.settings.data.SettingsEvent +import ua.acclorite.book_story.presentation.screens.settings.data.SettingsState import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.subcategories.ColorsSubcategory import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.subcategories.ThemePreferencesSubcategory @@ -20,13 +22,16 @@ import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.c * * @param state [MainState] instance. * @param onMainEvent [MainEvent] callback. + * @param onSettingsEvent [SettingsEvent] callback. * @param titleColor Title's color. * @param topPadding Top padding to be applied. * @param bottomPadding Bottom padding to be applied. */ fun LazyListScope.AppearanceSettingsCategory( state: State, + settingsState: State, onMainEvent: (MainEvent) -> Unit, + onSettingsEvent: (SettingsEvent) -> Unit, titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, topPadding: Dp = 16.dp, bottomPadding: Dp = 48.dp @@ -39,8 +44,10 @@ fun LazyListScope.AppearanceSettingsCategory( bottomPadding = 0.dp ) ColorsSubcategory( - state, + state = state, + settingsState = settingsState, onMainEvent = onMainEvent, + onSettingsEvent = onSettingsEvent, titleColor = titleColor, topPadding = 22.dp, bottomPadding = bottomPadding diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/ColorPresetSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/ColorPresetSetting.kt new file mode 100644 index 00000000..17a6a119 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/ColorPresetSetting.kt @@ -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, + 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() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/FastColorPresetChangeSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/FastColorPresetChangeSetting.kt new file mode 100644 index 00000000..39fb07d6 --- /dev/null +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/FastColorPresetChangeSetting.kt @@ -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, + 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!! + ) + ) + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/ThemeSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/ThemeSetting.kt index b47b13e0..bd0135dc 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/ThemeSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/settings/ThemeSetting.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -44,7 +43,7 @@ import ua.acclorite.book_story.domain.util.Constants import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.components.CustomAnimatedVisibility -import ua.acclorite.book_story.presentation.components.customItems +import ua.acclorite.book_story.presentation.components.customItemsIndexed import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainState import ua.acclorite.book_story.presentation.ui.Theme @@ -79,13 +78,19 @@ fun LazyItemScope.ThemeSetting( title = stringResource(id = R.string.app_theme_option), padding = horizontalPadding ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(10.dp)) LazyRow( Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), - contentPadding = PaddingValues(horizontal = horizontalPadding) + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - customItems(themes, key = { it.first.name }) { themeEntry -> + customItemsIndexed( + themes, + key = { item, _ -> item.first.name } + ) { index, themeEntry -> + if (index == 0) { + Spacer(modifier = Modifier.width(horizontalPadding)) + } + ThemeSettingItem( theme = themeEntry, darkTheme = state.value.darkTheme!!.isDark(), @@ -95,6 +100,10 @@ fun LazyItemScope.ThemeSetting( ) { onMainEvent(MainEvent.OnChangeTheme(themeEntry.first.toString())) } + + if (index == themes.lastIndex) { + Spacer(modifier = Modifier.width(horizontalPadding)) + } } } } diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/subcategories/ColorsSubcategory.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/subcategories/ColorsSubcategory.kt index 6a24b68e..8ce4d5ca 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/subcategories/ColorsSubcategory.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/appearance/components/subcategories/ColorsSubcategory.kt @@ -19,7 +19,10 @@ import ua.acclorite.book_story.R import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainState -import ua.acclorite.book_story.presentation.screens.settings.components.ColorPickerWithTitle +import ua.acclorite.book_story.presentation.screens.settings.data.SettingsEvent +import ua.acclorite.book_story.presentation.screens.settings.data.SettingsState +import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.settings.ColorPresetSetting +import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.settings.FastColorPresetChangeSetting /** * Colors subcategory. @@ -27,9 +30,12 @@ import ua.acclorite.book_story.presentation.screens.settings.components.ColorPic */ fun LazyListScope.ColorsSubcategory( state: State, + settingsState: State, onMainEvent: (MainEvent) -> Unit, + onSettingsEvent: (SettingsEvent) -> Unit, titleColor: @Composable () -> Color = { MaterialTheme.colorScheme.primary }, title: @Composable () -> String = { stringResource(id = R.string.colors_appearance_settings) }, + backgroundColor: @Composable () -> Color = { MaterialTheme.colorScheme.surfaceContainerLow }, showTitle: Boolean = true, topPadding: Dp, bottomPadding: Dp @@ -55,36 +61,20 @@ fun LazyListScope.ColorsSubcategory( } } - // todo: This will be replaced with ColorPresets item { - ColorPickerWithTitle( - modifier = Modifier.animateItem(), - value = Color(state.value.backgroundColor!!.toULong()), - title = stringResource(id = R.string.background_color_option), - onValueChange = { - onMainEvent( - MainEvent.OnChangeBackgroundColor( - it.value.toLong() - ) - ) - } + ColorPresetSetting( + state = settingsState, + backgroundColor = backgroundColor.invoke(), + onEvent = onSettingsEvent ) } + item { - ColorPickerWithTitle( - modifier = Modifier.animateItem(), - value = Color(state.value.fontColor!!.toULong()), - title = stringResource(id = R.string.font_color_option), - onValueChange = { - onMainEvent( - MainEvent.OnChangeFontColor( - it.value.toLong() - ) - ) - } + FastColorPresetChangeSetting( + state = state, + onMainEvent = onMainEvent ) } - // --------- item { Spacer( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/DoubleClickTranslationSetting.kt b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/DoubleClickTranslationSetting.kt index 45197da2..22c49a24 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/DoubleClickTranslationSetting.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/screens/settings/nested/reader/components/settings/DoubleClickTranslationSetting.kt @@ -22,8 +22,8 @@ fun LazyItemScope.DoubleClickTranslationSetting( SwitchWithTitle( selected = state.value.doubleClickTranslation!!, modifier = Modifier.animateItem(), - title = stringResource(id = R.string.translator_double_click_to_translate_option), - description = stringResource(id = R.string.translator_double_click_to_translate_option_desc), + title = stringResource(id = R.string.double_click_translation_option), + description = stringResource(id = R.string.double_click_translation_option_desc), onClick = { onMainEvent( MainEvent.OnChangeDoubleClickTranslation( diff --git a/app/src/main/java/ua/acclorite/book_story/presentation/ui/Transitions.kt b/app/src/main/java/ua/acclorite/book_story/presentation/ui/Transitions.kt index cc1440cd..2f532141 100644 --- a/app/src/main/java/ua/acclorite/book_story/presentation/ui/Transitions.kt +++ b/app/src/main/java/ua/acclorite/book_story/presentation/ui/Transitions.kt @@ -24,15 +24,16 @@ object Transitions { val SlidingTransitionIn = fadeIn(tween(350)) + slideInHorizontally(tween(350)) { it / 16 } - val BackSlidingTransitionIn = fadeIn(tween(350)) + slideInHorizontally(tween(350)) { -it / 16 } val SlidingTransitionOut = fadeOut(tween(250)) + slideOutHorizontally(tween(350)) { -it / 16 } - val BackSlidingTransitionOut = fadeOut(tween(250)) + slideOutHorizontally(tween(350)) { it / 16 } + + val NoEnterAnimation = fadeIn(tween(0)) + val NoExitAnimation = fadeOut(tween(0)) } @Composable diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 94afd041..fadaaf35 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -124,6 +124,8 @@ %1$s прочитано Шлях до файлу: %1$s Оновлення %1$s + Пресет %1$s + Вибрано «%1$s» Відмінити @@ -205,18 +207,22 @@ Висота рядка Висота абзаца Абзацний відступ + Пресет кольорів Колір фона Колір шрифта Перевіряти на оновлення Перевіряти на новішу версію застосунку при запуску Бокові відступи - Переклад подвійним кліком - Двічі клацніть на абзац, щоб перекласти його + Переклад подвійним кліком + Двічі клацніть на абзац, щоб перекласти його + Швидка зміна пресета кольорів + Проведіть вліво або вправо, щоб змінити пресет кольорів - + Червоний Зелений Синій + Назвіть пресет… Вимкнена @@ -484,8 +490,12 @@ Скинути початковий посібник Вийти з режиму редагування Відкрити в браузері + Видалити пресет кольорів + Створити новий пресет кольорів + Перетасувати кольори Тризуб GitHub Проєкт GitHub Профіль + Тягнути \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 45864a72..8c880051 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -139,6 +139,8 @@ %1$s read File path: %1$s Update %1$s + Preset %1$s + Selected «%1$s» Cancel @@ -220,18 +222,22 @@ Line height Paragraph height Paragraph indentation + Color preset Background color Font color Check for updates Check for newer app version on startup Side padding - Double click translation - Double click on paragraph to translate it + Double click translation + Double click on paragraph to translate it + Fast color preset change + Swipe left or right to change color preset - + Red Green Blue + Name a preset… Disabled @@ -500,10 +506,14 @@ Reset start guide Exit editing mode Open in web + Delete color preset + Create new color preset + Shuffle colors X Reddit Tryzub GitHub Project GitHub Profile + Drag \ No newline at end of file