0.9.6 - UI(New themes appearance, Up to date Material3 components, General UI improvements, New color roles) + Fixed terrible scrolling lag in LibraryScreen + Loading async cover image animation + Tooltips for each IconButton + ThemeContrast option(changes contrast of the theme)

This commit is contained in:
acclorite 2024-03-26 17:40:18 +02:00
parent 1689e9d7b9
commit 76e1ca3e3f
108 changed files with 5205 additions and 1851 deletions

View file

@ -15,7 +15,7 @@ android {
minSdk = 26 minSdk = 26
targetSdk = 34 targetSdk = 34
versionCode = 1 versionCode = 1
versionName = "0.9.3" versionName = "0.9.6"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
@ -66,13 +66,19 @@ dependencies {
implementation("androidx.activity:activity-compose:1.8.2") implementation("androidx.activity:activity-compose:1.8.2")
// Compose BOM libraries // Compose BOM libraries
implementation(platform("androidx.compose:compose-bom:2024.01.00")) implementation(platform("androidx.compose:compose-bom:2024.03.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.animation:animation")
implementation("androidx.compose.animation:animation-core")
implementation("androidx.compose.animation:animation-graphics")
implementation("androidx.compose.foundation:foundation-layout")
implementation("androidx.compose.ui:ui") implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics") implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.ui:ui-android") implementation("androidx.compose.ui:ui-android")
implementation("androidx.compose.material3:material3") implementation("androidx.compose.material3:material3:latest.release")
implementation("androidx.compose.material3:material3-window-size-class") implementation("androidx.compose.material3:material3-window-size-class")
implementation("androidx.compose.material:material-icons-extended")
implementation("androidx.compose.material:material") implementation("androidx.compose.material:material")
// Unit tests // Unit tests
@ -84,7 +90,7 @@ dependencies {
testImplementation("androidx.test.ext:truth:1.5.0") testImplementation("androidx.test.ext:truth:1.5.0")
testImplementation("com.squareup.okhttp3:mockwebserver:latest.release") testImplementation("com.squareup.okhttp3:mockwebserver:latest.release")
testImplementation("io.mockk:mockk:latest.release") testImplementation("io.mockk:mockk:latest.release")
debugImplementation("androidx.compose.ui:ui-test-manifest:1.6.3") debugImplementation("androidx.compose.ui:ui-test-manifest:1.6.4")
// Instrumentation tests // Instrumentation tests
androidTestImplementation("com.google.dagger:hilt-android-testing:latest.release") androidTestImplementation("com.google.dagger:hilt-android-testing:latest.release")
@ -100,7 +106,6 @@ dependencies {
// All dependencies // All dependencies
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0") implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
implementation("androidx.compose.material:material-icons-extended")
implementation("com.google.accompanist:accompanist-swiperefresh:0.24.2-alpha") implementation("com.google.accompanist:accompanist-swiperefresh:0.24.2-alpha")
// Dagger - Hilt // Dagger - Hilt

View file

@ -38,6 +38,7 @@ import ua.acclorite.book_story.presentation.screens.book_info.BookInfoScreen
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
import ua.acclorite.book_story.presentation.screens.browse.BrowseScreen import ua.acclorite.book_story.presentation.screens.browse.BrowseScreen
import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
import ua.acclorite.book_story.presentation.screens.help.HelpScreen
import ua.acclorite.book_story.presentation.screens.history.HistoryScreen import ua.acclorite.book_story.presentation.screens.history.HistoryScreen
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.LibraryScreen import ua.acclorite.book_story.presentation.screens.library.LibraryScreen
@ -111,7 +112,8 @@ class Activity : AppCompatActivity() {
if (isLoaded) { if (isLoaded) {
BooksHistoryResurrectionTheme( BooksHistoryResurrectionTheme(
theme = state.theme!!, theme = state.theme!!,
isDark = state.darkTheme!!.isDark() isDark = state.darkTheme!!.isDark(),
themeContrast = state.themeContrast!!
) { ) {
NavigationHost(startScreen = Screen.LIBRARY) { NavigationHost(startScreen = Screen.LIBRARY) {
val currentScreen by this.getCurrentScreen().collectAsState() val currentScreen by this.getCurrentScreen().collectAsState()
@ -269,6 +271,14 @@ class Activity : AppCompatActivity() {
) { ) {
AboutScreen(navigator = this@NavigationHost) AboutScreen(navigator = this@NavigationHost)
} }
// Help screen
composable(
screen = Screen.HELP,
enterAnim = Transitions.SlidingTransitionIn,
exitAnim = Transitions.SlidingTransitionOut
) {
HelpScreen(navigator = this@NavigationHost)
}
// Start screen (later) // Start screen (later)
// composable(screen = Screen.START) { // composable(screen = Screen.START) {

View file

@ -6,11 +6,11 @@ import ua.acclorite.book_story.domain.model.Category
@Entity @Entity
data class BookEntity( data class BookEntity(
@PrimaryKey(true) val id: Int? = null, @PrimaryKey(true) val id: Int = 0,
val title: String, val title: String,
val author: String?, val author: String?,
val description: String?, val description: String?,
val text: String, val textPath: String,
val filePath: String, val filePath: String,
val scrollIndex: Int, val scrollIndex: Int,
val scrollOffset: Int, val scrollOffset: Int,

View file

@ -6,7 +6,7 @@ import androidx.room.PrimaryKey
@Entity @Entity
data class HistoryEntity( data class HistoryEntity(
@PrimaryKey(true) @PrimaryKey(true)
val id: Int? = null, val id: Int = 0,
val bookId: Int, val bookId: Int,
val time: Long val time: Long
) )

View file

@ -10,10 +10,6 @@ import javax.inject.Inject
class BookMapperImpl @Inject constructor() : BookMapper { class BookMapperImpl @Inject constructor() : BookMapper {
override suspend fun toBookEntity(book: Book): BookEntity { override suspend fun toBookEntity(book: Book): BookEntity {
val textAsString = book.text.joinToString("\n") {
it.line.trim()
}
return BookEntity( return BookEntity(
id = book.id, id = book.id,
title = book.title, title = book.title,
@ -22,7 +18,7 @@ class BookMapperImpl @Inject constructor() : BookMapper {
scrollOffset = book.scrollOffset, scrollOffset = book.scrollOffset,
progress = book.progress, progress = book.progress,
author = book.author.string, author = book.author.string,
text = textAsString, textPath = book.textPath,
description = book.description, description = book.description,
image = if (book.coverImage != null) book.coverImage.toString() else null, image = if (book.coverImage != null) book.coverImage.toString() else null,
category = book.category category = book.category
@ -43,6 +39,7 @@ class BookMapperImpl @Inject constructor() : BookMapper {
scrollOffset = bookEntity.scrollOffset, scrollOffset = bookEntity.scrollOffset,
progress = bookEntity.progress, progress = bookEntity.progress,
file = if (file.exists()) file else null, file = if (file.exists()) file else null,
textPath = bookEntity.textPath,
text = emptyList(), text = emptyList(),
letters = 0, letters = 0,
words = 0, words = 0,

View file

@ -45,10 +45,10 @@ class EpubFileParser @Inject constructor() : FileParser {
} }
return Book( return Book(
id = null,
title = title, title = title,
author = author, author = author,
description = description?.toString(), description = description?.toString(),
textPath = "",
text = emptyList(), text = emptyList(),
letters = 0, letters = 0,
words = 0, words = 0,

View file

@ -33,10 +33,10 @@ class PdfFileParser @Inject constructor(private val application: Application) :
document.close() document.close()
return Book( return Book(
id = null,
title = title, title = title,
author = author, author = author,
description = description, description = description,
textPath = "",
text = emptyList(), text = emptyList(),
letters = 0, letters = 0,
words = 0, words = 0,

View file

@ -21,10 +21,10 @@ class TxtFileParser @Inject constructor() : FileParser {
val author = UIText.StringResource(R.string.unknown_author) val author = UIText.StringResource(R.string.unknown_author)
return Book( return Book(
id = null,
title = title, title = title,
author = author, author = author,
description = null, description = null,
textPath = "",
text = emptyList(), text = emptyList(),
letters = 0, letters = 0,
words = 0, words = 0,

View file

@ -4,6 +4,7 @@ import android.app.Application
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.os.Environment import android.os.Environment
import android.util.Log
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -46,35 +47,13 @@ class BookRepositoryImpl @Inject constructor(
) : BookRepository { ) : BookRepository {
override suspend fun getBooks(query: String): Flow<Resource<List<Book>>> { override suspend fun getBooks(query: String): List<Book> {
return flow {
emit(Resource.Loading(true))
val books = database.searchBooks(query)
emit(
Resource.Success(
data = books.map { entity ->
val book = bookMapper.toBook(entity)
val lastHistory = database.getLatestHistoryForBook(
book.id ?: -1
)
book.copy(
lastOpened = lastHistory?.time
)
}
)
)
}
}
override suspend fun fastGetBooks(query: String): List<Book> {
val books = database.searchBooks(query) val books = database.searchBooks(query)
return books.map { entity -> return books.map { entity ->
val book = bookMapper.toBook(entity) val book = bookMapper.toBook(entity)
val lastHistory = database.getLatestHistoryForBook( val lastHistory = database.getLatestHistoryForBook(
book.id ?: -1 book.id
) )
book.copy( book.copy(
@ -89,7 +68,7 @@ class BookRepositoryImpl @Inject constructor(
return books.map { entity -> return books.map { entity ->
val book = bookMapper.toBook(entity) val book = bookMapper.toBook(entity)
val lastHistory = database.getLatestHistoryForBook( val lastHistory = database.getLatestHistoryForBook(
book.id ?: -1 book.id
) )
book.copy( book.copy(
@ -98,35 +77,73 @@ class BookRepositoryImpl @Inject constructor(
} }
} }
override suspend fun getBookTextById(bookId: Int): String { override suspend fun getBookTextById(textPath: String): List<StringWithId> {
val book = database.findBookById(bookId) val textFile = File(textPath)
return book.text
if (textPath.isBlank() || !textFile.exists()) {
Log.w("BOOK TEXT", "Failed to load file")
return emptyList()
}
val text = textParser.parse(textFile)
if (text.data.isNullOrEmpty()) {
return emptyList()
}
return text.data
} }
override suspend fun insertBooks( override suspend fun insertBooks(
books: List<Pair<Book, CoverImage?>> books: List<Pair<Book, CoverImage?>>
) { ): Boolean {
val filesDir = application.filesDir val filesDir = application.filesDir
val coversDir = File(filesDir, "covers") val coversDir = File(filesDir, "covers")
val booksDir = File(filesDir, "books")
if (!coversDir.exists()) { if (!coversDir.exists()) {
coversDir.mkdirs() coversDir.mkdirs()
} }
if (!booksDir.exists()) {
booksDir.mkdirs()
}
val booksWithCover = books.map { val booksWithCoverAndText = books.map {
var uri = "" var coverUri = ""
val textUri: String
if (it.first.text.isEmpty()) {
return false
}
try {
textUri = "${UUID.randomUUID()}.txt"
val text = File(booksDir, textUri)
withContext(Dispatchers.IO) {
FileOutputStream(text).use { stream ->
it.first.text.forEach { line ->
stream.write(line.line.toByteArray())
stream.write(System.lineSeparator().toByteArray())
}
}
}
} catch (e: Exception) {
e.printStackTrace()
return false
}
if (it.second != null) { if (it.second != null) {
try { try {
uri = "${UUID.randomUUID()}.webp" coverUri = "${UUID.randomUUID()}.webp"
val cover = File(coversDir, uri) val cover = File(coversDir, coverUri)
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
FileOutputStream(cover).use { stream -> FileOutputStream(cover).use { stream ->
if ( if (
!it.second!!.compress( !it.second!!.copy(Bitmap.Config.RGB_565, false).compress(
Bitmap.CompressFormat.WEBP, Bitmap.CompressFormat.WEBP,
30, 20,
stream stream
) )
) { ) {
@ -135,33 +152,34 @@ class BookRepositoryImpl @Inject constructor(
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
uri = "" coverUri = ""
e.printStackTrace() e.printStackTrace()
} }
} }
val book = it.first.copy( val book = it.first.copy(
coverImage = if (uri.isNotBlank()) { textPath = "$booksDir/$textUri",
Uri.fromFile(File("$coversDir/$uri")) coverImage = if (coverUri.isNotBlank()) {
Uri.fromFile(File("$coversDir/$coverUri"))
} else null } else null
) )
bookMapper.toBookEntity(book) bookMapper.toBookEntity(book)
} }
database.insertBooks(booksWithCover) database.insertBooks(booksWithCoverAndText)
return true
} }
override suspend fun updateBooks(books: List<Book>) { override suspend fun updateBooks(books: List<Book>) {
// without text and cover image // without text and cover image
database.updateBooks( database.updateBooks(
books.map { books.map {
val book = database.findBookById(it.id ?: return) val book = database.findBookById(it.id)
bookMapper.toBookEntity( bookMapper.toBookEntity(
it.copy( it.copy(
text = book.text textPath = book.textPath,
.split("\n")
.map { line -> StringWithId(line.trim()) },
coverImage = if (book.image != null) Uri.parse(book.image) else null coverImage = if (book.image != null) Uri.parse(book.image) else null
) )
) )
@ -169,18 +187,66 @@ class BookRepositoryImpl @Inject constructor(
) )
} }
override suspend fun updateBooksWithText(books: List<Book>) { override suspend fun updateBooksWithText(books: List<Book>): Boolean {
// without cover image // without cover image
database.updateBooks( val filesDir = application.filesDir
books.map { val booksDir = File(filesDir, "books")
val book = database.findBookById(it.id ?: return)
if (!booksDir.exists()) {
booksDir.mkdirs()
}
val booksWithText = books.map {
val textUri: String
val bookEntity = database.findBookById(it.id)
if (it.text.isEmpty()) {
return false
}
try {
textUri = "${UUID.randomUUID()}.txt"
val text = File(booksDir, textUri)
withContext(Dispatchers.IO) {
FileOutputStream(text).use { stream ->
it.text.forEach { line ->
stream.write(line.line.toByteArray())
stream.write(System.lineSeparator().toByteArray())
}
}
}
} catch (e: Exception) {
e.printStackTrace()
return false
}
if (it.textPath.isNotBlank()) {
try {
val fileToDelete = File(
it.textPath
)
if (fileToDelete.exists()) {
fileToDelete.delete()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
bookMapper.toBookEntity( bookMapper.toBookEntity(
it.copy( it.copy(
coverImage = if (book.image != null) Uri.parse(book.image) else null textPath = "$booksDir/$textUri",
coverImage = if (bookEntity.image != null) Uri.parse(bookEntity.image) else null
) )
) )
} }
database.updateBooks(
booksWithText
) )
return true
} }
override suspend fun updateCoverImageOfBook( override suspend fun updateCoverImageOfBook(
@ -188,7 +254,7 @@ class BookRepositoryImpl @Inject constructor(
newCoverImage: CoverImage? newCoverImage: CoverImage?
) { ) {
// without text // without text
val book = database.findBookById(bookWithOldCover.id ?: return) val book = database.findBookById(bookWithOldCover.id)
var uri: String? = null var uri: String? = null
val filesDir = application.filesDir val filesDir = application.filesDir
@ -206,9 +272,9 @@ class BookRepositoryImpl @Inject constructor(
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
FileOutputStream(cover).use { stream -> FileOutputStream(cover).use { stream ->
if ( if (
!newCoverImage.compress( !newCoverImage.copy(Bitmap.Config.RGB_565, false).compress(
Bitmap.CompressFormat.WEBP, Bitmap.CompressFormat.WEBP,
30, 20,
stream stream
) )
) { ) {
@ -243,9 +309,7 @@ class BookRepositoryImpl @Inject constructor(
} }
val bookWithNewCover = bookWithOldCover.copy( val bookWithNewCover = bookWithOldCover.copy(
text = book.text textPath = book.textPath,
.split("\n")
.map { line -> StringWithId(line.trim()) },
coverImage = newCoverImageUri coverImage = newCoverImageUri
) )
@ -261,14 +325,18 @@ class BookRepositoryImpl @Inject constructor(
override suspend fun deleteBooks(books: List<Book>) { override suspend fun deleteBooks(books: List<Book>) {
val filesDir = application.filesDir val filesDir = application.filesDir
val coversDir = File(filesDir, "covers") val coversDir = File(filesDir, "covers")
val booksDir = File(filesDir, "books")
if (!coversDir.exists()) { if (!coversDir.exists()) {
coversDir.mkdirs() coversDir.mkdirs()
} }
if (!booksDir.exists()) {
booksDir.mkdirs()
}
database.deleteBooks( database.deleteBooks(
books.map { books.map {
val book = database.findBookById(it.id ?: return) val book = database.findBookById(it.id)
if (book.image != null) { if (book.image != null) {
try { try {
@ -284,6 +352,18 @@ class BookRepositoryImpl @Inject constructor(
} }
} }
if (book.textPath.isNotBlank()) {
try {
val fileToDelete = File(book.textPath)
if (fileToDelete.exists()) {
fileToDelete.delete()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
book book
} }
) )
@ -424,7 +504,7 @@ class BookRepositoryImpl @Inject constructor(
books.add( books.add(
NullableBook.Null( NullableBook.Null(
file.name, file.name,
UIText.StringResource(R.string.error_something_went_wrong) UIText.StringResource(R.string.error_something_went_wrong_with_file)
) )
) )
continue continue
@ -469,7 +549,6 @@ class BookRepositoryImpl @Inject constructor(
override suspend fun getHistory(): Flow<Resource<List<History>>> { override suspend fun getHistory(): Flow<Resource<List<History>>> {
return flow { return flow {
emit(Resource.Loading(true))
val history = database.getHistory() val history = database.getHistory()
emit( emit(

View file

@ -7,13 +7,13 @@ import java.io.File
@Immutable @Immutable
data class Book( data class Book(
val id: Int?, val id: Int = 0,
val title: String, val title: String,
val author: UIText, val author: UIText,
val description: String?, val description: String?,
// val textPath: String, val textPath: String,
val text: List<StringWithId> = emptyList(), val text: List<StringWithId> = emptyList(),
val letters: Int, val letters: Int,
val words: Int, val words: Int,

View file

@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
@Immutable @Immutable
data class ChipItem( data class ButtonItem(
val id: String, val id: String,
val title: String, val title: String,
val textStyle: TextStyle, val textStyle: TextStyle,

View file

@ -1,8 +1,15 @@
package ua.acclorite.book_story.domain.model package ua.acclorite.book_story.domain.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.util.Selected
@Immutable @Immutable
enum class Category { enum class Category {
READING, ALREADY_READ, PLANNING, DROPPED READING, ALREADY_READ, PLANNING, DROPPED
} }
@Immutable
data class CategorizedBooks(
val category: Category,
val books: List<Pair<Book, Selected>>
)

View file

@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
@Immutable @Immutable
data class History( data class History(
val id: Int?, val id: Int = 0,
val bookId: Int, val bookId: Int,
val book: Book?, val book: Book?,
val time: Long val time: Long

View file

@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.Flow
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.domain.util.CoverImage import ua.acclorite.book_story.domain.util.CoverImage
import ua.acclorite.book_story.domain.util.Resource import ua.acclorite.book_story.domain.util.Resource
import java.io.File import java.io.File
@ -13,10 +14,6 @@ interface BookRepository {
suspend fun getBooks( suspend fun getBooks(
query: String query: String
): Flow<Resource<List<Book>>>
suspend fun fastGetBooks(
query: String
): List<Book> ): List<Book>
suspend fun getBooksById( suspend fun getBooksById(
@ -24,12 +21,12 @@ interface BookRepository {
): List<Book> ): List<Book>
suspend fun getBookTextById( suspend fun getBookTextById(
bookId: Int textPath: String
): String ): List<StringWithId>
suspend fun insertBooks( suspend fun insertBooks(
books: List<Pair<Book, CoverImage?>> books: List<Pair<Book, CoverImage?>>
) ): Boolean
suspend fun updateBooks( suspend fun updateBooks(
books: List<Book> books: List<Book>
@ -37,7 +34,7 @@ interface BookRepository {
suspend fun updateBooksWithText( suspend fun updateBooksWithText(
books: List<Book> books: List<Book>
) ): Boolean
suspend fun updateCoverImageOfBook( suspend fun updateCoverImageOfBook(
bookWithOldCover: Book, bookWithOldCover: Book,

View file

@ -9,9 +9,7 @@ class DeleteBooks @Inject constructor(private val repository: BookRepository) {
suspend fun execute(books: List<Book>) { suspend fun execute(books: List<Book>) {
repository.deleteBooks(books) repository.deleteBooks(books)
books.forEach { books.forEach {
if (it.id != null) {
repository.deleteBookHistory(it.id) repository.deleteBookHistory(it.id)
} }
} }
}
} }

View file

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

View file

@ -1,14 +1,12 @@
package ua.acclorite.book_story.domain.use_case package ua.acclorite.book_story.domain.use_case
import kotlinx.coroutines.flow.Flow
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.domain.util.Resource
import javax.inject.Inject import javax.inject.Inject
class GetBooks @Inject constructor(private val repository: BookRepository) { class GetBooks @Inject constructor(private val repository: BookRepository) {
suspend fun execute(query: String): Flow<Resource<List<Book>>> { suspend fun execute(query: String): List<Book> {
return repository.getBooks(query) return repository.getBooks(query)
} }
} }

View file

@ -1,11 +1,12 @@
package ua.acclorite.book_story.domain.use_case package ua.acclorite.book_story.domain.use_case
import ua.acclorite.book_story.domain.model.StringWithId
import ua.acclorite.book_story.domain.repository.BookRepository import ua.acclorite.book_story.domain.repository.BookRepository
import javax.inject.Inject import javax.inject.Inject
class GetText @Inject constructor(private val repository: BookRepository) { class GetText @Inject constructor(private val repository: BookRepository) {
suspend fun execute(id: Int): String { suspend fun execute(textPath: String): List<StringWithId> {
return repository.getBookTextById(bookId = id) return repository.getBookTextById(textPath = textPath)
} }
} }

View file

@ -6,7 +6,7 @@ import ua.acclorite.book_story.domain.util.CoverImage
import javax.inject.Inject import javax.inject.Inject
class InsertBooks @Inject constructor(private val repository: BookRepository) { class InsertBooks @Inject constructor(private val repository: BookRepository) {
suspend fun execute(books: List<Pair<Book, CoverImage?>>) { suspend fun execute(books: List<Pair<Book, CoverImage?>>): Boolean {
repository.insertBooks(books) return repository.insertBooks(books)
} }
} }

View file

@ -6,7 +6,7 @@ import javax.inject.Inject
class UpdateBooksWithText @Inject constructor(private val repository: BookRepository) { class UpdateBooksWithText @Inject constructor(private val repository: BookRepository) {
suspend fun execute(books: List<Book>) { suspend fun execute(books: List<Book>): Boolean {
repository.updateBooksWithText(books) return repository.updateBooksWithText(books)
} }
} }

View file

@ -170,6 +170,7 @@ object Constants {
"", "",
UIText.StringValue(""), UIText.StringValue(""),
null, null,
"",
emptyList(), emptyList(),
0, 0,
0, 0,

View file

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

View file

@ -3,5 +3,4 @@ package ua.acclorite.book_story.domain.util
sealed class Resource<T>(val data: T? = null, val message: UIText? = null) { sealed class Resource<T>(val data: T? = null, val message: UIText? = null) {
class Success<T>(data: T?) : Resource<T>(data) class Success<T>(data: T?) : Resource<T>(data)
class Error<T>(message: UIText, data: T? = null) : Resource<T>(data, message) class Error<T>(message: UIText, data: T? = null) : Resource<T>(data, message)
class Loading<T>(val isLoading: Boolean = true) : Resource<T>()
} }

View file

@ -28,7 +28,6 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.ui.elevation
/** /**
* Animated top app bar. Has up to 3 different top bars inside it. Follow [TopAppBar] for more info. * Animated top app bar. Has up to 3 different top bars inside it. Follow [TopAppBar] for more info.
@ -42,7 +41,7 @@ import ua.acclorite.book_story.presentation.ui.elevation
@Composable @Composable
fun AnimatedTopAppBar( fun AnimatedTopAppBar(
containerColor: Color = MaterialTheme.colorScheme.surface, containerColor: Color = MaterialTheme.colorScheme.surface,
scrolledContainerColor: Color = MaterialTheme.elevation(), scrolledContainerColor: Color = MaterialTheme.colorScheme.surfaceContainer,
scrollBehavior: TopAppBarScrollBehavior?, scrollBehavior: TopAppBarScrollBehavior?,
isTopBarScrolled: Boolean?, isTopBarScrolled: Boolean?,

View file

@ -17,16 +17,15 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.ui.elevation
/** /**
* Custom Checkbox. Has a Circle shape. * Custom Checkbox. Has a Circle shape.
*/ */
@Composable @Composable
fun CustomCheckbox(selected: Boolean, size: Dp = 22.dp) { fun CustomCheckbox(selected: Boolean, containerColor: Color, size: Dp = 22.dp) {
Icon( Icon(
imageVector = Icons.Default.Check, imageVector = Icons.Default.Check,
tint = if (selected) MaterialTheme.elevation(elevation = 2.dp) else Color.Transparent, tint = if (selected) containerColor else Color.Transparent,
modifier = Modifier modifier = Modifier
.clip(CircleShape) .clip(CircleShape)
.border( .border(

View file

@ -0,0 +1,31 @@
package ua.acclorite.book_story.presentation.components
import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import coil.compose.AsyncImage
import coil.request.ImageRequest
import ua.acclorite.book_story.R
@Composable
fun CustomCoverImage(
uri: Uri,
animationDurationMillis: Int = 100,
contentDescription: String? = stringResource(id = R.string.cover_image_content_desc),
modifier: Modifier,
alpha: Float = 1f,
) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(uri)
.crossfade(animationDurationMillis)
.build(),
contentDescription = contentDescription,
modifier = modifier,
alpha = alpha,
contentScale = ContentScale.Crop
)
}

View file

@ -1,5 +1,6 @@
package ua.acclorite.book_story.presentation.components package ua.acclorite.book_story.presentation.components
import androidx.annotation.StringRes
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
@ -11,6 +12,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
/** /**
* Custom Icon Button. Can be disabled after click, so user can't press button fast 2 or 3 times. * Custom Icon Button. Can be disabled after click, so user can't press button fast 2 or 3 times.
@ -19,13 +21,14 @@ import androidx.compose.ui.graphics.vector.ImageVector
fun CustomIconButton( fun CustomIconButton(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
icon: ImageVector, icon: ImageVector,
contentDescription: String, @StringRes contentDescription: Int,
disableOnClick: Boolean, disableOnClick: Boolean,
enabled: Boolean = true, enabled: Boolean = true,
color: Color = LocalContentColor.current, color: Color = LocalContentColor.current,
onClick: () -> Unit onClick: () -> Unit
) { ) {
var isClicked by remember { mutableStateOf(false) } var isClicked by remember { mutableStateOf(false) }
CustomTooltip(text = stringResource(id = contentDescription)) {
IconButton( IconButton(
enabled = enabled && !isClicked, enabled = enabled && !isClicked,
onClick = { onClick = {
@ -38,8 +41,9 @@ fun CustomIconButton(
Icon( Icon(
imageVector = icon, imageVector = icon,
modifier = modifier, modifier = modifier,
contentDescription = contentDescription, contentDescription = stringResource(id = contentDescription),
tint = color tint = color
) )
} }
}
} }

View file

@ -20,6 +20,7 @@ import androidx.compose.material3.Snackbar
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -27,6 +28,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -44,10 +46,18 @@ fun CustomSnackbar(modifier: Modifier = Modifier, snackbarState: SnackbarHostSta
var snackbarData by remember { mutableStateOf(snackbarState.currentSnackbarData) } var snackbarData by remember { mutableStateOf(snackbarState.currentSnackbarData) }
var offset by remember { mutableFloatStateOf(0f) } var offset by remember { mutableFloatStateOf(0f) }
val density = LocalDensity.current val density = LocalDensity.current
val mutableInteractionSource = remember { MutableInteractionSource() } val mutableInteractionSource = remember { MutableInteractionSource() }
val isDragged by mutableInteractionSource.collectIsDraggedAsState() val isDragged by mutableInteractionSource.collectIsDraggedAsState()
val alpha by remember {
derivedStateOf {
1f - ((offset / 2f) / 100f)
}
}
LaunchedEffect(offset, isDragged) { LaunchedEffect(offset, isDragged) {
val offsetDp = with(density) { offset.toDp() } val offsetDp = with(density) { offset.toDp() }
if (offsetDp > 35.dp && !isDragged) { if (offsetDp > 35.dp && !isDragged) {
@ -74,8 +84,8 @@ fun CustomSnackbar(modifier: Modifier = Modifier, snackbarState: SnackbarHostSta
AnimatedVisibility( AnimatedVisibility(
visible = showSnackbar, visible = showSnackbar,
enter = slideInVertically(tween(250)) { it } + fadeIn(tween(250)), enter = slideInVertically(tween(350)) { it } + fadeIn(tween(350)),
exit = slideOutVertically(tween(100)) { it } + fadeOut(tween(100)) exit = slideOutVertically(tween(200)) { it } + fadeOut(tween(100))
) { ) {
Box( Box(
modifier = modifier modifier = modifier
@ -91,7 +101,9 @@ fun CustomSnackbar(modifier: Modifier = Modifier, snackbarState: SnackbarHostSta
) )
) { ) {
Snackbar( Snackbar(
modifier = Modifier.align(Alignment.Center), modifier = Modifier
.align(Alignment.Center)
.alpha(alpha),
snackbarData = snackbarData!!, snackbarData = snackbarData!!,
shape = MaterialTheme.shapes.medium shape = MaterialTheme.shapes.medium
) )

View file

@ -0,0 +1,26 @@
package ua.acclorite.book_story.presentation.components
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Text
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CustomTooltip(text: String, content: @Composable () -> Unit) {
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(12.dp),
tooltip = {
PlainTooltip {
Text(text = text)
}
},
state = rememberTooltipState()
) {
content()
}
}

View file

@ -3,7 +3,6 @@ package ua.acclorite.book_story.presentation.components
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
@ -14,7 +13,7 @@ import ua.acclorite.book_story.presentation.data.Navigator
fun GoBackButton(navigator: Navigator, enabled: Boolean = true, customOnClick: () -> Unit = {}) { fun GoBackButton(navigator: Navigator, enabled: Boolean = true, customOnClick: () -> Unit = {}) {
CustomIconButton( CustomIconButton(
icon = Icons.AutoMirrored.Filled.ArrowBack, icon = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.go_back_content_desc), contentDescription = R.string.go_back_content_desc,
disableOnClick = true, disableOnClick = true,
enabled = enabled enabled = enabled
) { ) {

View file

@ -33,7 +33,7 @@ fun MoreDropDown(navigator: Navigator) {
Box { Box {
CustomIconButton( CustomIconButton(
icon = Icons.Default.MoreVert, icon = Icons.Default.MoreVert,
contentDescription = stringResource(id = R.string.show_dropdown_content_desc), contentDescription = R.string.show_dropdown_content_desc,
disableOnClick = false, disableOnClick = false,
enabled = !showDropDown enabled = !showDropDown
) { ) {
@ -43,7 +43,7 @@ fun MoreDropDown(navigator: Navigator) {
DropdownMenu( DropdownMenu(
expanded = showDropDown, expanded = showDropDown,
onDismissRequest = { showDropDown = false }, onDismissRequest = { showDropDown = false },
offset = DpOffset(10.dp, 0.dp) offset = DpOffset((-15).dp, 0.dp)
) { ) {
DropdownMenuItem( DropdownMenuItem(
text = { text = {
@ -55,7 +55,6 @@ fun MoreDropDown(navigator: Navigator) {
}, },
onClick = { onClick = {
navigator.navigate(Screen.SETTINGS, false) navigator.navigate(Screen.SETTINGS, false)
showDropDown = false
}, },
contentPadding = PaddingValues(start = startPadding, end = endPadding) contentPadding = PaddingValues(start = startPadding, end = endPadding)
) )
@ -69,7 +68,6 @@ fun MoreDropDown(navigator: Navigator) {
}, },
onClick = { onClick = {
navigator.navigate(Screen.HELP, false) navigator.navigate(Screen.HELP, false)
showDropDown = false
}, },
contentPadding = PaddingValues(start = startPadding, end = endPadding) contentPadding = PaddingValues(start = startPadding, end = endPadding)
) )
@ -83,7 +81,6 @@ fun MoreDropDown(navigator: Navigator) {
}, },
onClick = { onClick = {
navigator.navigate(Screen.ABOUT, false) navigator.navigate(Screen.ABOUT, false)
showDropDown = false
}, },
contentPadding = PaddingValues(start = startPadding, end = endPadding) contentPadding = PaddingValues(start = startPadding, end = endPadding)
) )

View file

@ -1,6 +1,5 @@
package ua.acclorite.book_story.presentation.components.bottom_navigation_bar package ua.acclorite.book_story.presentation.components.bottom_navigation_bar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -14,7 +13,6 @@ import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.NavigationItem import ua.acclorite.book_story.domain.model.NavigationItem
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.ui.elevation
/** /**
* Bottom navigation bar, uses default [NavigationBar]. * Bottom navigation bar, uses default [NavigationBar].
@ -35,9 +33,7 @@ fun BottomNavigationBar(
} }
} }
NavigationBar( NavigationBar {
containerColor = MaterialTheme.elevation()
) {
BottomNavigationBarItem( BottomNavigationBarItem(
item = NavigationItem( item = NavigationItem(
stringResource(id = R.string.library_screen), stringResource(id = R.string.library_screen),

View file

@ -3,9 +3,7 @@ package ua.acclorite.book_story.presentation.components.bottom_navigation_bar
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -33,27 +31,19 @@ fun RowScope.BottomNavigationBarItem(
label = { label = {
Text( Text(
text = item.title, text = item.title,
style = MaterialTheme.typography.labelMedium,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
}, },
selected = isSelected, selected = isSelected,
enabled = !isSelected,
onClick = { onClick() }, onClick = { onClick() },
icon = { icon = {
Icon( Icon(
painter = icon, painter = icon,
contentDescription = item.title, contentDescription = null,
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp)
) )
}, },
modifier = modifier, modifier = modifier
colors = NavigationBarItemDefaults.colors(
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
disabledIconColor = MaterialTheme.colorScheme.onSurface,
disabledTextColor = MaterialTheme.colorScheme.onSurface
)
) )
} }

View file

@ -12,9 +12,9 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material3.AlertDialog import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -36,8 +36,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider import androidx.compose.ui.window.DialogWindowProvider
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.ui.ElevationDefaults
import ua.acclorite.book_story.presentation.ui.elevation
/** /**
* Custom Dialog with custom content. * Custom Dialog with custom content.
@ -72,7 +70,8 @@ fun CustomDialogWithContent(
customContent: @Composable (ColumnScope.() -> Unit) = {} customContent: @Composable (ColumnScope.() -> Unit) = {}
) { ) {
var actionClicked by remember { mutableStateOf(false) } var actionClicked by remember { mutableStateOf(false) }
AlertDialog(
BasicAlertDialog(
onDismissRequest = { onDismiss() }, onDismissRequest = { onDismiss() },
properties = DialogProperties( properties = DialogProperties(
dismissOnBackPress = !actionClicked, dismissOnBackPress = !actionClicked,
@ -88,7 +87,7 @@ fun CustomDialogWithContent(
.navigationBarsPadding() .navigationBarsPadding()
.statusBarsPadding() .statusBarsPadding()
.clip(MaterialTheme.shapes.extraLarge) .clip(MaterialTheme.shapes.extraLarge)
.background(MaterialTheme.elevation(ElevationDefaults.Dialog)) .background(MaterialTheme.colorScheme.surfaceContainerHigh)
.padding(top = 24.dp, bottom = 12.dp) .padding(top = 24.dp, bottom = 12.dp)
) { ) {
if (drawableIcon != null) { if (drawableIcon != null) {
@ -140,7 +139,7 @@ fun CustomDialogWithContent(
} }
if (withDivider) { if (withDivider) {
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
Divider( HorizontalDivider(
modifier = Modifier.padding(horizontal = 24.dp), modifier = Modifier.padding(horizontal = 24.dp),
color = MaterialTheme.colorScheme.outlineVariant color = MaterialTheme.colorScheme.outlineVariant
) )

View file

@ -13,9 +13,9 @@ import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.AlertDialog import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -37,8 +37,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.compose.ui.window.DialogWindowProvider import androidx.compose.ui.window.DialogWindowProvider
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.ui.ElevationDefaults
import ua.acclorite.book_story.presentation.ui.elevation
/** /**
@ -74,7 +72,7 @@ fun CustomDialogWithLazyColumn(
items: (LazyListScope.() -> Unit) = {} items: (LazyListScope.() -> Unit) = {}
) { ) {
var actionClicked by remember { mutableStateOf(false) } var actionClicked by remember { mutableStateOf(false) }
AlertDialog( BasicAlertDialog(
onDismissRequest = { onDismiss() }, onDismissRequest = { onDismiss() },
properties = DialogProperties( properties = DialogProperties(
dismissOnBackPress = !actionClicked, dismissOnBackPress = !actionClicked,
@ -90,7 +88,7 @@ fun CustomDialogWithLazyColumn(
.navigationBarsPadding() .navigationBarsPadding()
.statusBarsPadding() .statusBarsPadding()
.clip(MaterialTheme.shapes.extraLarge) .clip(MaterialTheme.shapes.extraLarge)
.background(MaterialTheme.elevation(ElevationDefaults.Dialog)) .background(MaterialTheme.colorScheme.surfaceContainerHigh)
.padding(top = 24.dp, bottom = 12.dp) .padding(top = 24.dp, bottom = 12.dp)
) { ) {
if (drawableIcon != null) { if (drawableIcon != null) {
@ -142,7 +140,7 @@ fun CustomDialogWithLazyColumn(
} }
if (withDivider) { if (withDivider) {
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
Divider( HorizontalDivider(
modifier = Modifier.padding(horizontal = 24.dp), modifier = Modifier.padding(horizontal = 24.dp),
color = MaterialTheme.colorScheme.outlineVariant color = MaterialTheme.colorScheme.outlineVariant
) )

View file

@ -29,7 +29,6 @@ import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.NavigationItem import ua.acclorite.book_story.domain.model.NavigationItem
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.ui.elevation
/** /**
* Custom Navigation Rail. It is used to be shown on Tablets. * Custom Navigation Rail. It is used to be shown on Tablets.
@ -54,7 +53,7 @@ fun BoxScope.CustomNavigationRail(
.align( .align(
Alignment.CenterStart Alignment.CenterStart
) )
.background(MaterialTheme.elevation()) .background(MaterialTheme.colorScheme.surfaceContainer)
.padding( .padding(
start = WindowInsets.navigationBars start = WindowInsets.navigationBars
.asPaddingValues() .asPaddingValues()
@ -62,7 +61,7 @@ fun BoxScope.CustomNavigationRail(
) )
.width(80.dp) .width(80.dp)
.padding(horizontal = 12.dp), .padding(horizontal = 12.dp),
containerColor = MaterialTheme.elevation(), containerColor = MaterialTheme.colorScheme.surfaceContainer,
windowInsets = WindowInsets(0, 0, 0, 0) windowInsets = WindowInsets(0, 0, 0, 0)
) { ) {
Column( Column(

View file

@ -2,9 +2,7 @@ package ua.acclorite.book_story.presentation.components.custom_navigation_rail
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationRailItem import androidx.compose.material3.NavigationRailItem
import androidx.compose.material3.NavigationRailItemDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -32,27 +30,19 @@ fun CustomNavigationRailItem(
label = { label = {
Text( Text(
text = item.title, text = item.title,
style = MaterialTheme.typography.labelMedium,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
}, },
selected = isSelected, selected = isSelected,
enabled = !isSelected,
onClick = { onClick() }, onClick = { onClick() },
icon = { icon = {
Icon( Icon(
painter = icon, painter = icon,
contentDescription = item.title, contentDescription = null,
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp)
) )
}, },
modifier = modifier, modifier = modifier
colors = NavigationRailItemDefaults.colors(
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
disabledIconColor = MaterialTheme.colorScheme.onSurface,
disabledTextColor = MaterialTheme.colorScheme.onSurface
)
) )
} }

View file

@ -1,9 +1,5 @@
package ua.acclorite.book_story.presentation.data package ua.acclorite.book_story.presentation.data
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Path
import kotlin.math.abs
fun String.removeTrailingZero(): String { fun String.removeTrailingZero(): String {
if (!this.contains('.')) if (!this.contains('.'))
return this return this
@ -20,11 +16,3 @@ fun calculateFamiliarity(string: String, target: String): Int {
return familiarity return familiarity
} }
fun Path.standardQuadFromTo(from: Offset, to: Offset) {
quadraticTo(
from.x,
from.y,
abs(from.x + to.x) / 2f,
abs(from.y + to.y) / 2f
)
}

View file

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

View file

@ -5,6 +5,7 @@ import android.os.Parcelable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.presentation.ui.DarkTheme import ua.acclorite.book_story.presentation.ui.DarkTheme
import ua.acclorite.book_story.presentation.ui.Theme import ua.acclorite.book_story.presentation.ui.Theme
import ua.acclorite.book_story.presentation.ui.ThemeContrast
/** /**
* Main State. All app's settings are here. Wrapped in SavedStateHandle, so it won't reset. * Main State. All app's settings are here. Wrapped in SavedStateHandle, so it won't reset.
@ -14,6 +15,7 @@ data class MainState(
val language: String? = null, val language: String? = null,
val theme: Theme? = null, val theme: Theme? = null,
val darkTheme: DarkTheme? = null, val darkTheme: DarkTheme? = null,
val themeContrast: ThemeContrast? = null,
val fontFamily: String? = null, val fontFamily: String? = null,
val isItalic: Boolean? = null, val isItalic: Boolean? = null,
val fontSize: Int? = null, val fontSize: Int? = null,
@ -32,6 +34,8 @@ data class MainState(
// String // String
darkTheme = DarkTheme.valueOf(parcel.readString() ?: DarkTheme.FOLLOW_SYSTEM.name), darkTheme = DarkTheme.valueOf(parcel.readString() ?: DarkTheme.FOLLOW_SYSTEM.name),
// String // String
themeContrast = ThemeContrast.valueOf(parcel.readString() ?: ThemeContrast.STANDARD.name),
// String
fontFamily = parcel.readString(), fontFamily = parcel.readString(),
// Boolean // Boolean
isItalic = if (parcel.readByte().toInt() != 0) parcel.readByte().toInt() == 1 else null, isItalic = if (parcel.readByte().toInt() != 0) parcel.readByte().toInt() == 1 else null,
@ -60,6 +64,8 @@ data class MainState(
parcel.writeString(theme?.name) parcel.writeString(theme?.name)
// Dark Theme // Dark Theme
parcel.writeString(darkTheme?.name) parcel.writeString(darkTheme?.name)
// Contrast Level
parcel.writeString(themeContrast?.name)
// Font Family // Font Family
parcel.writeString(fontFamily) parcel.writeString(fontFamily)
// Is Italic // Is Italic

View file

@ -21,8 +21,10 @@ import ua.acclorite.book_story.domain.util.DataStoreConstants
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.ui.DarkTheme import ua.acclorite.book_story.presentation.ui.DarkTheme
import ua.acclorite.book_story.presentation.ui.Theme import ua.acclorite.book_story.presentation.ui.Theme
import ua.acclorite.book_story.presentation.ui.ThemeContrast
import ua.acclorite.book_story.presentation.ui.toDarkTheme import ua.acclorite.book_story.presentation.ui.toDarkTheme
import ua.acclorite.book_story.presentation.ui.toTheme import ua.acclorite.book_story.presentation.ui.toTheme
import ua.acclorite.book_story.presentation.ui.toThemeContrast
import java.util.Locale import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
@ -70,6 +72,17 @@ class MainViewModel @Inject constructor(
} }
} }
is MainEvent.OnChangeThemeContrast -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.THEME_CONTRAST, event.themeContrast)
_state.updateWithSavedHandle {
it.copy(
themeContrast = event.themeContrast.toThemeContrast()
)
}
}
}
is MainEvent.OnChangeTheme -> { is MainEvent.OnChangeTheme -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.THEME, event.theme) setDatastore.execute(DataStoreConstants.THEME, event.theme)
@ -200,6 +213,7 @@ class MainViewModel @Inject constructor(
return@combine value.language != null && return@combine value.language != null &&
value.theme != null && value.theme != null &&
value.darkTheme != null && value.darkTheme != null &&
value.themeContrast != null &&
value.fontFamily != null && value.fontFamily != null &&
value.isItalic != null && value.isItalic != null &&
value.fontSize != null && value.fontSize != null &&
@ -270,6 +284,16 @@ class MainViewModel @Inject constructor(
} }
} }
// Theme Contrast
viewModelScope.launch(Dispatchers.IO) {
getDatastore
.execute(DataStoreConstants.THEME_CONTRAST, ThemeContrast.STANDARD.name)
.first {
onEvent(MainEvent.OnChangeThemeContrast(it))
it.isNotBlank()
}
}
// Show Start Screen // Show Start Screen
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
getDatastore getDatastore

View file

@ -1,6 +1,7 @@
package ua.acclorite.book_story.presentation.data package ua.acclorite.book_story.presentation.data
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Parcelable
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.viewModels import androidx.activity.viewModels
@ -14,7 +15,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -29,12 +29,15 @@ import dagger.hilt.android.lifecycle.withCreationCallback
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import ua.acclorite.book_story.presentation.ui.Transitions import ua.acclorite.book_story.presentation.ui.Transitions
import java.io.Serializable
private const val CURRENT_SCREEN = "current_screen" private const val CURRENT_SCREEN = "current_screen"
private const val BACKSTACK = "back_stack" private const val BACKSTACK = "back_stack"
private const val USE_BACK_ANIM = "use_back_animation" private const val USE_BACK_ANIM = "use_back_animation"
private const val ARGUMENTS = "arguments"
/** /**
* All screens are listed here, later each screen will be passed as a param for [Navigator.composable] function. * All screens are listed here, later each screen will be passed as a param for [Navigator.composable] function.
@ -57,11 +60,15 @@ enum class Screen {
START START
} }
/**
* Navigation Argument.
*/
@Immutable @Immutable
data class Argument( @Parcelize
data class Argument<out T : Serializable>(
val key: String, val key: String,
val arg: Any? val value: T
) ) : Parcelable
/** /**
* Navigator. Using for navigation between screens. * Navigator. Using for navigation between screens.
@ -78,38 +85,39 @@ class Navigator @AssistedInject constructor(
private val currentScreen = savedStateHandle.getStateFlow(CURRENT_SCREEN, startScreen) private val currentScreen = savedStateHandle.getStateFlow(CURRENT_SCREEN, startScreen)
private val useBackAnimation = savedStateHandle.getStateFlow(USE_BACK_ANIM, false) private val useBackAnimation = savedStateHandle.getStateFlow(USE_BACK_ANIM, false)
private val backStack = savedStateHandle.getStateFlow(BACKSTACK, mutableListOf<Screen>()) private val backStack = savedStateHandle.getStateFlow(BACKSTACK, mutableListOf<Screen>())
private val arguments = mutableStateListOf<Argument>() private val arguments = savedStateHandle
.getStateFlow(ARGUMENTS, mutableListOf<Argument<Serializable>>())
fun putArgument(argument: Argument) { fun putArgument(argument: Argument<Serializable>) {
var found = false var found = false
for ((index, arg) in arguments.withIndex()) { for ((index, arg) in arguments.value.withIndex()) {
if (arg.key == argument.key) { if (arg.key == argument.key) {
arguments[index] = argument arguments.value[index] = argument
found = true found = true
break break
} }
} }
if (!found) { if (!found) {
arguments.add(argument) arguments.value.add(argument)
} }
} }
fun retrieveArgument(key: String): Any? { fun retrieveArgument(key: String): Serializable? {
for (arg in arguments) { for (arg in arguments.value) {
if (arg.key == key) { if (arg.key == key) {
return arg.arg return arg.value
} }
} }
return null return null
} }
fun clearArgument(key: String) { fun clearArgument(key: String) {
arguments.removeIf { it.key == key } arguments.value.removeIf { it.key == key }
} }
fun navigate(screen: Screen, useBackAnimation: Boolean, vararg args: Argument) = fun navigate(screen: Screen, useBackAnimation: Boolean, vararg args: Argument<Serializable>) =
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.Default) {
backStack.value.add(currentScreen.value) backStack.value.add(currentScreen.value)
@ -121,8 +129,11 @@ class Navigator @AssistedInject constructor(
savedStateHandle[CURRENT_SCREEN] = screen savedStateHandle[CURRENT_SCREEN] = screen
} }
fun navigateWithoutBackStack(screen: Screen, useBackAnimation: Boolean, vararg args: Argument) = fun navigateWithoutBackStack(
viewModelScope.launch(Dispatchers.Default) { screen: Screen,
useBackAnimation: Boolean,
vararg args: Argument<Serializable>
) = viewModelScope.launch(Dispatchers.Default) {
if (backStack.value.last() == screen) { if (backStack.value.last() == screen) {
backStack.value.removeLast() backStack.value.removeLast()
} }
@ -153,7 +164,7 @@ class Navigator @AssistedInject constructor(
} }
/** /**
* Animated Screen. Using in [NavigationHost]. Be sure to not use the same [screen] parameter twice, it'll override the highest one in your code. * Animated Screen. Used in [NavigationHost]. Be sure to not use the same [screen] parameter twice, it'll override the highest one in your code.
* *
* @param screen The [Screen] that represents [content]. * @param screen The [Screen] that represents [content].
* @param enterAnim Enter Animation. * @param enterAnim Enter Animation.

View file

@ -13,8 +13,8 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -36,7 +36,6 @@ import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.screens.about.components.AboutItem import ua.acclorite.book_story.presentation.screens.about.components.AboutItem
import ua.acclorite.book_story.presentation.screens.about.data.AboutEvent import ua.acclorite.book_story.presentation.screens.about.data.AboutEvent
import ua.acclorite.book_story.presentation.screens.about.data.AboutViewModel import ua.acclorite.book_story.presentation.screens.about.data.AboutViewModel
import ua.acclorite.book_story.presentation.ui.elevation
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -69,7 +68,7 @@ fun AboutScreen(
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
colors = TopAppBarDefaults.largeTopAppBarColors( colors = TopAppBarDefaults.largeTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.elevation() scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer
) )
) )
} }
@ -101,7 +100,7 @@ fun AboutScreen(
item { item {
Spacer(modifier = Modifier.height(36.dp)) Spacer(modifier = Modifier.height(36.dp))
Divider( HorizontalDivider(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
color = MaterialTheme.colorScheme.outlineVariant color = MaterialTheme.colorScheme.outlineVariant
) )

View file

@ -63,6 +63,7 @@ import ua.acclorite.book_story.presentation.screens.book_info.components.BookInf
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoMoreDropDown import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoMoreDropDown
import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoStatisticSection import ua.acclorite.book_story.presentation.screens.book_info.components.BookInfoStatisticSection
import ua.acclorite.book_story.presentation.screens.book_info.components.change_cover_bottom_sheet.BookInfoChangeCoverBottomSheet import ua.acclorite.book_story.presentation.screens.book_info.components.change_cover_bottom_sheet.BookInfoChangeCoverBottomSheet
import ua.acclorite.book_story.presentation.screens.book_info.components.confirm_update_dialog.BookInfoConfirmUpdateDialog
import ua.acclorite.book_story.presentation.screens.book_info.components.details_bottom_sheet.BookInfoDetailsBottomSheet import ua.acclorite.book_story.presentation.screens.book_info.components.details_bottom_sheet.BookInfoDetailsBottomSheet
import ua.acclorite.book_story.presentation.screens.book_info.components.dialog.BookInfoDeleteDialog import ua.acclorite.book_story.presentation.screens.book_info.components.dialog.BookInfoDeleteDialog
import ua.acclorite.book_story.presentation.screens.book_info.components.dialog.BookInfoMoveDialog import ua.acclorite.book_story.presentation.screens.book_info.components.dialog.BookInfoMoveDialog
@ -75,7 +76,6 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.ui.DefaultTransition import ua.acclorite.book_story.presentation.ui.DefaultTransition
import ua.acclorite.book_story.presentation.ui.Transitions import ua.acclorite.book_story.presentation.ui.Transitions
import ua.acclorite.book_story.presentation.ui.elevation
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Composable @Composable
@ -96,11 +96,7 @@ fun BookInfoScreen(
refreshing = state.isRefreshing, refreshing = state.isRefreshing,
onRefresh = { onRefresh = {
viewModel.onEvent( viewModel.onEvent(
BookInfoEvent.OnUpdateBook( BookInfoEvent.OnLoadUpdate(
refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
},
snackbarState, snackbarState,
context context
) )
@ -142,6 +138,14 @@ fun BookInfoScreen(
navigator = navigator navigator = navigator
) )
} }
if (state.showConfirmUpdateDialog) {
BookInfoConfirmUpdateDialog(
libraryViewModel = libraryViewModel,
historyViewModel = historyViewModel,
viewModel = viewModel,
snackbarHostState = snackbarState
)
}
Scaffold( Scaffold(
Modifier Modifier
@ -153,7 +157,6 @@ fun BookInfoScreen(
topBar = { topBar = {
AnimatedTopAppBar( AnimatedTopAppBar(
containerColor = Color.Transparent, containerColor = Color.Transparent,
scrolledContainerColor = MaterialTheme.elevation(),
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
isTopBarScrolled = null, isTopBarScrolled = null,
@ -174,16 +177,12 @@ fun BookInfoScreen(
content1Actions = { content1Actions = {
CustomIconButton( CustomIconButton(
icon = Icons.Default.Refresh, icon = Icons.Default.Refresh,
contentDescription = stringResource(id = R.string.refresh_book_content_desc), contentDescription = R.string.refresh_book_content_desc,
disableOnClick = false, disableOnClick = false,
enabled = !state.isRefreshing enabled = !state.isRefreshing
) { ) {
viewModel.onEvent( viewModel.onEvent(
BookInfoEvent.OnUpdateBook( BookInfoEvent.OnLoadUpdate(
refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
},
snackbarState, snackbarState,
context context
) )
@ -206,9 +205,10 @@ fun BookInfoScreen(
) { ) {
CustomIconButton( CustomIconButton(
icon = Icons.Default.Done, icon = Icons.Default.Done,
contentDescription = stringResource(id = R.string.apply_changes_content_desc), contentDescription = R.string.apply_changes_content_desc,
disableOnClick = true, disableOnClick = true,
enabled = state.titleValue.isNotBlank() && enabled = !state.isRefreshing &&
state.titleValue.isNotBlank() &&
state.titleValue.trim() != state.titleValue.trim() !=
state.book.title.trim(), state.book.title.trim(),
color = if (state.titleValue.isNotBlank() color = if (state.titleValue.isNotBlank()

View file

@ -10,20 +10,20 @@ import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import ua.acclorite.book_story.presentation.components.CustomCoverImage
/** /**
* Background of the BookInfoScreen. * Background of the BookInfoScreen.
*/ */
@Composable @Composable
fun BookInfoBackground(height: Dp, image: Uri) { fun BookInfoBackground(height: Dp, image: Uri) {
val background = MaterialTheme.colorScheme.background val background = MaterialTheme.colorScheme.surface
AsyncImage( CustomCoverImage(
model = image, uri = image,
animationDurationMillis = 300,
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -38,7 +38,6 @@ fun BookInfoBackground(height: Dp, image: Uri) {
) )
} }
.blur(3.dp), .blur(3.dp),
alpha = 0.4f, alpha = 0.4f
contentScale = ContentScale.Crop
) )
} }

View file

@ -33,19 +33,17 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.presentation.components.CustomCoverImage
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
import ua.acclorite.book_story.presentation.ui.elevation
/** /**
* Info section. * Info section.
@ -66,7 +64,7 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
.height(195.dp) .height(195.dp)
.width(130.dp) .width(130.dp)
.clip(MaterialTheme.shapes.medium) .clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.elevation()) .background(MaterialTheme.colorScheme.surfaceContainerLow)
.combinedClickable( .combinedClickable(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
indication = null, indication = null,
@ -77,13 +75,11 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
) )
) { ) {
if (book.coverImage != null) { if (book.coverImage != null) {
AsyncImage( CustomCoverImage(
model = book.coverImage, uri = book.coverImage,
contentDescription = stringResource(id = R.string.cover_image_content_desc),
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.clip(MaterialTheme.shapes.medium), .clip(MaterialTheme.shapes.medium)
contentScale = ContentScale.Crop
) )
} else { } else {
Icon( Icon(
@ -93,7 +89,7 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
.align(Alignment.Center) .align(Alignment.Center)
.fillMaxWidth(0.7f) .fillMaxWidth(0.7f)
.aspectRatio(1f), .aspectRatio(1f),
tint = MaterialTheme.elevation(12.dp) tint = MaterialTheme.colorScheme.surfaceContainerHigh
) )
} }
} }

View file

@ -11,6 +11,9 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -26,37 +29,39 @@ import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewM
@Composable @Composable
fun BookInfoMoreDropDown(viewModel: BookInfoViewModel, snackbarState: SnackbarHostState) { fun BookInfoMoreDropDown(viewModel: BookInfoViewModel, snackbarState: SnackbarHostState) {
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
var showDropDown by remember { mutableStateOf(false) }
Box { Box {
CustomIconButton( CustomIconButton(
icon = Icons.Default.MoreVert, icon = Icons.Default.MoreVert,
contentDescription = stringResource(id = R.string.show_dropdown_content_desc), contentDescription = R.string.show_dropdown_content_desc,
disableOnClick = false, disableOnClick = false,
enabled = !state.showMoreDropDown enabled = !showDropDown &&
!state.isRefreshing
) { ) {
viewModel.onEvent(BookInfoEvent.OnShowHideMoreDropDown) showDropDown = true
} }
DropdownMenu( DropdownMenu(
expanded = state.showMoreDropDown, expanded = showDropDown,
onDismissRequest = { onDismissRequest = {
viewModel.onEvent(BookInfoEvent.OnShowHideMoreDropDown) showDropDown = false
}, },
offset = DpOffset(10.dp, 0.dp) offset = DpOffset((-15).dp, 0.dp)
) { ) {
CustomDropDownMenuItem( CustomDropDownMenuItem(
leadingIcon = Icons.AutoMirrored.Outlined.DriveFileMove, leadingIcon = Icons.AutoMirrored.Outlined.DriveFileMove,
text = stringResource(id = R.string.move_this_book) text = stringResource(id = R.string.move_this_book)
) { ) {
viewModel.onEvent(BookInfoEvent.OnShowHideMoveDialog) viewModel.onEvent(BookInfoEvent.OnShowHideMoveDialog)
viewModel.onEvent(BookInfoEvent.OnShowHideMoreDropDown) showDropDown = false
} }
CustomDropDownMenuItem( CustomDropDownMenuItem(
leadingIcon = Icons.Outlined.Delete, leadingIcon = Icons.Outlined.Delete,
text = stringResource(id = R.string.delete_this_book) text = stringResource(id = R.string.delete_this_book)
) { ) {
viewModel.onEvent(BookInfoEvent.OnShowHideDeleteDialog) viewModel.onEvent(BookInfoEvent.OnShowHideDeleteDialog)
viewModel.onEvent(BookInfoEvent.OnShowHideMoreDropDown) showDropDown = false
} }
CustomDropDownMenuItem( CustomDropDownMenuItem(
leadingIcon = Icons.Outlined.Info, leadingIcon = Icons.Outlined.Info,
@ -64,7 +69,7 @@ fun BookInfoMoreDropDown(viewModel: BookInfoViewModel, snackbarState: SnackbarHo
) { ) {
snackbarState.currentSnackbarData?.dismiss() snackbarState.currentSnackbarData?.dismiss()
viewModel.onEvent(BookInfoEvent.OnShowHideDetailsBottomSheet) viewModel.onEvent(BookInfoEvent.OnShowHideDetailsBottomSheet)
viewModel.onEvent(BookInfoEvent.OnShowHideMoreDropDown) showDropDown = false
} }
} }
} }

View file

@ -53,11 +53,11 @@ fun BookInfoStatisticSection(book: Book) {
Spacer(modifier = Modifier.height(6.dp)) Spacer(modifier = Modifier.height(6.dp))
LinearProgressIndicator( LinearProgressIndicator(
book.progress.coerceAtLeast(0.01f), progress = { book.progress.coerceAtLeast(0.01f) },
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth(),
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
trackColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(0.05f), trackColor = MaterialTheme.colorScheme.secondaryContainer.copy(0.7f),
strokeCap = StrokeCap.Round, strokeCap = StrokeCap.Round,
) )

View file

@ -31,8 +31,6 @@ import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.ui.ElevationDefaults
import ua.acclorite.book_story.presentation.ui.elevation
/** /**
* Change cover bottom sheet. Lets user select photo from the gallery and replaces old one. * Change cover bottom sheet. Lets user select photo from the gallery and replaces old one.
@ -79,7 +77,7 @@ fun BookInfoChangeCoverBottomSheet(
}, },
sheetState = rememberModalBottomSheetState(true), sheetState = rememberModalBottomSheetState(true),
windowInsets = WindowInsets(0, 0, 0, 0), windowInsets = WindowInsets(0, 0, 0, 0),
containerColor = MaterialTheme.elevation(ElevationDefaults.BottomSheet) containerColor = MaterialTheme.colorScheme.surfaceContainerLow
) { ) {
BookInfoChangeCoverBottomSheetItem( BookInfoChangeCoverBottomSheetItem(

View file

@ -0,0 +1,78 @@
package ua.acclorite.book_story.presentation.screens.book_info.components.confirm_update_dialog
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Update
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.custom_dialog.CustomDialogWithLazyColumn
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
@Composable
fun BookInfoConfirmUpdateDialog(
libraryViewModel: LibraryViewModel,
historyViewModel: HistoryViewModel,
viewModel: BookInfoViewModel,
snackbarHostState: SnackbarHostState
) {
val context = LocalContext.current
val state by viewModel.state.collectAsState()
CustomDialogWithLazyColumn(
title = stringResource(id = R.string.confirm_update),
imageVectorIcon = Icons.Default.Update,
description = stringResource(
id = R.string.confirm_update_description
),
actionText = stringResource(id = R.string.confirm),
isActionEnabled = true,
onDismiss = { viewModel.onEvent(BookInfoEvent.OnDismissConfirmUpdateDialog) },
onAction = {
viewModel.onEvent(
BookInfoEvent.OnConfirmUpdate(
snackbarState = snackbarHostState,
context = context,
refreshList = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(it))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(it))
}
)
)
},
withDivider = false,
items = {
if (state.authorChanged) {
item {
BookInfoConfirmUpdateDialogItem(title = stringResource(id = R.string.author))
}
}
if (state.descriptionChanged) {
item {
BookInfoConfirmUpdateDialogItem(title = stringResource(id = R.string.description))
}
}
if (state.textChanged) {
item {
BookInfoConfirmUpdateDialogItem(title = stringResource(id = R.string.text))
}
}
}
)
}

View file

@ -0,0 +1,18 @@
package ua.acclorite.book_story.presentation.screens.book_info.components.confirm_update_dialog
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun BookInfoConfirmUpdateDialogItem(title: String) {
Text(
text = "$title",
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(horizontal = 24.dp, vertical = 4.dp)
)
}

View file

@ -22,8 +22,6 @@ import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
import ua.acclorite.book_story.presentation.ui.ElevationDefaults
import ua.acclorite.book_story.presentation.ui.elevation
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@ -72,7 +70,7 @@ fun BookInfoDetailsBottomSheet(
}, },
sheetState = rememberModalBottomSheetState(true), sheetState = rememberModalBottomSheetState(true),
windowInsets = WindowInsets(0, 0, 0, 0), windowInsets = WindowInsets(0, 0, 0, 0),
containerColor = MaterialTheme.elevation(ElevationDefaults.BottomSheet) containerColor = MaterialTheme.colorScheme.surfaceContainerLow
) { ) {
BookInfoDetailsBottomSheetItem( BookInfoDetailsBottomSheetItem(
title = stringResource(id = R.string.file_name), title = stringResource(id = R.string.file_name),

View file

@ -19,7 +19,6 @@ sealed class BookInfoEvent {
data class OnRequestFocus(val focusRequester: FocusRequester) : BookInfoEvent() data class OnRequestFocus(val focusRequester: FocusRequester) : BookInfoEvent()
data class OnTitleValueChange(val value: String) : BookInfoEvent() data class OnTitleValueChange(val value: String) : BookInfoEvent()
data class OnUpdateTitle(val refreshList: (Book) -> Unit) : BookInfoEvent() data class OnUpdateTitle(val refreshList: (Book) -> Unit) : BookInfoEvent()
data object OnShowHideMoreDropDown : BookInfoEvent()
data object OnShowHideDeleteDialog : BookInfoEvent() data object OnShowHideDeleteDialog : BookInfoEvent()
data object OnShowHideMoveDialog : BookInfoEvent() data object OnShowHideMoveDialog : BookInfoEvent()
data class OnDeleteBook(val refreshList: () -> Unit, val navigator: Navigator) : BookInfoEvent() data class OnDeleteBook(val refreshList: () -> Unit, val navigator: Navigator) : BookInfoEvent()
@ -44,8 +43,7 @@ sealed class BookInfoEvent {
val snackbarState: SnackbarHostState val snackbarState: SnackbarHostState
) : BookInfoEvent() ) : BookInfoEvent()
data class OnUpdateBook( data class OnLoadUpdate(
val refreshList: (Book) -> Unit,
val snackbarState: SnackbarHostState, val snackbarState: SnackbarHostState,
val context: Context val context: Context
) : BookInfoEvent() ) : BookInfoEvent()
@ -53,4 +51,19 @@ sealed class BookInfoEvent {
data class OnNavigateToReaderScreen( data class OnNavigateToReaderScreen(
val navigator: Navigator val navigator: Navigator
) : BookInfoEvent() ) : BookInfoEvent()
data object OnDismissConfirmUpdateDialog : BookInfoEvent()
data class OnShowConfirmUpdateDialog(
val updatedBook: Book,
val authorUpdated: Boolean,
val descriptionUpdated: Boolean,
val textUpdated: Boolean,
) : BookInfoEvent()
data class OnConfirmUpdate(
val snackbarState: SnackbarHostState,
val context: Context,
val refreshList: (Book) -> Unit
) : BookInfoEvent()
} }

View file

@ -8,7 +8,13 @@ import ua.acclorite.book_story.domain.util.Constants
@Immutable @Immutable
data class BookInfoState( data class BookInfoState(
val book: Book = Constants.EMPTY_BOOK, val book: Book = Constants.EMPTY_BOOK,
val isRefreshing: Boolean = false, val isRefreshing: Boolean = false,
val showConfirmUpdateDialog: Boolean = false,
val updatedBook: Book? = null,
val authorChanged: Boolean = false,
val descriptionChanged: Boolean = false,
val textChanged: Boolean = false,
val editTitle: Boolean = false, val editTitle: Boolean = false,
val hasFocused: Boolean = false, val hasFocused: Boolean = false,
@ -17,9 +23,7 @@ data class BookInfoState(
val showChangeCoverBottomSheet: Boolean = false, val showChangeCoverBottomSheet: Boolean = false,
val showDetailsBottomSheet: Boolean = false, val showDetailsBottomSheet: Boolean = false,
val showMoreDropDown: Boolean = false,
val showDeleteDialog: Boolean = false, val showDeleteDialog: Boolean = false,
val showMoveDialog: Boolean = false, val showMoveDialog: Boolean = false,
val selectedCategory: Category = Category.READING, val selectedCategory: Category = Category.READING,
) )

View file

@ -16,14 +16,15 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.domain.use_case.DeleteBooks import ua.acclorite.book_story.domain.use_case.DeleteBooks
import ua.acclorite.book_story.domain.use_case.GetBookFromFile import ua.acclorite.book_story.domain.use_case.GetBookFromFile
import ua.acclorite.book_story.domain.use_case.GetBooksById import ua.acclorite.book_story.domain.use_case.GetBooksById
import ua.acclorite.book_story.domain.use_case.GetText
import ua.acclorite.book_story.domain.use_case.InsertHistory import ua.acclorite.book_story.domain.use_case.InsertHistory
import ua.acclorite.book_story.domain.use_case.UpdateBooks import ua.acclorite.book_story.domain.use_case.UpdateBooks
import ua.acclorite.book_story.domain.use_case.UpdateBooksWithText import ua.acclorite.book_story.domain.use_case.UpdateBooksWithText
@ -43,7 +44,8 @@ class BookInfoViewModel @Inject constructor(
private val insertHistory: InsertHistory, private val insertHistory: InsertHistory,
private val deleteBooks: DeleteBooks, private val deleteBooks: DeleteBooks,
private val getBookFromFile: GetBookFromFile, private val getBookFromFile: GetBookFromFile,
private val getBookById: GetBooksById private val getBookById: GetBooksById,
private val getText: GetText
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow(BookInfoState()) private val _state = MutableStateFlow(BookInfoState())
@ -74,7 +76,7 @@ class BookInfoViewModel @Inject constructor(
val newCoverImage = getBookById.execute( val newCoverImage = getBookById.execute(
listOf( listOf(
_state.value.book.id ?: return@launch _state.value.book.id
) )
).first().coverImage ).first().coverImage
@ -174,14 +176,6 @@ class BookInfoViewModel @Inject constructor(
} }
} }
is BookInfoEvent.OnShowHideMoreDropDown -> {
_state.update {
it.copy(
showMoreDropDown = !it.showMoreDropDown
)
}
}
is BookInfoEvent.OnShowHideDeleteDialog -> { is BookInfoEvent.OnShowHideDeleteDialog -> {
_state.update { _state.update {
it.copy( it.copy(
@ -274,7 +268,9 @@ class BookInfoViewModel @Inject constructor(
if (event.durationMillis > 0) { if (event.durationMillis > 0) {
job = viewModelScope.launch(Dispatchers.IO) { job = viewModelScope.launch(Dispatchers.IO) {
yield()
delay(event.durationMillis) delay(event.durationMillis)
yield()
event.snackbarState.currentSnackbarData?.dismiss() event.snackbarState.currentSnackbarData?.dismiss()
} }
} }
@ -293,7 +289,7 @@ class BookInfoViewModel @Inject constructor(
} }
} }
is BookInfoEvent.OnUpdateBook -> { is BookInfoEvent.OnLoadUpdate -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
_state.update { _state.update {
it.copy( it.copy(
@ -314,8 +310,7 @@ class BookInfoViewModel @Inject constructor(
action = event.context.getString(R.string.retry), action = event.context.getString(R.string.retry),
onAction = { onAction = {
onEvent( onEvent(
BookInfoEvent.OnUpdateBook( BookInfoEvent.OnLoadUpdate(
refreshList = event.refreshList,
snackbarState = event.snackbarState, snackbarState = event.snackbarState,
context = event.context context = event.context
) )
@ -335,17 +330,15 @@ class BookInfoViewModel @Inject constructor(
} }
val nullableBook = getBookFromFile.execute(_state.value.book.file!!) val nullableBook = getBookFromFile.execute(_state.value.book.file!!)
if (nullableBook is NullableBook.Null) { if (nullableBook is NullableBook.Null) {
onEvent( onEvent(
BookInfoEvent.OnShowSnackbar( BookInfoEvent.OnShowSnackbar(
text = nullableBook.message?.asString(event.context) text = nullableBook.message?.asString(event.context)
?: event.context.getString(R.string.error_something_went_wrong), ?: event.context.getString(R.string.error_something_went_wrong_with_file),
action = event.context.getString(R.string.retry), action = event.context.getString(R.string.retry),
onAction = { onAction = {
onEvent( onEvent(
BookInfoEvent.OnUpdateBook( BookInfoEvent.OnLoadUpdate(
refreshList = event.refreshList,
snackbarState = event.snackbarState, snackbarState = event.snackbarState,
context = event.context context = event.context
) )
@ -366,24 +359,212 @@ class BookInfoViewModel @Inject constructor(
} }
val updatedBook = nullableBook.book?.first ?: return@launch val updatedBook = nullableBook.book?.first ?: return@launch
val book = _state.value.book
var authorUpdated = false
var descriptionUpdated = false
var textUpdated = false
if (
updatedBook.author.asString(event.context) !=
book.author.asString(event.context)
) {
authorUpdated = true
}
if (updatedBook.description != book.description) {
descriptionUpdated = true
}
if (
updatedBook.text.map { it.line } !=
book.text.ifEmpty {
getText.execute(book.textPath)
}.map { it.line }
) {
textUpdated = true
}
if (!authorUpdated && !descriptionUpdated && !textUpdated) {
onEvent(
BookInfoEvent.OnShowSnackbar(
event.context.getString(R.string.nothing_changed),
action = null,
durationMillis = 4000L,
snackbarState = event.snackbarState
)
)
delay(500)
_state.update {
it.copy(
isRefreshing = false
)
}
return@launch
}
onEvent(
BookInfoEvent.OnShowConfirmUpdateDialog(
updatedBook = updatedBook,
authorUpdated = authorUpdated,
descriptionUpdated = descriptionUpdated,
textUpdated = textUpdated
)
)
_state.update {
it.copy(
isRefreshing = false
)
}
}
}
is BookInfoEvent.OnDismissConfirmUpdateDialog -> {
_state.update {
it.copy(
showConfirmUpdateDialog = false,
updatedBook = null,
authorChanged = false,
descriptionChanged = false,
textChanged = false
)
}
}
is BookInfoEvent.OnShowConfirmUpdateDialog -> {
_state.update {
it.copy(
showConfirmUpdateDialog = true,
updatedBook = event.updatedBook,
authorChanged = event.authorUpdated,
descriptionChanged = event.descriptionUpdated,
textChanged = event.textUpdated
)
}
}
is BookInfoEvent.OnConfirmUpdate -> {
viewModelScope.launch(Dispatchers.IO) {
_state.update {
it.copy(
isRefreshing = true,
showConfirmUpdateDialog = false
)
}
if (_state.value.updatedBook == null) {
onEvent(
BookInfoEvent.OnShowSnackbar(
text = event.context.getString(
R.string.error_something_went_wrong_with_file
),
action = event.context.getString(R.string.retry),
onAction = {
onEvent(
BookInfoEvent.OnLoadUpdate(
snackbarState = event.snackbarState,
context = event.context
)
)
},
durationMillis = 4000L,
snackbarState = event.snackbarState
)
)
delay(500)
_state.update {
it.copy(
isRefreshing = false
)
}
return@launch
}
val book = _state.value.book
val updatedBook = _state.value.updatedBook ?: return@launch
val author = if (_state.value.authorChanged) {
updatedBook.author
} else {
book.author
}
val description = if (_state.value.descriptionChanged) {
updatedBook.description
} else {
book.description
}
val text = if (_state.value.textChanged) {
updatedBook.text
} else {
book.text
}
_state.update { _state.update {
it.copy( it.copy(
book = it.book.copy( book = it.book.copy(
author = updatedBook.author, author = author,
description = updatedBook.description description = description,
text = text
) )
) )
} }
updateBooksWithText.execute(
if (_state.value.textChanged) {
val isSuccess = updateBooksWithText.execute(
listOf( listOf(
_state.value.book.copy( _state.value.book
author = updatedBook.author,
description = updatedBook.description,
text = updatedBook.text
) )
) )
if (!isSuccess) {
onEvent(
BookInfoEvent.OnShowSnackbar(
text = event.context.getString(
R.string.error_something_went_wrong_with_file
),
action = event.context.getString(R.string.retry),
onAction = {
onEvent(
BookInfoEvent.OnLoadUpdate(
snackbarState = event.snackbarState,
context = event.context
) )
)
},
durationMillis = 4000L,
snackbarState = event.snackbarState
)
)
delay(500)
_state.update {
it.copy(
isRefreshing = false
)
}
return@launch
}
} else {
updateBooks.execute(
listOf(
_state.value.book
)
)
}
if (_state.value.textChanged) {
val newBook = getBookById.execute(
listOf(_state.value.book.id)
).first()
_state.update {
it.copy(
book = newBook,
authorChanged = false,
descriptionChanged = false,
textChanged = false,
updatedBook = null
)
}
}
event.refreshList(_state.value.book) event.refreshList(_state.value.book)
onEvent( onEvent(
@ -406,14 +587,13 @@ class BookInfoViewModel @Inject constructor(
is BookInfoEvent.OnNavigateToReaderScreen -> { is BookInfoEvent.OnNavigateToReaderScreen -> {
viewModelScope.launch { viewModelScope.launch {
_state.value.book.id?.let { _state.value.book.id.let {
insertHistory.execute( insertHistory.execute(
listOf( listOf(
History( History(
null, bookId = it,
it, book = null,
null, time = Date().time
Date().time
) )
) )
) )
@ -422,7 +602,7 @@ class BookInfoViewModel @Inject constructor(
Screen.READER, Screen.READER,
false, false,
Argument( Argument(
"book", _state.value.book "book", _state.value.book.id
) )
) )
} }
@ -432,15 +612,22 @@ class BookInfoViewModel @Inject constructor(
fun init(navigator: Navigator) { fun init(navigator: Navigator) {
viewModelScope.launch { viewModelScope.launch {
val book = navigator.retrieveArgument("book") as? Book val bookId = navigator.retrieveArgument("book") as? Int
if (book == null) { if (bookId == null) {
navigator.navigateBack()
return@launch
}
val book = getBookById.execute(listOf(bookId))
if (book.isEmpty()) {
navigator.navigateBack() navigator.navigateBack()
return@launch return@launch
} }
_state.update { _state.update {
BookInfoState(book = book) BookInfoState(book = book.first())
} }
} }
} }

View file

@ -67,7 +67,6 @@ import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.ui.DefaultTransition import ua.acclorite.book_story.presentation.ui.DefaultTransition
import ua.acclorite.book_story.presentation.ui.Transitions import ua.acclorite.book_story.presentation.ui.Transitions
import ua.acclorite.book_story.presentation.ui.elevation
@OptIn( @OptIn(
ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
@ -96,6 +95,7 @@ fun BrowseScreen(
var showErrorMessage by remember { mutableStateOf(false) } var showErrorMessage by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.onEvent(BrowseEvent.OnUpdateScrollOffset)
viewModel.onEvent( viewModel.onEvent(
BrowseEvent.OnPermissionCheck( BrowseEvent.OnPermissionCheck(
permissionState, permissionState,
@ -124,9 +124,6 @@ fun BrowseScreen(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
topBar = { topBar = {
AnimatedTopAppBar( AnimatedTopAppBar(
containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.elevation(),
scrollBehavior = null, scrollBehavior = null,
isTopBarScrolled = state.hasSelectedItems || state.listState.canScrollBackward, isTopBarScrolled = state.hasSelectedItems || state.listState.canScrollBackward,
@ -138,7 +135,7 @@ fun BrowseScreen(
content1Actions = { content1Actions = {
CustomIconButton( CustomIconButton(
icon = Icons.Default.Search, icon = Icons.Default.Search,
contentDescription = stringResource(id = R.string.search_content_desc), contentDescription = R.string.search_content_desc,
disableOnClick = true disableOnClick = true
) { ) {
viewModel.onEvent(BrowseEvent.OnSearchShowHide) viewModel.onEvent(BrowseEvent.OnSearchShowHide)
@ -150,8 +147,7 @@ fun BrowseScreen(
content2NavigationIcon = { content2NavigationIcon = {
CustomIconButton( CustomIconButton(
icon = Icons.Default.Clear, icon = Icons.Default.Clear,
contentDescription = contentDescription = R.string.clear_selected_items_content_desc,
stringResource(id = R.string.clear_selected_items_content_desc),
disableOnClick = true disableOnClick = true
) { ) {
viewModel.onEvent(BrowseEvent.OnClearSelectedFiles) viewModel.onEvent(BrowseEvent.OnClearSelectedFiles)
@ -170,8 +166,7 @@ fun BrowseScreen(
content2Actions = { content2Actions = {
CustomIconButton( CustomIconButton(
icon = Icons.Default.Check, icon = Icons.Default.Check,
contentDescription = contentDescription = R.string.add_files_content_desc,
stringResource(id = R.string.add_files_content_desc),
disableOnClick = false, disableOnClick = false,
enabled = !state.showAddingDialog enabled = !state.showAddingDialog
) { ) {
@ -183,9 +178,7 @@ fun BrowseScreen(
content3NavigationIcon = { content3NavigationIcon = {
CustomIconButton( CustomIconButton(
icon = Icons.AutoMirrored.Default.ArrowBack, icon = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = stringResource( contentDescription = R.string.exit_search_content_desc,
id = R.string.exit_search_content_desc
),
disableOnClick = true disableOnClick = true
) { ) {
viewModel.onEvent(BrowseEvent.OnSearchShowHide) viewModel.onEvent(BrowseEvent.OnSearchShowHide)
@ -256,8 +249,7 @@ fun BrowseScreen(
) { selectableFile -> ) { selectableFile ->
BrowseFileItem( BrowseFileItem(
file = selectableFile, file = selectableFile,
modifier = Modifier modifier = Modifier.animateItemPlacement(),
.animateItemPlacement(),
hasSelectedFiles = state.selectableFiles.any { it.second }, hasSelectedFiles = state.selectableFiles.any { it.second },
onLongClick = { onLongClick = {
Toast.makeText( Toast.makeText(

View file

@ -1,7 +1,5 @@
package ua.acclorite.book_story.presentation.screens.browse.components package ua.acclorite.book_story.presentation.screens.browse.components
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
@ -23,7 +21,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -71,24 +68,13 @@ fun BrowseFileItem(
val backgroundColor = if (file.second) MaterialTheme.colorScheme.secondaryContainer val backgroundColor = if (file.second) MaterialTheme.colorScheme.secondaryContainer
else Color.Transparent else Color.Transparent
val animatedOutlineColor by animateColorAsState(
targetValue = outlineColor,
tween(300),
label = stringResource(id = R.string.outline_anim_content_desc)
)
val animatedBackgroundColor by animateColorAsState(
targetValue = backgroundColor,
tween(300),
label = stringResource(id = R.string.background_anim_content_desc)
)
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 3.dp) .padding(horizontal = 8.dp, vertical = 3.dp)
.clip(RoundedCornerShape(10.dp)) .clip(RoundedCornerShape(10.dp))
.background(animatedBackgroundColor) .background(backgroundColor)
.combinedClickable( .combinedClickable(
onLongClick = { onLongClick = {
onLongClick() onLongClick()
@ -107,7 +93,7 @@ fun BrowseFileItem(
modifier = Modifier modifier = Modifier
.border( .border(
1.dp, 1.dp,
animatedOutlineColor, outlineColor,
RoundedCornerShape(6.dp) RoundedCornerShape(6.dp)
) )
.padding(14.dp), .padding(14.dp),
@ -153,7 +139,11 @@ fun BrowseFileItem(
DefaultTransition( DefaultTransition(
visible = hasSelectedFiles visible = hasSelectedFiles
) { ) {
CustomCheckbox(selected = file.second, size = 18.dp) CustomCheckbox(
selected = file.second,
containerColor = MaterialTheme.colorScheme.surface,
size = 18.dp
)
} }
} }
} }

View file

@ -6,9 +6,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddChart import androidx.compose.material.icons.filled.AddChart
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
@ -55,14 +55,23 @@ fun BrowseAddingDialog(
resetScroll = { resetScroll = {
libraryViewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(0)) libraryViewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(0))
libraryViewModel.onEvent(LibraryEvent.OnLoadList) libraryViewModel.onEvent(LibraryEvent.OnLoadList)
} },
) onFailed = {
) Toast.makeText(
context,
context.getString(R.string.error_something_went_wrong),
Toast.LENGTH_LONG
).show()
},
onSuccess = {
Toast.makeText( Toast.makeText(
context, context,
context.getString(R.string.books_added), context.getString(R.string.books_added),
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
}
)
)
}, },
withDivider = true, withDivider = true,
items = { items = {

View file

@ -59,7 +59,10 @@ fun BrowseAddingDialogItem(result: Pair<NullableBook, Selected>, onClick: (Boole
) )
} }
Box(modifier = Modifier.weight(0.15f), contentAlignment = Alignment.CenterEnd) { Box(modifier = Modifier.weight(0.15f), contentAlignment = Alignment.CenterEnd) {
CustomCheckbox(selected = result.second) CustomCheckbox(
selected = result.second,
MaterialTheme.colorScheme.surfaceContainerHigh
)
} }
} }
} else { } else {

View file

@ -39,7 +39,13 @@ sealed class BrowseEvent {
data object OnAddingDialogRequest : BrowseEvent() data object OnAddingDialogRequest : BrowseEvent()
data object OnAddingDialogDismiss : BrowseEvent() data object OnAddingDialogDismiss : BrowseEvent()
data object OnGetBooksFromFiles : BrowseEvent() data object OnGetBooksFromFiles : BrowseEvent()
data class OnAddBooks(val navigator: Navigator, val resetScroll: () -> Unit) : BrowseEvent() data class OnAddBooks(
data object OnLoadList : BrowseEvent() val navigator: Navigator,
val resetScroll: () -> Unit,
val onFailed: () -> Unit,
val onSuccess: () -> Unit
) : BrowseEvent()
data object OnLoadList : BrowseEvent()
data object OnUpdateScrollOffset : BrowseEvent()
} }

View file

@ -18,13 +18,12 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import ua.acclorite.book_story.domain.model.NullableBook import ua.acclorite.book_story.domain.model.NullableBook
import ua.acclorite.book_story.domain.use_case.FastGetBooks
import ua.acclorite.book_story.domain.use_case.GetBooksFromFiles import ua.acclorite.book_story.domain.use_case.GetBooksFromFiles
import ua.acclorite.book_story.domain.use_case.GetFilesFromDevice import ua.acclorite.book_story.domain.use_case.GetFilesFromDevice
import ua.acclorite.book_story.domain.use_case.InsertBooks import ua.acclorite.book_story.domain.use_case.InsertBooks
import ua.acclorite.book_story.domain.util.Resource import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.presentation.data.Argument
import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.data.Screen
import javax.inject.Inject import javax.inject.Inject
@ -33,8 +32,7 @@ import javax.inject.Inject
class BrowseViewModel @Inject constructor( class BrowseViewModel @Inject constructor(
private val getBooksFromFiles: GetBooksFromFiles, private val getBooksFromFiles: GetBooksFromFiles,
private val getFilesFromDevice: GetFilesFromDevice, private val getFilesFromDevice: GetFilesFromDevice,
private val insertBooks: InsertBooks, private val insertBooks: InsertBooks
private val fastGetBooks: FastGetBooks
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow(BrowseState()) private val _state = MutableStateFlow(BrowseState())
@ -42,6 +40,7 @@ class BrowseViewModel @Inject constructor(
private var job: Job? = null private var job: Job? = null
private var job2: Job? = null private var job2: Job? = null
private var job3: Job? = null
init { init {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
@ -138,7 +137,7 @@ class BrowseViewModel @Inject constructor(
listState = LazyListState(0, 0) listState = LazyListState(0, 0)
) )
} }
yield()
getFilesFromDownloads("") getFilesFromDownloads("")
delay(500) delay(500)
_state.update { _state.update {
@ -270,6 +269,7 @@ class BrowseViewModel @Inject constructor(
job?.cancel() job?.cancel()
job = viewModelScope.launch(Dispatchers.IO) { job = viewModelScope.launch(Dispatchers.IO) {
delay(500) delay(500)
yield()
getFilesFromDownloads() getFilesFromDownloads()
} }
} }
@ -280,6 +280,7 @@ class BrowseViewModel @Inject constructor(
showAddingDialog = false showAddingDialog = false
) )
} }
job3?.cancel(null)
} }
is BrowseEvent.OnAddingDialogRequest -> { is BrowseEvent.OnAddingDialogRequest -> {
@ -293,18 +294,29 @@ class BrowseViewModel @Inject constructor(
} }
is BrowseEvent.OnGetBooksFromFiles -> { is BrowseEvent.OnGetBooksFromFiles -> {
viewModelScope.launch(Dispatchers.IO) { job3 = viewModelScope.launch(Dispatchers.IO) {
_state.update { _state.update {
it.copy( it.copy(
isBooksLoading = true isBooksLoading = true
) )
} }
val books = getBooksFromFiles.execute( yield()
val books = mutableListOf<NullableBook>()
_state.value.selectableFiles _state.value.selectableFiles
.filter { it.second } .filter { it.second }
.map { it.first } .map { it.first }
.forEach {
yield()
books.add(
getBooksFromFiles.execute(
listOf(it)
).first()
) )
}
yield()
_state.update { _state.update {
it.copy( it.copy(
@ -326,21 +338,21 @@ class BrowseViewModel @Inject constructor(
return@launch return@launch
} }
insertBooks.execute(booksToInsert) if (!insertBooks.execute(booksToInsert)) {
val books = fastGetBooks.execute("") event.onFailed()
return@launch
}
event.resetScroll() event.resetScroll()
event.navigator.navigate( event.navigator.navigate(
Screen.LIBRARY, Screen.LIBRARY,
false, false
Argument("added_books", books)
) )
event.onSuccess()
_state.update { _state.update {
it.copy( it.copy(
showAddingDialog = false, showAddingDialog = false
selectableFiles = emptyList(),
listState = LazyListState(0, 0)
) )
} }
onEvent(BrowseEvent.OnLoadList) onEvent(BrowseEvent.OnLoadList)
@ -360,6 +372,17 @@ class BrowseViewModel @Inject constructor(
getFilesFromDownloads() getFilesFromDownloads()
} }
} }
is BrowseEvent.OnUpdateScrollOffset -> {
_state.update {
it.copy(
listState = LazyListState(
it.listState.firstVisibleItemIndex,
0
)
)
}
}
} }
} }
@ -378,8 +401,6 @@ class BrowseViewModel @Inject constructor(
} }
} }
is Resource.Loading -> Unit
is Resource.Error -> Unit is Resource.Error -> Unit
} }
} }

View file

@ -0,0 +1,85 @@
package ua.acclorite.book_story.presentation.screens.help
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LargeTopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.GoBackButton
import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.screens.help.components.HelpClickableNote
import ua.acclorite.book_story.presentation.screens.help.data.HelpViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HelpScreen(
viewModel: HelpViewModel = hiltViewModel(),
navigator: Navigator
) {
val context = LocalContext.current
val listState = rememberLazyListState()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
canScroll = {
listState.canScrollForward || listState.canScrollBackward
}
)
Scaffold(
Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection)
.windowInsetsPadding(WindowInsets.navigationBars),
containerColor = MaterialTheme.colorScheme.surface,
topBar = {
LargeTopAppBar(
title = {
Text(stringResource(id = R.string.help_screen))
},
navigationIcon = {
GoBackButton(navigator = navigator)
},
scrollBehavior = scrollBehavior,
colors = TopAppBarDefaults.largeTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer
)
)
}
) { paddingValues ->
LazyColumn(
Modifier
.fillMaxSize()
.padding(top = paddingValues.calculateTopPadding())
.padding(horizontal = 18.dp),
state = listState
) {
item { Spacer(modifier = Modifier.height(16.dp)) }
item { HelpClickableNote() }
item { Spacer(modifier = Modifier.height(16.dp)) }
/* TODO Help screen */
item { Spacer(modifier = Modifier.height(48.dp)) }
}
}
}

View file

@ -0,0 +1,83 @@
package ua.acclorite.book_story.presentation.screens.help.components
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun HelpClickableNote() {
val context = LocalContext.current
Column {
Icon(
imageVector = Icons.Outlined.Info,
contentDescription = stringResource(id = R.string.note_content_desc),
modifier = Modifier
.size(20.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(8.dp))
FlowRow(verticalArrangement = Arrangement.spacedBy(1.dp)) {
Text(
stringResource(id = R.string.clickable_note_1) + " ",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.align(Alignment.CenterVertically)
)
Text(
stringResource(id = R.string.clickable_note_2) + " ",
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.tertiary,
modifier = Modifier
.align(Alignment.CenterVertically)
.clickable(
interactionSource = null,
indication = null,
onClick = {
Toast
.makeText(
context,
context.getString(R.string.clickable_note_action),
Toast.LENGTH_SHORT
)
.show()
}
)
)
Text(
stringResource(id = R.string.clickable_note_3),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.align(Alignment.CenterVertically)
)
}
}
}

View file

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

View file

@ -0,0 +1,43 @@
package ua.acclorite.book_story.presentation.screens.help.data
import android.content.Intent
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class HelpViewModel @Inject constructor(
) : ViewModel() {
fun onEvent(event: HelpEvent) {
when (event) {
is HelpEvent.OnNavigateToBrowserPage -> {
viewModelScope.launch {
val intent = Intent(
Intent.ACTION_VIEW,
Uri.parse(event.page)
)
if (intent.resolveActivity(event.context.packageManager) != null) {
event.context.startActivity(intent)
return@launch
}
event.noAppsFound()
}
}
}
}
}

View file

@ -30,6 +30,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -64,8 +65,6 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.ui.DefaultTransition import ua.acclorite.book_story.presentation.ui.DefaultTransition
import ua.acclorite.book_story.presentation.ui.Transitions import ua.acclorite.book_story.presentation.ui.Transitions
import ua.acclorite.book_story.presentation.ui.elevation
import java.util.UUID
@OptIn( @OptIn(
ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
@ -88,6 +87,10 @@ fun HistoryScreen(
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val snackbarState = remember { SnackbarHostState() } val snackbarState = remember { SnackbarHostState() }
LaunchedEffect(Unit) {
viewModel.onEvent(HistoryEvent.OnUpdateScrollOffset)
}
if (state.showDeleteWholeHistoryDialog) { if (state.showDeleteWholeHistoryDialog) {
HistoryDeleteWholeHistoryDialog(viewModel = viewModel, libraryViewModel = libraryViewModel) HistoryDeleteWholeHistoryDialog(viewModel = viewModel, libraryViewModel = libraryViewModel)
} }
@ -99,7 +102,6 @@ fun HistoryScreen(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
topBar = { topBar = {
AnimatedTopAppBar( AnimatedTopAppBar(
scrolledContainerColor = MaterialTheme.elevation(),
scrollBehavior = null, scrollBehavior = null,
isTopBarScrolled = state.listState.canScrollBackward, isTopBarScrolled = state.listState.canScrollBackward,
@ -113,16 +115,14 @@ fun HistoryScreen(
content1Actions = { content1Actions = {
CustomIconButton( CustomIconButton(
icon = Icons.Default.Search, icon = Icons.Default.Search,
contentDescription = stringResource(id = R.string.search_content_desc), contentDescription = R.string.search_content_desc,
disableOnClick = true, disableOnClick = true,
) { ) {
viewModel.onEvent(HistoryEvent.OnSearchShowHide) viewModel.onEvent(HistoryEvent.OnSearchShowHide)
} }
CustomIconButton( CustomIconButton(
icon = Icons.Outlined.DeleteSweep, icon = Icons.Outlined.DeleteSweep,
contentDescription = stringResource( contentDescription = R.string.delete_whole_history_content_desc,
id = R.string.delete_whole_history_content_desc
),
disableOnClick = false, disableOnClick = false,
enabled = !state.isLoading enabled = !state.isLoading
&& !state.isRefreshing && !state.isRefreshing
@ -138,9 +138,7 @@ fun HistoryScreen(
content2NavigationIcon = { content2NavigationIcon = {
CustomIconButton( CustomIconButton(
icon = Icons.AutoMirrored.Default.ArrowBack, icon = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = stringResource( contentDescription = R.string.exit_search_content_desc,
id = R.string.exit_search_content_desc
),
disableOnClick = true disableOnClick = true
) { ) {
viewModel.onEvent(HistoryEvent.OnSearchShowHide) viewModel.onEvent(HistoryEvent.OnSearchShowHide)
@ -229,7 +227,7 @@ fun HistoryScreen(
} }
items( items(
groupedHistory.history, key = { it.id ?: UUID.randomUUID() } groupedHistory.history, key = { it.id }
) { ) {
HistoryItem( HistoryItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemPlacement(),
@ -239,14 +237,14 @@ fun HistoryScreen(
navigator.navigate( navigator.navigate(
Screen.BOOK_INFO, Screen.BOOK_INFO,
false, false,
Argument("book", it.book) Argument("book", it.bookId)
) )
}, },
onTitleClick = { onTitleClick = {
viewModel.onEvent( viewModel.onEvent(
HistoryEvent.OnNavigateToReaderScreen( HistoryEvent.OnNavigateToReaderScreen(
navigator, navigator,
it.book it.book!!
) )
) )
}, },

View file

@ -19,7 +19,6 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Image import androidx.compose.material.icons.filled.Image
import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -27,15 +26,14 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.presentation.ui.elevation import ua.acclorite.book_story.presentation.components.CustomCoverImage
import ua.acclorite.book_story.presentation.components.CustomIconButton
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@ -76,16 +74,14 @@ fun HistoryItem(
.height(90.dp) .height(90.dp)
.width(60.dp) .width(60.dp)
.clip(RoundedCornerShape(10.dp)) .clip(RoundedCornerShape(10.dp))
.background(MaterialTheme.elevation()) .background(MaterialTheme.colorScheme.surfaceContainerLow)
) { ) {
if (history.book?.coverImage != null) { if (history.book?.coverImage != null) {
AsyncImage( CustomCoverImage(
model = history.book.coverImage, uri = history.book.coverImage,
contentDescription = stringResource(id = R.string.cover_image_content_desc),
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.clip(RoundedCornerShape(10.dp)), .clip(RoundedCornerShape(10.dp))
contentScale = ContentScale.Crop
) )
} else { } else {
Icon( Icon(
@ -97,7 +93,7 @@ fun HistoryItem(
.align(Alignment.Center) .align(Alignment.Center)
.fillMaxWidth(0.7f) .fillMaxWidth(0.7f)
.aspectRatio(1f), .aspectRatio(1f),
tint = MaterialTheme.elevation(12.dp) tint = MaterialTheme.colorScheme.surfaceContainerHigh
) )
} }
} }
@ -137,17 +133,14 @@ fun HistoryItem(
} }
} }
Box(modifier = Modifier.weight(0.11f), contentAlignment = Alignment.CenterEnd) { Box(modifier = Modifier.weight(0.11f), contentAlignment = Alignment.CenterEnd) {
IconButton( CustomIconButton(
enabled = isDeleteEnabled && isOnClickEnabled, icon = Icons.Outlined.Delete,
onClick = { contentDescription = R.string.delete_history_element_content_desc,
onDeleteClick() disableOnClick = true,
} enabled = isDeleteEnabled,
color = MaterialTheme.colorScheme.onSurfaceVariant
) { ) {
Icon( onDeleteClick()
imageVector = Icons.Outlined.Delete,
contentDescription = "Delete this history element",
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
} }
} }
} }

View file

@ -25,6 +25,8 @@ sealed class HistoryEvent {
data class OnUpdateBook(val book: Book) : HistoryEvent() data class OnUpdateBook(val book: Book) : HistoryEvent()
data class OnNavigateToReaderScreen( data class OnNavigateToReaderScreen(
val navigator: Navigator, val navigator: Navigator,
val book: Book? val book: Book
) : HistoryEvent() ) : HistoryEvent()
data object OnUpdateScrollOffset : HistoryEvent()
} }

View file

@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.GroupedHistory import ua.acclorite.book_story.domain.model.GroupedHistory
import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.History
@ -69,7 +70,9 @@ class HistoryViewModel @Inject constructor(
) )
} }
yield()
getHistoryFromDatabase("") getHistoryFromDatabase("")
delay(500) delay(500)
_state.update { _state.update {
it.copy( it.copy(
@ -121,14 +124,27 @@ class HistoryViewModel @Inject constructor(
listOf(event.historyToDelete) listOf(event.historyToDelete)
) )
onEvent(HistoryEvent.OnRefreshList) _state.update {
it.copy(
isRefreshing = true
)
}
getHistoryFromDatabase()
_state.update {
it.copy(
isRefreshing = false
)
}
event.refreshList() event.refreshList()
job2?.cancel() job2?.cancel()
event.snackbarState.currentSnackbarData?.dismiss() event.snackbarState.currentSnackbarData?.dismiss()
job2 = viewModelScope.launch(Dispatchers.IO) { job2 = viewModelScope.launch(Dispatchers.IO) {
yield()
delay(10000) delay(10000)
yield()
event.snackbarState.currentSnackbarData?.dismiss() event.snackbarState.currentSnackbarData?.dismiss()
} }
val snackbar = event.snackbarState.showSnackbar( val snackbar = event.snackbarState.showSnackbar(
@ -143,7 +159,18 @@ class HistoryViewModel @Inject constructor(
listOf(event.historyToDelete) listOf(event.historyToDelete)
) )
event.refreshList() event.refreshList()
onEvent(HistoryEvent.OnRefreshList) _state.update {
it.copy(
isRefreshing = true
)
}
getHistoryFromDatabase()
delay(500)
_state.update {
it.copy(
isRefreshing = false
)
}
} }
} }
} }
@ -180,6 +207,7 @@ class HistoryViewModel @Inject constructor(
job?.cancel() job?.cancel()
job = viewModelScope.launch(Dispatchers.IO) { job = viewModelScope.launch(Dispatchers.IO) {
delay(500) delay(500)
yield()
getHistoryFromDatabase() getHistoryFromDatabase()
} }
} }
@ -221,14 +249,13 @@ class HistoryViewModel @Inject constructor(
is HistoryEvent.OnNavigateToReaderScreen -> { is HistoryEvent.OnNavigateToReaderScreen -> {
viewModelScope.launch { viewModelScope.launch {
event.book?.id?.let { event.book.id.let {
insertHistory.execute( insertHistory.execute(
listOf( listOf(
History( History(
null, bookId = it,
it, book = null,
null, time = Date().time
Date().time
) )
) )
) )
@ -237,7 +264,18 @@ class HistoryViewModel @Inject constructor(
Screen.READER, Screen.READER,
false, false,
Argument( Argument(
"book", event.book "book", event.book.id
)
)
}
}
is HistoryEvent.OnUpdateScrollOffset -> {
_state.update {
it.copy(
listState = LazyListState(
it.listState.firstVisibleItemIndex,
0
) )
) )
} }
@ -343,8 +381,6 @@ class HistoryViewModel @Inject constructor(
} }
} }
is Resource.Loading -> Unit
is Resource.Error -> Unit is Resource.Error -> Unit
} }
} }

View file

@ -21,8 +21,10 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
@ -43,7 +45,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@ -86,8 +87,6 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.ui.DefaultTransition import ua.acclorite.book_story.presentation.ui.DefaultTransition
import ua.acclorite.book_story.presentation.ui.Transitions import ua.acclorite.book_story.presentation.ui.Transitions
import ua.acclorite.book_story.presentation.ui.elevation
import java.util.UUID
@OptIn( @OptIn(
ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class,
@ -105,6 +104,7 @@ fun LibraryScreen(
initialPage = state.currentPage, initialPage = state.currentPage,
pageCount = { Category.entries.size } pageCount = { Category.entries.size }
) )
var isScrollInProgress by remember { mutableStateOf(false) }
val refreshState = rememberPullRefreshState( val refreshState = rememberPullRefreshState(
refreshing = state.isRefreshing, refreshing = state.isRefreshing,
onRefresh = { onRefresh = {
@ -145,9 +145,6 @@ fun LibraryScreen(
topBar = { topBar = {
Column(Modifier.fillMaxWidth()) { Column(Modifier.fillMaxWidth()) {
AnimatedTopAppBar( AnimatedTopAppBar(
containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.elevation(),
scrollBehavior = null, scrollBehavior = null,
isTopBarScrolled = state.hasSelectedItems, isTopBarScrolled = state.hasSelectedItems,
@ -160,8 +157,8 @@ fun LibraryScreen(
Text( Text(
text = state.books.size.toString(), text = state.books.size.toString(),
modifier = Modifier modifier = Modifier
.clip(MaterialTheme.shapes.medium) .clip(RoundedCornerShape(14.dp))
.background(MaterialTheme.elevation(elevation = 6.dp)) .background(MaterialTheme.colorScheme.surfaceContainer)
.padding(horizontal = 8.dp, vertical = 3.dp), .padding(horizontal = 8.dp, vertical = 3.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 16.sp fontSize = 16.sp
@ -171,7 +168,7 @@ fun LibraryScreen(
content1Actions = { content1Actions = {
CustomIconButton( CustomIconButton(
icon = Icons.Default.Search, icon = Icons.Default.Search,
contentDescription = stringResource(id = R.string.search_content_desc), contentDescription = R.string.search_content_desc,
disableOnClick = true, disableOnClick = true,
) { ) {
viewModel.onEvent(LibraryEvent.OnSearchShowHide) viewModel.onEvent(LibraryEvent.OnSearchShowHide)
@ -183,8 +180,7 @@ fun LibraryScreen(
content2NavigationIcon = { content2NavigationIcon = {
CustomIconButton( CustomIconButton(
icon = Icons.Default.Clear, icon = Icons.Default.Clear,
contentDescription = contentDescription = R.string.clear_selected_items_content_desc,
stringResource(id = R.string.clear_selected_items_content_desc),
disableOnClick = true disableOnClick = true
) { ) {
viewModel.onEvent(LibraryEvent.OnClearSelectedBooks) viewModel.onEvent(LibraryEvent.OnClearSelectedBooks)
@ -203,9 +199,7 @@ fun LibraryScreen(
content2Actions = { content2Actions = {
CustomIconButton( CustomIconButton(
icon = Icons.AutoMirrored.Outlined.DriveFileMove, icon = Icons.AutoMirrored.Outlined.DriveFileMove,
contentDescription = stringResource( contentDescription = R.string.move_books_content_desc,
id = R.string.move_books_content_desc
),
enabled = !state.isLoading enabled = !state.isLoading
&& !state.isRefreshing && !state.isRefreshing
&& !state.showMoveDialog, && !state.showMoveDialog,
@ -216,9 +210,7 @@ fun LibraryScreen(
} }
CustomIconButton( CustomIconButton(
icon = Icons.Outlined.Delete, icon = Icons.Outlined.Delete,
contentDescription = stringResource( contentDescription = R.string.delete_books_content_desc,
id = R.string.delete_books_content_desc
),
enabled = !state.isLoading enabled = !state.isLoading
&& !state.isRefreshing && !state.isRefreshing
&& !state.showDeleteDialog, && !state.showDeleteDialog,
@ -232,9 +224,7 @@ fun LibraryScreen(
content3NavigationIcon = { content3NavigationIcon = {
CustomIconButton( CustomIconButton(
icon = Icons.AutoMirrored.Default.ArrowBack, icon = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = stringResource( contentDescription = R.string.exit_search_content_desc,
id = R.string.exit_search_content_desc
),
disableOnClick = true disableOnClick = true
) { ) {
viewModel.onEvent( viewModel.onEvent(
@ -289,7 +279,7 @@ fun LibraryScreen(
) )
LibraryTabRow( LibraryTabRow(
viewModel = viewModel, viewModel = viewModel,
books = state.books.map { it.first }, books = state.categorizedBooks,
pagerState = pagerState pagerState = pagerState
) )
} }
@ -300,34 +290,40 @@ fun LibraryScreen(
.fillMaxSize() .fillMaxSize()
.padding(top = paddingValues.calculateTopPadding()) .padding(top = paddingValues.calculateTopPadding())
) { ) {
HorizontalPager(state = pagerState, userScrollEnabled = !state.isRefreshing) { index -> HorizontalPager(
var categoryIsLoading by remember { mutableStateOf(true) } state = pagerState,
val categorizedBooks = remember { mutableStateListOf<Pair<Book, Selected>>() } userScrollEnabled = !isScrollInProgress
) { index ->
val category = remember { Category.entries[index] } val category = remember { Category.entries[index] }
val books = remember(state.categorizedBooks) {
LaunchedEffect(state.books) { state.categorizedBooks.find {
categorizedBooks.clear() it.category == category
categorizedBooks.addAll( }?.books?.sortedWith(
state.books
.filter { it.first.category == category }
.sortedWith(
compareByDescending<Pair<Book, Selected>> { it.first.lastOpened } compareByDescending<Pair<Book, Selected>> { it.first.lastOpened }
.thenBy { it.first.title } .thenBy { it.first.title }
) ) ?: emptyList()
) }
categoryIsLoading = false val listState = rememberLazyGridState()
LaunchedEffect(listState.isScrollInProgress, pagerState.currentPage) {
if (listState.isScrollInProgress != isScrollInProgress
&& pagerState.currentPage == state.currentPage
) {
isScrollInProgress = listState.isScrollInProgress
}
} }
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
DefaultTransition(visible = !state.isLoading && !categoryIsLoading) { DefaultTransition(visible = !state.isLoading) {
LazyVerticalGrid( LazyVerticalGrid(
columns = GridCells.Adaptive(120.dp), columns = GridCells.Adaptive(120.dp),
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
state = listState,
contentPadding = PaddingValues(8.dp) contentPadding = PaddingValues(8.dp)
) { ) {
items( items(
categorizedBooks, books,
key = { it.first.id ?: UUID.randomUUID() } key = { it.first.id }
) { ) {
LibraryBookItem( LibraryBookItem(
book = it, book = it,
@ -339,7 +335,7 @@ fun LibraryScreen(
navigator.navigate( navigator.navigate(
Screen.BOOK_INFO, Screen.BOOK_INFO,
false, false,
Argument("book", it.first) Argument("book", it.first.id)
) )
} }
}, },
@ -364,8 +360,7 @@ fun LibraryScreen(
AnimatedVisibility( AnimatedVisibility(
visible = !state.isLoading visible = !state.isLoading
&& !state.isRefreshing && !state.isRefreshing
&& categorizedBooks.isEmpty() && books.isEmpty(),
&& !categoryIsLoading,
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
enter = Transitions.DefaultTransitionIn, enter = Transitions.DefaultTransitionIn,
exit = fadeOut(tween(0)) exit = fadeOut(tween(0))
@ -379,6 +374,8 @@ fun LibraryScreen(
navigator.navigate(Screen.BROWSE, false) navigator.navigate(Screen.BROWSE, false)
} }
} }
}
}
PullRefreshIndicator( PullRefreshIndicator(
state.isRefreshing, state.isRefreshing,
@ -389,8 +386,6 @@ fun LibraryScreen(
) )
} }
} }
}
}
val activity = LocalContext.current as ComponentActivity val activity = LocalContext.current as ComponentActivity
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()

View file

@ -1,7 +1,5 @@
package ua.acclorite.book_story.presentation.screens.library.components package ua.acclorite.book_story.presentation.screens.library.components
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
@ -23,24 +21,21 @@ import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.util.Selected import ua.acclorite.book_story.domain.util.Selected
import ua.acclorite.book_story.presentation.components.CustomCoverImage
import ua.acclorite.book_story.presentation.data.removeDigits import ua.acclorite.book_story.presentation.data.removeDigits
import ua.acclorite.book_story.presentation.data.removeTrailingZero import ua.acclorite.book_story.presentation.data.removeTrailingZero
import ua.acclorite.book_story.presentation.ui.elevation
/** /**
* Library list element item. * Library list element item.
@ -54,10 +49,18 @@ fun LibraryBookItem(
onLongClick: () -> Unit, onLongClick: () -> Unit,
onButtonClick: () -> Unit onButtonClick: () -> Unit
) { ) {
val backgroundColor = if (book.second) MaterialTheme.colorScheme.primary val primaryColor = MaterialTheme.colorScheme.primary
val onPrimaryColor = MaterialTheme.colorScheme.onPrimary
val onSurfaceColor = MaterialTheme.colorScheme.onSurface
val backgroundColor = remember(book.second) {
if (book.second) primaryColor
else Color.Transparent else Color.Transparent
val fontColor = if (book.second) MaterialTheme.colorScheme.onPrimary }
else MaterialTheme.colorScheme.onSurface val fontColor = remember(book.second) {
if (book.second) onPrimaryColor
else onSurfaceColor
}
val progress = remember(book.first) { val progress = remember(book.first) {
"${ "${
@ -68,22 +71,11 @@ fun LibraryBookItem(
}%" }%"
} }
val animatedBackgroundColor by animateColorAsState(
targetValue = backgroundColor,
tween(300),
label = "Background animation"
)
val animatedFontColor by animateColorAsState(
targetValue = fontColor,
tween(300),
label = "Font color animation"
)
Column( Column(
modifier modifier
.padding(3.dp) .padding(3.dp)
.background( .background(
animatedBackgroundColor, backgroundColor,
MaterialTheme.shapes.large MaterialTheme.shapes.large
) )
.padding(3.dp) .padding(3.dp)
@ -94,7 +86,7 @@ fun LibraryBookItem(
.aspectRatio(1f / 1.5f) .aspectRatio(1f / 1.5f)
.fillMaxWidth() .fillMaxWidth()
.clip(MaterialTheme.shapes.medium) .clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.elevation()) .background(MaterialTheme.colorScheme.surfaceContainerLow)
.combinedClickable( .combinedClickable(
onClick = { onClick = {
onCoverImageClick() onCoverImageClick()
@ -105,13 +97,11 @@ fun LibraryBookItem(
) )
) { ) {
if (book.first.coverImage != null) { if (book.first.coverImage != null) {
AsyncImage( CustomCoverImage(
model = book.first.coverImage, uri = book.first.coverImage!!,
contentDescription = stringResource(id = R.string.cover_image_content_desc),
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.clip(MaterialTheme.shapes.medium), .clip(MaterialTheme.shapes.medium)
contentScale = ContentScale.Crop
) )
} else { } else {
Icon( Icon(
@ -123,7 +113,7 @@ fun LibraryBookItem(
.align(Alignment.Center) .align(Alignment.Center)
.fillMaxWidth(0.7f) .fillMaxWidth(0.7f)
.aspectRatio(1f), .aspectRatio(1f),
tint = MaterialTheme.elevation(12.dp) tint = MaterialTheme.colorScheme.surfaceContainerHigh
) )
} }
@ -155,7 +145,7 @@ fun LibraryBookItem(
) { ) {
Icon( Icon(
imageVector = Icons.Filled.PlayArrow, imageVector = Icons.Filled.PlayArrow,
contentDescription = "Continue reading", contentDescription = stringResource(id = R.string.continue_reading_content_desc),
Modifier.size(20.dp) Modifier.size(20.dp)
) )
} }
@ -163,7 +153,7 @@ fun LibraryBookItem(
Spacer(modifier = Modifier.height(6.dp)) Spacer(modifier = Modifier.height(6.dp))
Text( Text(
book.first.title, book.first.title,
color = animatedFontColor, color = fontColor,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()

View file

@ -1,6 +1,6 @@
package ua.acclorite.book_story.presentation.screens.library.components package ua.acclorite.book_story.presentation.screens.library.components
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -9,15 +9,16 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Divider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScrollableTabRow import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -26,48 +27,51 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.CategorizedBooks
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.ui.elevation
/** /**
* Tab row, either scrollable or static, depends on screen width. * Tab row, either scrollable or static, depends on screen width.
*/ */
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun LibraryTabRow(viewModel: LibraryViewModel, books: List<Book>, pagerState: PagerState) { fun LibraryTabRow(
viewModel: LibraryViewModel,
books: List<CategorizedBooks>,
pagerState: PagerState
) {
val context = LocalContext.current val context = LocalContext.current
val tabItems = remember(books) { val tabItems = remember(books) {
listOf( listOf(
Pair( Pair(
context.getString(R.string.reading_tab), context.getString(R.string.reading_tab),
books.filter { it.category == Category.READING }.size books.find { it.category == Category.READING }?.books?.size ?: 0
), ),
Pair( Pair(
context.getString(R.string.already_read_tab), context.getString(R.string.already_read_tab),
books.filter { it.category == Category.ALREADY_READ }.size books.find { it.category == Category.ALREADY_READ }?.books?.size ?: 0
), ),
Pair( Pair(
context.getString(R.string.planning_tab), context.getString(R.string.planning_tab),
books.filter { it.category == Category.PLANNING }.size books.find { it.category == Category.PLANNING }?.books?.size ?: 0
), ),
Pair( Pair(
context.getString(R.string.dropped_tab), context.getString(R.string.dropped_tab),
books.filter { it.category == Category.DROPPED }.size books.find { it.category == Category.DROPPED }?.books?.size ?: 0
) )
) )
} }
Box(Modifier.fillMaxWidth()) { Box(Modifier.fillMaxWidth()) {
Divider( HorizontalDivider(
modifier = Modifier modifier = Modifier
.align(Alignment.BottomCenter) .align(Alignment.BottomCenter)
.fillMaxWidth(), .fillMaxWidth(),
color = MaterialTheme.colorScheme.surfaceVariant color = MaterialTheme.colorScheme.surfaceContainerHighest
) )
ScrollableTabRow( PrimaryScrollableTabRow(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 0.5.dp), .padding(bottom = 0.5.dp),
@ -76,20 +80,19 @@ fun LibraryTabRow(viewModel: LibraryViewModel, books: List<Book>, pagerState: Pa
edgePadding = 0.dp, edgePadding = 0.dp,
divider = {}, divider = {},
indicator = { tabPositions -> indicator = { tabPositions ->
TabRowDefaults.Indicator( if (pagerState.currentPage < tabPositions.size) {
val width by animateDpAsState(
targetValue = tabPositions[pagerState.currentPage].contentWidth,
label = ""
)
TabRowDefaults.PrimaryIndicator(
Modifier Modifier
.tabIndicatorOffset(tabPositions[pagerState.currentPage]) .tabIndicatorOffset(tabPositions[pagerState.currentPage]),
.padding(horizontal = 16.dp) width = width
.padding(end = 5.5.dp)
.clip(
RoundedCornerShape(
100
)
),
color = MaterialTheme.colorScheme.primary,
height = 3.dp
) )
} }
}
) { ) {
tabItems.forEachIndexed { index, tabItem -> tabItems.forEachIndexed { index, tabItem ->
Tab( Tab(
@ -112,7 +115,7 @@ fun LibraryTabRow(viewModel: LibraryViewModel, books: List<Book>, pagerState: Pa
text = tabItem.second.toString(), text = tabItem.second.toString(),
modifier = Modifier modifier = Modifier
.clip(MaterialTheme.shapes.medium) .clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.elevation(6.dp)) .background(MaterialTheme.colorScheme.surfaceContainer)
.padding(horizontal = 8.dp, vertical = 4.dp), .padding(horizontal = 8.dp, vertical = 4.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium

View file

@ -1,7 +1,6 @@
package ua.acclorite.book_story.presentation.screens.library.components.dialog package ua.acclorite.book_story.presentation.screens.library.components.dialog
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.PagerState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -23,7 +22,6 @@ import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewMode
/** /**
* Move dialog. Moves all selected books to the selected category. * Move dialog. Moves all selected books to the selected category.
*/ */
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun LibraryMoveDialog( fun LibraryMoveDialog(
viewModel: LibraryViewModel, viewModel: LibraryViewModel,

View file

@ -1,8 +1,5 @@
@file:OptIn(ExperimentalFoundationApi::class)
package ua.acclorite.book_story.presentation.screens.library.data package ua.acclorite.book_story.presentation.screens.library.data
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.PagerState
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
@ -13,6 +10,7 @@ import ua.acclorite.book_story.presentation.data.Navigator
sealed class LibraryEvent { sealed class LibraryEvent {
data object OnRefreshList : LibraryEvent() data object OnRefreshList : LibraryEvent()
data object OnLoadList : LibraryEvent() data object OnLoadList : LibraryEvent()
data class OnRecalculateCategories(val books: List<Pair<Book, Selected>>) : LibraryEvent()
data class OnScrollToPage(val index: Int, val pagerState: PagerState) : LibraryEvent() data class OnScrollToPage(val index: Int, val pagerState: PagerState) : LibraryEvent()
data class OnUpdateCurrentPage(val page: Int) : LibraryEvent() data class OnUpdateCurrentPage(val page: Int) : LibraryEvent()
data object OnSearchShowHide : LibraryEvent() data object OnSearchShowHide : LibraryEvent()

View file

@ -2,12 +2,15 @@ package ua.acclorite.book_story.presentation.screens.library.data
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.CategorizedBooks
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.util.Selected import ua.acclorite.book_story.domain.util.Selected
@Immutable @Immutable
data class LibraryState( data class LibraryState(
val books: List<Pair<Book, Selected>> = emptyList(), val books: List<Pair<Book, Selected>> = emptyList(),
val categorizedBooks: List<CategorizedBooks> = emptyList(),
val isLoading: Boolean = true, val isLoading: Boolean = true,
val isRefreshing: Boolean = false, val isRefreshing: Boolean = false,

View file

@ -1,6 +1,5 @@
package ua.acclorite.book_story.presentation.screens.library.data package ua.acclorite.book_story.presentation.screens.library.data
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -11,19 +10,19 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import ua.acclorite.book_story.domain.model.CategorizedBooks
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.model.History import ua.acclorite.book_story.domain.model.History
import ua.acclorite.book_story.domain.use_case.DeleteBooks import ua.acclorite.book_story.domain.use_case.DeleteBooks
import ua.acclorite.book_story.domain.use_case.GetBooks import ua.acclorite.book_story.domain.use_case.GetBooks
import ua.acclorite.book_story.domain.use_case.InsertHistory import ua.acclorite.book_story.domain.use_case.InsertHistory
import ua.acclorite.book_story.domain.use_case.UpdateBooks import ua.acclorite.book_story.domain.use_case.UpdateBooks
import ua.acclorite.book_story.domain.util.Resource
import ua.acclorite.book_story.presentation.data.Argument import ua.acclorite.book_story.presentation.data.Argument
import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.data.Screen
import java.util.Date import java.util.Date
import javax.inject.Inject import javax.inject.Inject
@OptIn(ExperimentalFoundationApi::class)
@HiltViewModel @HiltViewModel
class LibraryViewModel @Inject constructor( class LibraryViewModel @Inject constructor(
private val getBooks: GetBooks, private val getBooks: GetBooks,
@ -87,7 +86,9 @@ class LibraryViewModel @Inject constructor(
) )
} }
yield()
getBooksFromDatabase("") getBooksFromDatabase("")
delay(500) delay(500)
_state.update { _state.update {
it.copy( it.copy(
@ -152,6 +153,7 @@ class LibraryViewModel @Inject constructor(
job?.cancel() job?.cancel()
job = viewModelScope.launch(Dispatchers.IO) { job = viewModelScope.launch(Dispatchers.IO) {
delay(500) delay(500)
yield()
getBooksFromDatabase() getBooksFromDatabase()
} }
} }
@ -168,6 +170,7 @@ class LibraryViewModel @Inject constructor(
} }
} }
onEvent(LibraryEvent.OnRecalculateCategories(editedList))
_state.update { _state.update {
it.copy( it.copy(
books = editedList, books = editedList,
@ -186,6 +189,7 @@ class LibraryViewModel @Inject constructor(
hasSelectedItems = false hasSelectedItems = false
) )
} }
onEvent(LibraryEvent.OnRecalculateCategories(_state.value.books))
} }
} }
@ -289,6 +293,7 @@ class LibraryViewModel @Inject constructor(
} }
} }
onEvent(LibraryEvent.OnRecalculateCategories(books))
_state.update { _state.update {
it.copy( it.copy(
books = books books = books
@ -299,14 +304,13 @@ class LibraryViewModel @Inject constructor(
is LibraryEvent.OnNavigateToReaderScreen -> { is LibraryEvent.OnNavigateToReaderScreen -> {
viewModelScope.launch { viewModelScope.launch {
event.book.id?.let { event.book.id.let {
insertHistory.execute( insertHistory.execute(
listOf( listOf(
History( History(
null, bookId = it,
it, book = null,
null, time = Date().time
Date().time
) )
) )
) )
@ -315,35 +319,51 @@ class LibraryViewModel @Inject constructor(
Screen.READER, Screen.READER,
false, false,
Argument( Argument(
"book", event.book "book", event.book.id
) )
) )
} }
} }
is LibraryEvent.OnRecalculateCategories -> {
val categorizedBooks = event.books.groupBy {
it.first.category
}.toList().map {
CategorizedBooks(
it.first,
it.second
)
}
_state.update {
it.copy(
categorizedBooks = categorizedBooks
)
}
}
} }
} }
private suspend fun getBooksFromDatabase( private suspend fun getBooksFromDatabase(
query: String = if (_state.value.showSearch) _state.value.searchQuery else "" query: String = if (_state.value.showSearch) _state.value.searchQuery else ""
) { ) {
getBooks.execute(query).collect { result -> val books = getBooks.execute(query).map { book -> Pair(book, false) }
when (result) { val categorizedBooks = books.groupBy {
is Resource.Success -> { it.first.category
}.toList().map {
CategorizedBooks(
it.first,
it.second
)
}
_state.update { _state.update {
it.copy( it.copy(
books = result.data?.map { book -> Pair(book, false) } books = books,
?: emptyList(), categorizedBooks = categorizedBooks,
isLoading = false isLoading = false
) )
} }
} }
is Resource.Loading -> Unit
is Resource.Error -> Unit
}
}
}
} }

View file

@ -21,7 +21,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.selection.DisableSelection import androidx.compose.foundation.text.selection.DisableSelection
import androidx.compose.material.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -71,7 +71,6 @@ import ua.acclorite.book_story.presentation.screens.reader.components.app_bar.Re
import ua.acclorite.book_story.presentation.screens.reader.components.settings_bottom_sheet.ReaderSettingsBottomSheet import ua.acclorite.book_story.presentation.screens.reader.components.settings_bottom_sheet.ReaderSettingsBottomSheet
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
import ua.acclorite.book_story.presentation.ui.elevation
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@ -84,7 +83,7 @@ fun ReaderScreen(
navigator: Navigator navigator: Navigator
) { ) {
val context = LocalContext.current as ComponentActivity val context = LocalContext.current as ComponentActivity
val systemBarsColor = MaterialTheme.elevation(20.dp).copy(0.85f) val systemBarsColor = MaterialTheme.colorScheme.surfaceContainerHighest.copy(0.85f)
var loading by remember { mutableStateOf(true) } var loading by remember { mutableStateOf(true) }
val listState = rememberLazyListState() val listState = rememberLazyListState()
@ -100,7 +99,7 @@ fun ReaderScreen(
): Offset { ): Offset {
val velocity = consumed.y val velocity = consumed.y
if ((velocity > 70 || velocity < -70) && state.showMenu) { if ((velocity > 70 || velocity < -70) && state.showMenu && !state.lockMenu) {
viewModel.onEvent(ReaderEvent.OnShowHideMenu(context = context)) viewModel.onEvent(ReaderEvent.OnShowHideMenu(context = context))
} }
@ -140,7 +139,7 @@ fun ReaderScreen(
snapshotFlow { snapshotFlow {
listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset
}.debounce(500).collectLatest { items -> }.debounce(500).collectLatest { items ->
if (!loading) { if (!loading && state.book.text.isNotEmpty() && listState.layoutInfo.totalItemsCount > 0) {
val lastVisibleItemIndex = listState.layoutInfo.visibleItemsInfo.last().index val lastVisibleItemIndex = listState.layoutInfo.visibleItemsInfo.last().index
val progress = if (items.first > 0) { val progress = if (items.first > 0) {
@ -386,8 +385,7 @@ fun ReaderScreen(
LinearProgressIndicator( LinearProgressIndicator(
modifier = Modifier.fillMaxWidth(0.55f), modifier = Modifier.fillMaxWidth(0.55f),
strokeCap = StrokeCap.Round, strokeCap = StrokeCap.Round,
color = MaterialTheme.colorScheme.primary, trackColor = MaterialTheme.colorScheme.secondaryContainer.copy(0.7f)
backgroundColor = MaterialTheme.elevation(2.dp)
) )
} }
} }

View file

@ -21,6 +21,7 @@ import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -55,6 +56,13 @@ fun ReaderEndItem(
val state by viewModel.state.collectAsState() val state by viewModel.state.collectAsState()
val context = LocalContext.current as ComponentActivity val context = LocalContext.current as ComponentActivity
val buttonText = remember {
context.getString(
if (state.book.category != Category.ALREADY_READ) R.string.move_to_read
else R.string.back_to_library
)
}
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -133,10 +141,7 @@ fun ReaderEndItem(
) )
) { ) {
Text( Text(
stringResource( buttonText,
if (state.book.category != Category.ALREADY_READ) R.string.move_to_read
else R.string.back_to_library
),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis

View file

@ -28,14 +28,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.CustomCoverImage
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
import ua.acclorite.book_story.presentation.ui.elevation
/** /**
* Reader start item. Displays at the beginning of the book. * Reader start item. Displays at the beginning of the book.
@ -61,16 +59,14 @@ fun ReaderStartItem(viewModel: ReaderViewModel) {
.height(135.dp) .height(135.dp)
.width(90.dp) .width(90.dp)
.clip(RoundedCornerShape(10.dp)) .clip(RoundedCornerShape(10.dp))
.background(MaterialTheme.elevation()) .background(MaterialTheme.colorScheme.surfaceContainerLow)
) { ) {
if (state.book.coverImage != null) { if (state.book.coverImage != null) {
AsyncImage( CustomCoverImage(
model = state.book.coverImage!!, uri = state.book.coverImage!!,
contentDescription = stringResource(id = R.string.cover_image_content_desc),
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.clip(RoundedCornerShape(10.dp)), .clip(RoundedCornerShape(10.dp))
contentScale = ContentScale.Crop
) )
} else { } else {
Icon( Icon(
@ -80,7 +76,7 @@ fun ReaderStartItem(viewModel: ReaderViewModel) {
.align(Alignment.Center) .align(Alignment.Center)
.fillMaxWidth(0.7f) .fillMaxWidth(0.7f)
.aspectRatio(1f), .aspectRatio(1f),
tint = MaterialTheme.elevation(12.dp) tint = MaterialTheme.colorScheme.surfaceContainerHigh
) )
} }
} }

View file

@ -4,7 +4,9 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
@ -74,9 +76,12 @@ fun ReaderBottomBar(
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleLarge style = MaterialTheme.typography.titleLarge
) )
Spacer(modifier = Modifier.height(3.dp))
Slider( Slider(
value = state.book.progress, value = state.book.progress,
enabled = !state.lockMenu,
onValueChange = { onValueChange = {
if (listState.layoutInfo.totalItemsCount > 0) {
viewModel.onEvent(ReaderEvent.OnScroll(listState, it)) viewModel.onEvent(ReaderEvent.OnScroll(listState, it))
viewModel.onEvent( viewModel.onEvent(
ReaderEvent.OnChangeProgress( ReaderEvent.OnChangeProgress(
@ -90,19 +95,15 @@ fun ReaderBottomBar(
} }
) )
) )
}
}, },
colors = SliderDefaults.colors( colors = SliderDefaults.colors(
activeTrackColor = MaterialTheme.colorScheme.primary, inactiveTrackColor = MaterialTheme.colorScheme.secondary.copy(0.15f),
thumbColor = MaterialTheme.colorScheme.primary, disabledActiveTrackColor = MaterialTheme.colorScheme.primary,
inactiveTrackColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(0.15f), disabledThumbColor = MaterialTheme.colorScheme.primary,
disabledActiveTickColor = Color.Transparent, disabledInactiveTrackColor = MaterialTheme.colorScheme.secondary.copy(0.15f),
disabledActiveTrackColor = Color.Transparent,
disabledInactiveTickColor = Color.Transparent,
disabledInactiveTrackColor = Color.Transparent,
disabledThumbColor = Color.Transparent,
activeTickColor = Color.Transparent,
inactiveTickColor = Color.Transparent
) )
) )
Spacer(modifier = Modifier.height(5.dp))
} }
} }

View file

@ -69,7 +69,7 @@ fun ReaderTopBar(
navigationIcon = { navigationIcon = {
CustomIconButton( CustomIconButton(
icon = Icons.AutoMirrored.Filled.ArrowBack, icon = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.go_back_content_desc), contentDescription = R.string.go_back_content_desc,
disableOnClick = true disableOnClick = true
) { ) {
viewModel.onEvent( viewModel.onEvent(
@ -99,6 +99,7 @@ fun ReaderTopBar(
maxLines = 1, maxLines = 1,
modifier = Modifier modifier = Modifier
.clickable( .clickable(
enabled = !state.lockMenu,
interactionSource = null, interactionSource = null,
indication = null, indication = null,
onClick = { onClick = {
@ -138,8 +139,9 @@ fun ReaderTopBar(
actions = { actions = {
CustomIconButton( CustomIconButton(
icon = Icons.Default.Settings, icon = Icons.Default.Settings,
contentDescription = stringResource(id = R.string.open_reader_settings_content_desc), contentDescription = R.string.open_reader_settings_content_desc,
disableOnClick = false disableOnClick = false,
enabled = !state.lockMenu
) { ) {
viewModel.onEvent(ReaderEvent.OnShowHideSettingsBottomSheet) viewModel.onEvent(ReaderEvent.OnShowHideSettingsBottomSheet)
} }

View file

@ -4,7 +4,7 @@ import androidx.activity.ComponentActivity
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.asPaddingValues
@ -27,6 +27,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@ -34,7 +35,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.ChipItem import ua.acclorite.book_story.domain.model.ButtonItem
import ua.acclorite.book_story.domain.util.Constants import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainEvent
@ -45,14 +46,12 @@ import ua.acclorite.book_story.presentation.screens.settings.components.Checkbox
import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle
import ua.acclorite.book_story.presentation.screens.settings.components.ColorPickerWithTitle import ua.acclorite.book_story.presentation.screens.settings.components.ColorPickerWithTitle
import ua.acclorite.book_story.presentation.screens.settings.components.SliderWithTitle import ua.acclorite.book_story.presentation.screens.settings.components.SliderWithTitle
import ua.acclorite.book_story.presentation.ui.ElevationDefaults
import ua.acclorite.book_story.presentation.ui.elevation
/** /**
* Settings bottom sheet. Has General and Colors categories. * Settings bottom sheet. Has General and Colors categories.
*/ */
@OptIn( @OptIn(
ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class ExperimentalMaterial3Api::class
) )
@Composable @Composable
fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderViewModel) { fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderViewModel) {
@ -98,10 +97,12 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
) )
} }
Box(modifier = Modifier.fillMaxSize()) {
ModalBottomSheet( ModalBottomSheet(
scrimColor = animatedScrimColor, scrimColor = animatedScrimColor,
dragHandle = {}, dragHandle = {},
modifier = Modifier modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight(animatedHeight), .fillMaxHeight(animatedHeight),
onDismissRequest = { onDismissRequest = {
@ -109,12 +110,12 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
}, },
sheetState = rememberModalBottomSheetState(true), sheetState = rememberModalBottomSheetState(true),
windowInsets = WindowInsets(0, 0, 0, 0), windowInsets = WindowInsets(0, 0, 0, 0),
containerColor = MaterialTheme.elevation(ElevationDefaults.BottomSheet) containerColor = MaterialTheme.colorScheme.surfaceContainerLow
) { ) {
ReaderSettingsBottomSheetTabRow(viewModel = viewModel, pagerState = pagerState) ReaderSettingsBottomSheetTabRow(viewModel = viewModel, pagerState = pagerState)
HorizontalPager(state = pagerState, modifier = Modifier.fillMaxSize()) { page -> HorizontalPager(state = pagerState) { page ->
LazyColumn(Modifier.fillMaxSize()) { LazyColumn {
if (page == 0) { if (page == 0) {
item { item {
Spacer(modifier = Modifier.height(22.dp)) Spacer(modifier = Modifier.height(22.dp))
@ -133,7 +134,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
title = stringResource(id = R.string.font_family_option), title = stringResource(id = R.string.font_family_option),
chips = Constants.FONTS chips = Constants.FONTS
.map { .map {
ChipItem( ButtonItem(
it.id, it.id,
it.fontName.asString(), it.fontName.asString(),
MaterialTheme.typography.labelLarge.copy( MaterialTheme.typography.labelLarge.copy(
@ -151,7 +152,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
ChipsWithTitle( ChipsWithTitle(
title = stringResource(id = R.string.font_style_option), title = stringResource(id = R.string.font_style_option),
chips = listOf( chips = listOf(
ChipItem( ButtonItem(
"normal", "normal",
stringResource(id = R.string.font_style_normal), stringResource(id = R.string.font_style_normal),
MaterialTheme.typography.labelLarge.copy( MaterialTheme.typography.labelLarge.copy(
@ -160,7 +161,7 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
), ),
!state.isItalic!! !state.isItalic!!
), ),
ChipItem( ButtonItem(
"italic", "italic",
stringResource(id = R.string.font_style_italic), stringResource(id = R.string.font_style_italic),
MaterialTheme.typography.labelLarge.copy( MaterialTheme.typography.labelLarge.copy(
@ -287,6 +288,8 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
} }
} }
} }
}
} }

View file

@ -1,22 +1,16 @@
package ua.acclorite.book_story.presentation.screens.reader.components.settings_bottom_sheet package ua.acclorite.book_story.presentation.screens.reader.components.settings_bottom_sheet
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.TabRowDefaults
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent import ua.acclorite.book_story.presentation.screens.reader.data.ReaderEvent
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
@ -24,7 +18,7 @@ import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
/** /**
* Settings bottom sheet tab row. It is used to switch between categories. * Settings bottom sheet tab row. It is used to switch between categories.
*/ */
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ReaderSettingsBottomSheetTabRow(viewModel: ReaderViewModel, pagerState: PagerState) { fun ReaderSettingsBottomSheetTabRow(viewModel: ReaderViewModel, pagerState: PagerState) {
val tabItems = listOf( val tabItems = listOf(
@ -32,27 +26,11 @@ fun ReaderSettingsBottomSheetTabRow(viewModel: ReaderViewModel, pagerState: Page
stringResource(id = R.string.color_tab) stringResource(id = R.string.color_tab)
) )
TabRow( PrimaryTabRow(
modifier = Modifier modifier = Modifier
.fillMaxWidth(), .fillMaxWidth(),
selectedTabIndex = pagerState.currentPage, selectedTabIndex = pagerState.currentPage,
divider = { containerColor = Color.Transparent
Divider(color = MaterialTheme.colorScheme.surfaceVariant)
},
indicator = { tabPositions ->
TabRowDefaults.Indicator(
Modifier
.tabIndicatorOffset(tabPositions[pagerState.currentPage])
.padding(horizontal = 64.dp)
.clip(
RoundedCornerShape(
topStartPercent = 100,
topEndPercent = 100
)
),
color = MaterialTheme.colorScheme.primary
)
}
) { ) {
tabItems.forEachIndexed { index, tabItem -> tabItems.forEachIndexed { index, tabItem ->
Tab( Tab(
@ -62,7 +40,7 @@ fun ReaderSettingsBottomSheetTabRow(viewModel: ReaderViewModel, pagerState: Page
viewModel.onEvent(ReaderEvent.OnScrollToSettingsPage(index, pagerState)) viewModel.onEvent(ReaderEvent.OnScrollToSettingsPage(index, pagerState))
}, },
selectedContentColor = MaterialTheme.colorScheme.primary, selectedContentColor = MaterialTheme.colorScheme.primary,
unselectedContentColor = MaterialTheme.colorScheme.onSurface, unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
text = { text = {
Text( Text(
tabItem, tabItem,

View file

@ -1,9 +1,6 @@
@file:OptIn(ExperimentalFoundationApi::class)
package ua.acclorite.book_story.presentation.screens.reader.data package ua.acclorite.book_story.presentation.screens.reader.data
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.PagerState
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book

View file

@ -9,7 +9,10 @@ import ua.acclorite.book_story.domain.util.UIText
data class ReaderState( data class ReaderState(
val book: Book = Constants.EMPTY_BOOK, val book: Book = Constants.EMPTY_BOOK,
val errorMessage: UIText? = null, val errorMessage: UIText? = null,
val showMenu: Boolean = false, val showMenu: Boolean = false,
val lockMenu: Boolean = false,
val showSettingsBottomSheet: Boolean = false, val showSettingsBottomSheet: Boolean = false,
val currentPage: Int = 0, val currentPage: Int = 0,
) )

View file

@ -2,8 +2,8 @@ package ua.acclorite.book_story.presentation.screens.reader.data
import android.app.SearchManager import android.app.SearchManager
import android.content.Intent import android.content.Intent
import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
@ -22,7 +22,7 @@ import kotlinx.coroutines.launch
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.Book import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.model.Category import ua.acclorite.book_story.domain.model.Category
import ua.acclorite.book_story.domain.model.StringWithId import ua.acclorite.book_story.domain.use_case.GetBooksById
import ua.acclorite.book_story.domain.use_case.GetLatestHistory import ua.acclorite.book_story.domain.use_case.GetLatestHistory
import ua.acclorite.book_story.domain.use_case.GetText import ua.acclorite.book_story.domain.use_case.GetText
import ua.acclorite.book_story.domain.use_case.UpdateBooks import ua.acclorite.book_story.domain.use_case.UpdateBooks
@ -33,12 +33,12 @@ import ua.acclorite.book_story.presentation.data.Screen
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.roundToInt import kotlin.math.roundToInt
@OptIn(ExperimentalFoundationApi::class)
@HiltViewModel @HiltViewModel
class ReaderViewModel @Inject constructor( class ReaderViewModel @Inject constructor(
private val updateBooks: UpdateBooks, private val updateBooks: UpdateBooks,
private val getText: GetText, private val getText: GetText,
private val getLatestHistory: GetLatestHistory private val getLatestHistory: GetLatestHistory,
private val getBooksById: GetBooksById
) : ViewModel() { ) : ViewModel() {
private val _state = MutableStateFlow(ReaderState()) private val _state = MutableStateFlow(ReaderState())
@ -57,21 +57,44 @@ class ReaderViewModel @Inject constructor(
is ReaderEvent.OnLoadText -> { is ReaderEvent.OnLoadText -> {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val text = getText.execute(_state.value.book.id!!) if (
_state.value.book.text.isEmpty() ||
_state.value.book.letters < 1 ||
_state.value.book.words < 1
) {
val text = getText.execute(_state.value.book.textPath)
if (text.isBlank()) { if (text.isEmpty()) {
event.onTextIsEmpty() event.onTextIsEmpty()
} }
val letters = text val textAsLine = text.joinToString(
separator = "\n",
transform = {
it.line
}
)
val letters = textAsLine
.replace("\n", "") .replace("\n", "")
.length .length
val words = text val words = textAsLine
.replace("\n", " ") .replace("\n", " ")
.split("\\s+".toRegex()) .split("\\s+".toRegex())
.size .size
val history = _state.value.book.id?.let { _state.update {
it.copy(
book = it.book.copy(
text = text,
letters = letters,
words = words
)
)
}
}
val history = _state.value.book.id.let {
getLatestHistory.execute( getLatestHistory.execute(
it it
) )
@ -80,12 +103,7 @@ class ReaderViewModel @Inject constructor(
_state.update { _state.update {
it.copy( it.copy(
book = it.book.copy( book = it.book.copy(
text = text
.split("\n")
.map { line -> StringWithId(line.trim()) },
lastOpened = history?.time, lastOpened = history?.time,
letters = letters,
words = words
) )
) )
} }
@ -114,6 +132,10 @@ class ReaderViewModel @Inject constructor(
loaded = true loaded = true
break break
} catch (e: Exception) { } catch (e: Exception) {
Log.w(
"READER",
"Couldn't scroll to desired index and offset"
)
delay(100) delay(100)
} }
} }
@ -133,6 +155,10 @@ class ReaderViewModel @Inject constructor(
} }
is ReaderEvent.OnShowHideMenu -> { is ReaderEvent.OnShowHideMenu -> {
if (_state.value.lockMenu) {
return
}
val shouldShow = event.show ?: !_state.value.showMenu val shouldShow = event.show ?: !_state.value.showMenu
val insetsController = WindowCompat.getInsetsController( val insetsController = WindowCompat.getInsetsController(
@ -160,11 +186,18 @@ class ReaderViewModel @Inject constructor(
is ReaderEvent.OnGoBack -> { is ReaderEvent.OnGoBack -> {
viewModelScope.launch { viewModelScope.launch {
_state.update {
it.copy(
lockMenu = true
)
}
val insetsController = WindowCompat.getInsetsController( val insetsController = WindowCompat.getInsetsController(
event.context.window, event.context.window,
event.context.window.decorView event.context.window.decorView
) )
if (event.listState.layoutInfo.totalItemsCount > 0) {
val firstVisibleItemIndex = event.listState.firstVisibleItemIndex val firstVisibleItemIndex = event.listState.firstVisibleItemIndex
val firstVisibleItemOffset = event.listState.firstVisibleItemScrollOffset val firstVisibleItemOffset = event.listState.firstVisibleItemScrollOffset
val lastVisibleItemIndex = event.listState.layoutInfo val lastVisibleItemIndex = event.listState.layoutInfo
@ -190,12 +223,13 @@ class ReaderViewModel @Inject constructor(
) )
) )
} }
}
updateBooks.execute( updateBooks.execute(
listOf(_state.value.book) listOf(_state.value.book)
) )
event.navigator.putArgument( event.navigator.putArgument(
Argument("book", _state.value.book) Argument("book", _state.value.book.id)
) )
event.refreshList(_state.value.book) event.refreshList(_state.value.book)
@ -232,7 +266,7 @@ class ReaderViewModel @Inject constructor(
event.navigator.putArgument( event.navigator.putArgument(
Argument( Argument(
"book", "book",
_state.value.book _state.value.book.id
) )
) )
event.refreshList(_state.value.book) event.refreshList(_state.value.book)
@ -355,9 +389,16 @@ class ReaderViewModel @Inject constructor(
onLoaded: () -> Unit onLoaded: () -> Unit
) { ) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val book = navigator.retrieveArgument("book") as? Book val bookId = navigator.retrieveArgument("book") as? Int
if (book == null) { if (bookId == null) {
navigator.navigateBack()
return@launch
}
val book = getBooksById.execute(listOf(bookId))
if (book.isEmpty()) {
navigator.navigateBack() navigator.navigateBack()
return@launch return@launch
} }
@ -367,7 +408,7 @@ class ReaderViewModel @Inject constructor(
} }
_state.update { _state.update {
ReaderState(book = book) ReaderState(book = book.first())
} }
onEvent( onEvent(

View file

@ -29,7 +29,6 @@ import ua.acclorite.book_story.presentation.components.GoBackButton
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.data.Screen import ua.acclorite.book_story.presentation.data.Screen
import ua.acclorite.book_story.presentation.screens.settings.components.SettingsCategoryItem import ua.acclorite.book_story.presentation.screens.settings.components.SettingsCategoryItem
import ua.acclorite.book_story.presentation.ui.elevation
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -60,7 +59,7 @@ fun SettingsScreen(
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
colors = TopAppBarDefaults.largeTopAppBarColors( colors = TopAppBarDefaults.largeTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.elevation() scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer
) )
) )
} }

View file

@ -1,6 +1,5 @@
package ua.acclorite.book_story.presentation.screens.settings.components package ua.acclorite.book_story.presentation.screens.settings.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.ExperimentalLayoutApi
@ -9,30 +8,26 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.ChipDefaults import androidx.compose.material3.FilterChip
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.domain.model.ChipItem import ua.acclorite.book_story.domain.model.ButtonItem
import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.components.CategoryTitle
/** /**
* Chips with title. Use list of [ChipItem]s to display chips. * Chips with title. Use list of [ButtonItem]s to display chips.
*/ */
@OptIn(ExperimentalMaterialApi::class, ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun ChipsWithTitle( fun ChipsWithTitle(
title: String, title: String,
chips: List<ChipItem>, chips: List<ButtonItem>,
horizontalPadding: Dp = 18.dp, horizontalPadding: Dp = 18.dp,
verticalPadding: Dp = 8.dp, verticalPadding: Dp = 8.dp,
onClick: (ChipItem) -> Unit onClick: (ButtonItem) -> Unit
) { ) {
Column( Column(
Modifier Modifier
@ -50,29 +45,15 @@ fun ChipsWithTitle(
FilterChip( FilterChip(
modifier = Modifier.height(36.dp), modifier = Modifier.height(36.dp),
selected = item.selected, selected = item.selected,
enabled = !item.selected, label = {
shape = MaterialTheme.shapes.small,
border = if (!item.selected) BorderStroke(
1.dp,
MaterialTheme.colorScheme.outline
) else null,
colors = ChipDefaults.filterChipColors(
backgroundColor = Color.Transparent,
disabledBackgroundColor = MaterialTheme.colorScheme.secondaryContainer
),
onClick = { onClick(item) },
) {
Text( Text(
item.title, item.title,
style = item.textStyle, style = item.textStyle,
color = if (item.selected) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.onSurfaceVariant
},
maxLines = 1 maxLines = 1
) )
} },
onClick = { onClick(item) },
)
} }
}, },
) )

View file

@ -9,8 +9,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.History import androidx.compose.material.icons.filled.History
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -27,6 +25,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.components.CustomIconButton
/** /**
* Color picker with title. * Color picker with title.
@ -49,8 +48,7 @@ fun ColorPickerWithTitle(
) { ) {
CategoryTitle( CategoryTitle(
title = title, title = title,
padding = 0.dp, padding = 0.dp
// color = MaterialTheme.colorScheme.onSurface
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
RevertibleSlider( RevertibleSlider(
@ -111,22 +109,18 @@ private fun RevertibleSlider(
horizontalPadding = 0.dp, horizontalPadding = 0.dp,
verticalPadding = 0.dp verticalPadding = 0.dp
) )
Box(modifier = Modifier.weight(0.15f)) { Box(modifier = Modifier.weight(0.15f), contentAlignment = Alignment.CenterEnd) {
IconButton( CustomIconButton(
modifier = Modifier.align(Alignment.CenterEnd), modifier = Modifier.size(28.dp),
icon = Icons.Default.History,
contentDescription = R.string.revert_content_desc,
disableOnClick = false,
enabled = initialValue != value.first, enabled = initialValue != value.first,
onClick = { onValueChange(initialValue) } color = if (initialValue == value.first) MaterialTheme.colorScheme.onSurfaceVariant
) {
Icon(
imageVector = Icons.Default.History,
contentDescription = stringResource(id = R.string.revert_content_desc),
modifier = Modifier
.size(28.dp),
tint = if (initialValue == value.first) MaterialTheme.colorScheme.onSurfaceVariant
else MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurface
) ) {
onValueChange(initialValue)
} }
} }
} }
} }

View file

@ -0,0 +1,86 @@
package ua.acclorite.book_story.presentation.screens.settings.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.domain.model.ButtonItem
import ua.acclorite.book_story.presentation.components.CategoryTitle
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SegmentedButtonWithTitle(
title: String,
buttons: List<ButtonItem>,
enabled: Boolean,
horizontalPadding: Dp = 18.dp,
verticalPadding: Dp = 8.dp,
onClick: (ButtonItem) -> Unit
) {
val colors = SegmentedButtonDefaults.colors()
Column(
Modifier
.fillMaxWidth()
.padding(horizontal = horizontalPadding, vertical = verticalPadding)
) {
CategoryTitle(title = title, padding = 0.dp)
Spacer(modifier = Modifier.height(8.dp))
SingleChoiceSegmentedButtonRow {
buttons.forEachIndexed { index, buttonItem ->
SegmentedButton(
enabled = enabled,
selected = buttonItem.selected,
onClick = { onClick(buttonItem) },
shape = when (index) {
buttons.lastIndex -> RoundedCornerShape(
topEndPercent = 100,
bottomEndPercent = 100
)
0 -> RoundedCornerShape(
topStartPercent = 100,
bottomStartPercent = 100
)
else -> RoundedCornerShape(0)
},
colors = SegmentedButtonDefaults.colors(
disabledInactiveBorderColor = colors.disabledActiveBorderColor,
disabledInactiveContentColor = colors.disabledActiveContentColor,
disabledActiveContainerColor = colors.activeContainerColor.copy(0.12f),
),
label = {
Text(
text = buttonItem.title,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
)
}
}
}
}

View file

@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -54,7 +55,11 @@ fun SliderWithTitle(
onValueChange = { onValueChange = {
onValueChange(it.roundToInt()) onValueChange(it.roundToInt())
}, },
steps = toValue - fromValue steps = toValue - fromValue,
colors = SliderDefaults.colors(
activeTickColor = MaterialTheme.colorScheme.onPrimary,
inactiveTickColor = MaterialTheme.colorScheme.onSurfaceVariant,
)
) )
} }
} }
@ -90,7 +95,11 @@ fun SliderWithTitle(
value = value.first, value = value.first,
onValueChange = { onValueChange = {
onValueChange(it) onValueChange(it)
} },
colors = SliderDefaults.colors(
activeTickColor = MaterialTheme.colorScheme.onPrimary,
inactiveTickColor = MaterialTheme.colorScheme.onSurfaceVariant,
)
) )
} }
} }

View file

@ -24,17 +24,18 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.ChipItem import ua.acclorite.book_story.domain.model.ButtonItem
import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.components.GoBackButton import ua.acclorite.book_story.presentation.components.GoBackButton
import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainEvent
import ua.acclorite.book_story.presentation.data.MainViewModel import ua.acclorite.book_story.presentation.data.MainViewModel
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle
import ua.acclorite.book_story.presentation.screens.settings.components.ColorPickerWithTitle import ua.acclorite.book_story.presentation.screens.settings.components.ColorPickerWithTitle
import ua.acclorite.book_story.presentation.screens.settings.components.SegmentedButtonWithTitle
import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.theme_switcher.AppearanceSettingsThemeSwitcher import ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.theme_switcher.AppearanceSettingsThemeSwitcher
import ua.acclorite.book_story.presentation.ui.DarkTheme import ua.acclorite.book_story.presentation.ui.DarkTheme
import ua.acclorite.book_story.presentation.ui.elevation import ua.acclorite.book_story.presentation.ui.Theme
import ua.acclorite.book_story.presentation.ui.ThemeContrast
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -68,7 +69,7 @@ fun AppearanceSettings(
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
colors = TopAppBarDefaults.largeTopAppBarColors( colors = TopAppBarDefaults.largeTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.elevation() scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer
) )
) )
} }
@ -93,10 +94,11 @@ fun AppearanceSettings(
} }
item { item {
ChipsWithTitle( SegmentedButtonWithTitle(
title = stringResource(id = R.string.dark_theme_option), title = stringResource(id = R.string.dark_theme_option),
chips = DarkTheme.entries.map { enabled = true,
ChipItem( buttons = DarkTheme.entries.map {
ButtonItem(
it.toString(), it.toString(),
when (it) { when (it) {
DarkTheme.OFF -> stringResource(id = R.string.dark_theme_off) DarkTheme.OFF -> stringResource(id = R.string.dark_theme_off)
@ -106,7 +108,7 @@ fun AppearanceSettings(
MaterialTheme.typography.labelLarge, MaterialTheme.typography.labelLarge,
it == state.darkTheme it == state.darkTheme
) )
}, }
) { ) {
mainViewModel.onEvent( mainViewModel.onEvent(
MainEvent.OnChangeDarkTheme( MainEvent.OnChangeDarkTheme(
@ -116,6 +118,31 @@ fun AppearanceSettings(
} }
} }
item {
SegmentedButtonWithTitle(
title = stringResource(id = R.string.theme_contrast_option),
enabled = state.theme != Theme.DYNAMIC,
buttons = ThemeContrast.entries.map {
ButtonItem(
it.toString(),
when (it) {
ThemeContrast.STANDARD -> stringResource(id = R.string.theme_contrast_standard)
ThemeContrast.MEDIUM -> stringResource(id = R.string.theme_contrast_medium)
ThemeContrast.HIGH -> stringResource(id = R.string.theme_contrast_high)
},
MaterialTheme.typography.labelLarge,
it == state.themeContrast
)
}
) {
mainViewModel.onEvent(
MainEvent.OnChangeThemeContrast(
it.id
)
)
}
}
item { item {
AppearanceSettingsThemeSwitcher(mainViewModel = mainViewModel) AppearanceSettingsThemeSwitcher(mainViewModel = mainViewModel)
} }

View file

@ -31,7 +31,6 @@ fun AppearanceSettingsThemeSwitcher(
mainViewModel: MainViewModel mainViewModel: MainViewModel
) { ) {
val state by mainViewModel.state.collectAsState() val state by mainViewModel.state.collectAsState()
val themes = remember { val themes = remember {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Constants.THEMES if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Constants.THEMES
else Constants.THEMES.dropWhile { it.first == Theme.DYNAMIC } else Constants.THEMES.dropWhile { it.first == Theme.DYNAMIC }
@ -48,23 +47,25 @@ fun AppearanceSettingsThemeSwitcher(
Spacer(modifier = Modifier.height(10.dp)) Spacer(modifier = Modifier.height(10.dp))
LazyRow( LazyRow(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth(),
) { ) {
itemsIndexed(themes) { index, themeEntry -> itemsIndexed(themes) { index, themeEntry ->
if (index == 0) if (index == 0) {
Spacer(modifier = Modifier.width(18.dp)) Spacer(modifier = Modifier.width(18.dp))
} else {
Spacer(modifier = Modifier.width(8.dp))
}
AppearanceSettingsThemeSwitcherItem( AppearanceSettingsThemeSwitcherItem(
theme = themeEntry, theme = themeEntry,
darkTheme = state.darkTheme!!.isDark(), darkTheme = state.darkTheme!!.isDark(),
themeContrast = state.themeContrast!!,
selected = state.theme == themeEntry.first selected = state.theme == themeEntry.first
) { ) {
mainViewModel.onEvent(MainEvent.OnChangeTheme(themeEntry.first.toString())) mainViewModel.onEvent(MainEvent.OnChangeTheme(themeEntry.first.toString()))
} }
if (index != Theme.entries.lastIndex) { if (index == themes.lastIndex) {
Spacer(modifier = Modifier.width(8.dp))
} else {
Spacer(modifier = Modifier.width(18.dp)) Spacer(modifier = Modifier.width(18.dp))
} }
} }

View file

@ -1,5 +1,10 @@
package ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.theme_switcher package ua.acclorite.book_story.presentation.screens.settings.nested.appearance.components.theme_switcher
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@ -19,21 +24,19 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBarDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.util.UIText import ua.acclorite.book_story.domain.util.UIText
import ua.acclorite.book_story.presentation.ui.Theme import ua.acclorite.book_story.presentation.ui.Theme
import ua.acclorite.book_story.presentation.ui.colorScheme import ua.acclorite.book_story.presentation.ui.ThemeContrast
import ua.acclorite.book_story.presentation.ui.animatedColorScheme
/** /**
* Theme switcher item. * Theme switcher item.
@ -42,10 +45,11 @@ import ua.acclorite.book_story.presentation.ui.colorScheme
fun AppearanceSettingsThemeSwitcherItem( fun AppearanceSettingsThemeSwitcherItem(
theme: Pair<Theme, UIText>, theme: Pair<Theme, UIText>,
darkTheme: Boolean, darkTheme: Boolean,
themeContrast: ThemeContrast,
selected: Boolean, selected: Boolean,
onClick: () -> Unit onClick: () -> Unit
) { ) {
val colorScheme = colorScheme(theme.first, darkTheme) val colorScheme = animatedColorScheme(theme.first, darkTheme, themeContrast)
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -83,16 +87,24 @@ fun AppearanceSettingsThemeSwitcherItem(
.background(colorScheme.onSurface) .background(colorScheme.onSurface)
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
AnimatedVisibility(
visible = selected,
enter = scaleIn(tween(300), initialScale = 0.5f) +
fadeIn(tween(300)),
exit = fadeOut(tween(100))
) {
Icon( Icon(
imageVector = Icons.Default.CheckCircle, imageVector = Icons.Default.CheckCircle,
contentDescription = stringResource(id = R.string.selected_content_desc), contentDescription = stringResource(id = R.string.selected_content_desc),
modifier = Modifier modifier = Modifier
.size(26.dp), .size(26.dp),
tint = tint = colorScheme.primary
if (selected) colorScheme.primary
else Color.Transparent
) )
} }
Box(modifier = Modifier.height(26.dp))
}
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Box( Box(
Modifier Modifier
@ -101,9 +113,7 @@ fun AppearanceSettingsThemeSwitcherItem(
.width(70.dp) .width(70.dp)
.clip(RoundedCornerShape(14.dp)) .clip(RoundedCornerShape(14.dp))
.background( .background(
colorScheme.surfaceColorAtElevation( colorScheme.surfaceContainer
NavigationBarDefaults.Elevation
)
) )
) )
Spacer(modifier = Modifier.height(40.dp)) Spacer(modifier = Modifier.height(40.dp))
@ -112,9 +122,7 @@ fun AppearanceSettingsThemeSwitcherItem(
.width(130.dp) .width(130.dp)
.height(40.dp) .height(40.dp)
.background( .background(
colorScheme.surfaceColorAtElevation( colorScheme.surfaceContainer
NavigationBarDefaults.Elevation
)
), ),
horizontalArrangement = Arrangement.SpaceEvenly, horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@ -141,7 +149,7 @@ fun AppearanceSettingsThemeSwitcherItem(
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = 1 maxLines = 2
) )
} }
} }

View file

@ -23,14 +23,13 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.ChipItem import ua.acclorite.book_story.domain.model.ButtonItem
import ua.acclorite.book_story.domain.util.Constants import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.presentation.components.GoBackButton import ua.acclorite.book_story.presentation.components.GoBackButton
import ua.acclorite.book_story.presentation.data.MainEvent import ua.acclorite.book_story.presentation.data.MainEvent
import ua.acclorite.book_story.presentation.data.MainViewModel import ua.acclorite.book_story.presentation.data.MainViewModel
import ua.acclorite.book_story.presentation.data.Navigator import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle
import ua.acclorite.book_story.presentation.ui.elevation
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -64,7 +63,7 @@ fun GeneralSettings(
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
colors = TopAppBarDefaults.largeTopAppBarColors( colors = TopAppBarDefaults.largeTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.elevation() scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer
) )
) )
} }
@ -82,7 +81,7 @@ fun GeneralSettings(
ChipsWithTitle( ChipsWithTitle(
title = stringResource(id = R.string.language_option), title = stringResource(id = R.string.language_option),
chips = Constants.LANGUAGES.sortedBy { it.second }.map { chips = Constants.LANGUAGES.sortedBy { it.second }.map {
ChipItem( ButtonItem(
it.first, it.first,
it.second, it.second,
MaterialTheme.typography.labelLarge, MaterialTheme.typography.labelLarge,

View file

@ -25,7 +25,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ua.acclorite.book_story.R import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.ChipItem import ua.acclorite.book_story.domain.model.ButtonItem
import ua.acclorite.book_story.domain.util.Constants import ua.acclorite.book_story.domain.util.Constants
import ua.acclorite.book_story.presentation.components.CategoryTitle import ua.acclorite.book_story.presentation.components.CategoryTitle
import ua.acclorite.book_story.presentation.components.GoBackButton import ua.acclorite.book_story.presentation.components.GoBackButton
@ -35,7 +35,6 @@ import ua.acclorite.book_story.presentation.data.Navigator
import ua.acclorite.book_story.presentation.screens.settings.components.CheckboxWithTitle import ua.acclorite.book_story.presentation.screens.settings.components.CheckboxWithTitle
import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle import ua.acclorite.book_story.presentation.screens.settings.components.ChipsWithTitle
import ua.acclorite.book_story.presentation.screens.settings.components.SliderWithTitle import ua.acclorite.book_story.presentation.screens.settings.components.SliderWithTitle
import ua.acclorite.book_story.presentation.ui.elevation
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -74,7 +73,7 @@ fun ReaderSettings(
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
colors = TopAppBarDefaults.largeTopAppBarColors( colors = TopAppBarDefaults.largeTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.elevation() scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer
) )
) )
} }
@ -102,7 +101,7 @@ fun ReaderSettings(
title = stringResource(id = R.string.font_family_option), title = stringResource(id = R.string.font_family_option),
chips = Constants.FONTS chips = Constants.FONTS
.map { .map {
ChipItem( ButtonItem(
it.id, it.id,
it.fontName.asString(), it.fontName.asString(),
MaterialTheme.typography.labelLarge.copy( MaterialTheme.typography.labelLarge.copy(
@ -120,7 +119,7 @@ fun ReaderSettings(
ChipsWithTitle( ChipsWithTitle(
title = stringResource(id = R.string.font_style_option), title = stringResource(id = R.string.font_style_option),
chips = listOf( chips = listOf(
ChipItem( ButtonItem(
"normal", "normal",
stringResource(id = R.string.font_style_normal), stringResource(id = R.string.font_style_normal),
MaterialTheme.typography.labelLarge.copy( MaterialTheme.typography.labelLarge.copy(
@ -129,7 +128,7 @@ fun ReaderSettings(
), ),
!state.isItalic!! !state.isItalic!!
), ),
ChipItem( ButtonItem(
"italic", "italic",
stringResource(id = R.string.font_style_italic), stringResource(id = R.string.font_style_italic),
MaterialTheme.typography.labelLarge.copy( MaterialTheme.typography.labelLarge.copy(

View file

@ -5,6 +5,7 @@ import androidx.compose.material3.ColorScheme
import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import ua.acclorite.book_story.presentation.ui.theme.aquaTheme import ua.acclorite.book_story.presentation.ui.theme.aquaTheme
import ua.acclorite.book_story.presentation.ui.theme.blueTheme import ua.acclorite.book_story.presentation.ui.theme.blueTheme
@ -14,6 +15,7 @@ import ua.acclorite.book_story.presentation.ui.theme.purpleTheme
import ua.acclorite.book_story.presentation.ui.theme.redTheme import ua.acclorite.book_story.presentation.ui.theme.redTheme
import ua.acclorite.book_story.presentation.ui.theme.yellowTheme import ua.acclorite.book_story.presentation.ui.theme.yellowTheme
@Immutable
enum class Theme { enum class Theme {
DYNAMIC, DYNAMIC,
BLUE, BLUE,
@ -41,7 +43,7 @@ fun String.toTheme(): Theme {
*/ */
@SuppressLint("NewApi") @SuppressLint("NewApi")
@Composable @Composable
fun colorScheme(theme: Theme, darkTheme: Boolean): ColorScheme { fun colorScheme(theme: Theme, darkTheme: Boolean, themeContrast: ThemeContrast): ColorScheme {
when (theme) { when (theme) {
Theme.DYNAMIC -> { Theme.DYNAMIC -> {
/* Dynamic Theme */ /* Dynamic Theme */
@ -53,37 +55,37 @@ fun colorScheme(theme: Theme, darkTheme: Boolean): ColorScheme {
Theme.BLUE -> { Theme.BLUE -> {
/* Blue Theme */ /* Blue Theme */
return blueTheme(isDark = darkTheme) return blueTheme(isDark = darkTheme, themeContrast = themeContrast)
} }
Theme.PURPLE -> { Theme.PURPLE -> {
/* Purple Theme */ /* Purple Theme */
return purpleTheme(isDark = darkTheme) return purpleTheme(isDark = darkTheme, themeContrast = themeContrast)
} }
Theme.GREEN -> { Theme.GREEN -> {
/* Green Theme */ /* Green Theme */
return greenTheme(isDark = darkTheme) return greenTheme(isDark = darkTheme, themeContrast = themeContrast)
} }
Theme.PINK -> { Theme.PINK -> {
/* Pink Theme */ /* Pink Theme */
return pinkTheme(isDark = darkTheme) return pinkTheme(isDark = darkTheme, themeContrast = themeContrast)
} }
Theme.YELLOW -> { Theme.YELLOW -> {
/* Yellow Theme */ /* Yellow Theme */
return yellowTheme(isDark = darkTheme) return yellowTheme(isDark = darkTheme, themeContrast = themeContrast)
} }
Theme.RED -> { Theme.RED -> {
/* Red Theme */ /* Red Theme */
return redTheme(isDark = darkTheme) return redTheme(isDark = darkTheme, themeContrast = themeContrast)
} }
Theme.AQUA -> { Theme.AQUA -> {
/* Aqua Theme */ /* Aqua Theme */
return aquaTheme(isDark = darkTheme) return aquaTheme(isDark = darkTheme, themeContrast = themeContrast)
} }
} }
} }

View file

@ -1,8 +0,0 @@
package ua.acclorite.book_story.presentation.ui
import androidx.compose.ui.unit.dp
object ElevationDefaults {
val Dialog = 6.dp
val BottomSheet = 1.dp
}

View file

@ -2,19 +2,28 @@ package ua.acclorite.book_story.presentation.ui
import android.app.Activity import android.app.Activity
import android.os.Build import android.os.Build
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBarDefaults
import androidx.compose.material3.Shapes import androidx.compose.material3.Shapes
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.Dp
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
@Immutable
enum class ThemeContrast {
STANDARD,
MEDIUM,
HIGH
}
@Immutable
enum class DarkTheme { enum class DarkTheme {
FOLLOW_SYSTEM, FOLLOW_SYSTEM,
OFF, OFF,
@ -25,6 +34,10 @@ fun String.toDarkTheme(): DarkTheme {
return DarkTheme.valueOf(this) return DarkTheme.valueOf(this)
} }
fun String.toThemeContrast(): ThemeContrast {
return ThemeContrast.valueOf(this)
}
@Composable @Composable
fun DarkTheme.isDark(): Boolean { fun DarkTheme.isDark(): Boolean {
return when (this) { return when (this) {
@ -38,6 +51,7 @@ fun DarkTheme.isDark(): Boolean {
fun BooksHistoryResurrectionTheme( fun BooksHistoryResurrectionTheme(
theme: Theme, theme: Theme,
isDark: Boolean, isDark: Boolean,
themeContrast: ThemeContrast,
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val view = LocalView.current val view = LocalView.current
@ -58,12 +72,16 @@ fun BooksHistoryResurrectionTheme(
} }
} }
MaterialTheme( val colorScheme = colorScheme(
colorScheme = colorScheme(
theme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) theme theme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) theme
else if (theme != Theme.DYNAMIC) theme else Theme.BLUE, else if (theme != Theme.DYNAMIC) theme else Theme.BLUE,
darkTheme = isDark darkTheme = isDark,
), themeContrast = themeContrast
)
val animatedColorScheme = animateColorScheme(targetColorScheme = colorScheme)
MaterialTheme(
colorScheme = animatedColorScheme,
shapes = Shapes(), shapes = Shapes(),
typography = Typography, typography = Typography,
content = content content = content
@ -71,5 +89,71 @@ fun BooksHistoryResurrectionTheme(
} }
@Composable @Composable
fun MaterialTheme.elevation(elevation: Dp = NavigationBarDefaults.Elevation): Color = private fun animateColor(targetColor: Color): Color {
colorScheme.surfaceColorAtElevation(elevation) return animateColorAsState(
targetValue = targetColor,
animationSpec = tween(300),
label = ""
).value
}
@Composable
private fun animateColorScheme(targetColorScheme: ColorScheme): ColorScheme {
return ColorScheme(
primary = animateColor(targetColor = targetColorScheme.primary),
onPrimary = animateColor(targetColor = targetColorScheme.onPrimary),
primaryContainer = animateColor(targetColor = targetColorScheme.primaryContainer),
onPrimaryContainer = animateColor(targetColor = targetColorScheme.onPrimaryContainer),
secondary = animateColor(targetColor = targetColorScheme.secondary),
onSecondary = animateColor(targetColor = targetColorScheme.onSecondary),
secondaryContainer = animateColor(targetColor = targetColorScheme.secondaryContainer),
onSecondaryContainer = animateColor(targetColor = targetColorScheme.onSecondaryContainer),
tertiary = animateColor(targetColor = targetColorScheme.tertiary),
onTertiary = animateColor(targetColor = targetColorScheme.onTertiary),
tertiaryContainer = animateColor(targetColor = targetColorScheme.tertiaryContainer),
onTertiaryContainer = animateColor(targetColor = targetColorScheme.onTertiaryContainer),
error = animateColor(targetColor = targetColorScheme.error),
errorContainer = animateColor(targetColor = targetColorScheme.errorContainer),
onError = animateColor(targetColor = targetColorScheme.onError),
onErrorContainer = animateColor(targetColor = targetColorScheme.onErrorContainer),
background = animateColor(targetColor = targetColorScheme.background),
onBackground = animateColor(targetColor = targetColorScheme.onBackground),
surface = animateColor(targetColor = targetColorScheme.surface),
onSurface = animateColor(targetColor = targetColorScheme.onSurface),
surfaceVariant = animateColor(targetColor = targetColorScheme.surfaceVariant),
onSurfaceVariant = animateColor(targetColor = targetColorScheme.onSurfaceVariant),
outline = animateColor(targetColor = targetColorScheme.outline),
inverseOnSurface = animateColor(targetColor = targetColorScheme.inverseOnSurface),
inverseSurface = animateColor(targetColor = targetColorScheme.inverseSurface),
inversePrimary = animateColor(targetColor = targetColorScheme.inversePrimary),
surfaceTint = animateColor(targetColor = targetColorScheme.surfaceTint),
outlineVariant = animateColor(targetColor = targetColorScheme.outlineVariant),
scrim = animateColor(targetColor = targetColorScheme.scrim),
surfaceBright = animateColor(targetColor = targetColorScheme.surfaceBright),
surfaceDim = animateColor(targetColor = targetColorScheme.surfaceDim),
surfaceContainer = animateColor(targetColor = targetColorScheme.surfaceContainer),
surfaceContainerHigh = animateColor(targetColor = targetColorScheme.surfaceContainerHigh),
surfaceContainerHighest = animateColor(targetColor = targetColorScheme.surfaceContainerHighest),
surfaceContainerLow = animateColor(targetColor = targetColorScheme.surfaceContainerLow),
surfaceContainerLowest = animateColor(targetColor = targetColorScheme.surfaceContainerLowest),
)
}
@Composable
fun animatedColorScheme(
theme: Theme,
isDark: Boolean,
themeContrast: ThemeContrast
): ColorScheme {
val colorScheme = colorScheme(
theme = theme,
darkTheme = isDark,
themeContrast = themeContrast
)
return animateColorScheme(targetColorScheme = colorScheme)
}

View file

@ -5,132 +5,472 @@ import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import ua.acclorite.book_story.presentation.ui.ThemeContrast
private val md_theme_light_primary = Color(0xFF006876)
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
private val md_theme_light_primaryContainer = Color(0xFFA1EFFF)
private val md_theme_light_onPrimaryContainer = Color(0xFF001F25)
private val md_theme_light_secondary = Color(0xFF4A6268)
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
private val md_theme_light_secondaryContainer = Color(0xFFCDE7ED)
private val md_theme_light_onSecondaryContainer = Color(0xFF051F23)
private val md_theme_light_tertiary = Color(0xFF545D7E)
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
private val md_theme_light_tertiaryContainer = Color(0xFFDBE1FF)
private val md_theme_light_onTertiaryContainer = Color(0xFF101A37)
private val md_theme_light_error = Color(0xFFBA1A1A)
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
private val md_theme_light_onError = Color(0xFFFFFFFF)
private val md_theme_light_onErrorContainer = Color(0xFF410002)
private val md_theme_light_background = Color(0xFFFBFCFD)
private val md_theme_light_onBackground = Color(0xFF191C1D)
private val md_theme_light_surface = Color(0xFFFBFCFD)
private val md_theme_light_onSurface = Color(0xFF191C1D)
private val md_theme_light_surfaceVariant = Color(0xFFDBE4E6)
private val md_theme_light_onSurfaceVariant = Color(0xFF3F484A)
private val md_theme_light_outline = Color(0xFF6F797B)
private val md_theme_light_inverseOnSurface = Color(0xFFEFF1F2)
private val md_theme_light_inverseSurface = Color(0xFF2E3132)
private val md_theme_light_inversePrimary = Color(0xFF51D7EF)
private val md_theme_light_surfaceTint = Color(0xFF006876)
private val md_theme_light_outlineVariant = Color(0xFFBFC8CA)
private val md_theme_light_scrim = Color(0xFF000000)
private val md_theme_dark_primary = Color(0xFF51D7EF) private val primaryLight = Color(0xFF116682)
private val md_theme_dark_onPrimary = Color(0xFF00363E) private val onPrimaryLight = Color(0xFFFFFFFF)
private val md_theme_dark_primaryContainer = Color(0xFF004E59) private val primaryContainerLight = Color(0xFFBDE9FF)
private val md_theme_dark_onPrimaryContainer = Color(0xFFA1EFFF) private val onPrimaryContainerLight = Color(0xFF001F2A)
private val md_theme_dark_secondary = Color(0xFFB1CBD1) private val secondaryLight = Color(0xFF4D616C)
private val md_theme_dark_onSecondary = Color(0xFF1C3439) private val onSecondaryLight = Color(0xFFFFFFFF)
private val md_theme_dark_secondaryContainer = Color(0xFF334A50) private val secondaryContainerLight = Color(0xFFD0E6F2)
private val md_theme_dark_onSecondaryContainer = Color(0xFFCDE7ED) private val onSecondaryContainerLight = Color(0xFF081E27)
private val md_theme_dark_tertiary = Color(0xFFBCC5EB) private val tertiaryLight = Color(0xFF5D5B7D)
private val md_theme_dark_onTertiary = Color(0xFF262F4D) private val onTertiaryLight = Color(0xFFFFFFFF)
private val md_theme_dark_tertiaryContainer = Color(0xFF3C4665) private val tertiaryContainerLight = Color(0xFFE3DFFF)
private val md_theme_dark_onTertiaryContainer = Color(0xFFDBE1FF) private val onTertiaryContainerLight = Color(0xFF191836)
private val md_theme_dark_error = Color(0xFFFFB4AB) private val errorLight = Color(0xFFBA1A1A)
private val md_theme_dark_errorContainer = Color(0xFF93000A) private val onErrorLight = Color(0xFFFFFFFF)
private val md_theme_dark_onError = Color(0xFF690005) private val errorContainerLight = Color(0xFFFFDAD6)
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) private val onErrorContainerLight = Color(0xFF410002)
private val md_theme_dark_background = Color(0xFF191C1D) private val backgroundLight = Color(0xFFF6FAFD)
private val md_theme_dark_onBackground = Color(0xFFE1E3E3) private val onBackgroundLight = Color(0xFF171C1F)
private val md_theme_dark_surface = Color(0xFF191C1D) private val surfaceLight = Color(0xFFF6FAFD)
private val md_theme_dark_onSurface = Color(0xFFE1E3E3) private val onSurfaceLight = Color(0xFF171C1F)
private val md_theme_dark_surfaceVariant = Color(0xFF3F484A) private val surfaceVariantLight = Color(0xFFDCE4E9)
private val md_theme_dark_onSurfaceVariant = Color(0xFFBFC8CA) private val onSurfaceVariantLight = Color(0xFF40484C)
private val md_theme_dark_outline = Color(0xFF899295) private val outlineLight = Color(0xFF70787D)
private val md_theme_dark_inverseOnSurface = Color(0xFF191C1D) private val outlineVariantLight = Color(0xFFC0C8CD)
private val md_theme_dark_inverseSurface = Color(0xFFE1E3E3) private val scrimLight = Color(0xFF000000)
private val md_theme_dark_inversePrimary = Color(0xFF006876) private val inverseSurfaceLight = Color(0xFF2C3134)
private val md_theme_dark_surfaceTint = Color(0xFF51D7EF) private val inverseOnSurfaceLight = Color(0xFFEDF1F5)
private val md_theme_dark_outlineVariant = Color(0xFF3F484A) private val inversePrimaryLight = Color(0xFF8BD0EF)
private val md_theme_dark_scrim = Color(0xFF000000) private val surfaceDimLight = Color(0xFFD6DBDE)
private val surfaceBrightLight = Color(0xFFF6FAFD)
private val surfaceContainerLowestLight = Color(0xFFFFFFFF)
private val surfaceContainerLowLight = Color(0xFFF0F4F8)
private val surfaceContainerLight = Color(0xFFEAEEF2)
private val surfaceContainerHighLight = Color(0xFFE4E9EC)
private val surfaceContainerHighestLight = Color(0xFFDFE3E7)
private val primaryLightMediumContrast = Color(0xFF00495F)
private val onPrimaryLightMediumContrast = Color(0xFFFFFFFF)
private val primaryContainerLightMediumContrast = Color(0xFF337D99)
private val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF)
private val secondaryLightMediumContrast = Color(0xFF31464F)
private val onSecondaryLightMediumContrast = Color(0xFFFFFFFF)
private val secondaryContainerLightMediumContrast = Color(0xFF637882)
private val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF)
private val tertiaryLightMediumContrast = Color(0xFF413F60)
private val onTertiaryLightMediumContrast = Color(0xFFFFFFFF)
private val tertiaryContainerLightMediumContrast = Color(0xFF737195)
private val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF)
private val errorLightMediumContrast = Color(0xFF8C0009)
private val onErrorLightMediumContrast = Color(0xFFFFFFFF)
private val errorContainerLightMediumContrast = Color(0xFFDA342E)
private val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF)
private val backgroundLightMediumContrast = Color(0xFFF6FAFD)
private val onBackgroundLightMediumContrast = Color(0xFF171C1F)
private val surfaceLightMediumContrast = Color(0xFFF6FAFD)
private val onSurfaceLightMediumContrast = Color(0xFF171C1F)
private val surfaceVariantLightMediumContrast = Color(0xFFDCE4E9)
private val onSurfaceVariantLightMediumContrast = Color(0xFF3C4448)
private val outlineLightMediumContrast = Color(0xFF586065)
private val outlineVariantLightMediumContrast = Color(0xFF747C80)
private val scrimLightMediumContrast = Color(0xFF000000)
private val inverseSurfaceLightMediumContrast = Color(0xFF2C3134)
private val inverseOnSurfaceLightMediumContrast = Color(0xFFEDF1F5)
private val inversePrimaryLightMediumContrast = Color(0xFF8BD0EF)
private val surfaceDimLightMediumContrast = Color(0xFFD6DBDE)
private val surfaceBrightLightMediumContrast = Color(0xFFF6FAFD)
private val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF)
private val surfaceContainerLowLightMediumContrast = Color(0xFFF0F4F8)
private val surfaceContainerLightMediumContrast = Color(0xFFEAEEF2)
private val surfaceContainerHighLightMediumContrast = Color(0xFFE4E9EC)
private val surfaceContainerHighestLightMediumContrast = Color(0xFFDFE3E7)
private val primaryLightHighContrast = Color(0xFF002633)
private val onPrimaryLightHighContrast = Color(0xFFFFFFFF)
private val primaryContainerLightHighContrast = Color(0xFF00495F)
private val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF)
private val secondaryLightHighContrast = Color(0xFF10252E)
private val onSecondaryLightHighContrast = Color(0xFFFFFFFF)
private val secondaryContainerLightHighContrast = Color(0xFF31464F)
private val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF)
private val tertiaryLightHighContrast = Color(0xFF201F3D)
private val onTertiaryLightHighContrast = Color(0xFFFFFFFF)
private val tertiaryContainerLightHighContrast = Color(0xFF413F60)
private val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF)
private val errorLightHighContrast = Color(0xFF4E0002)
private val onErrorLightHighContrast = Color(0xFFFFFFFF)
private val errorContainerLightHighContrast = Color(0xFF8C0009)
private val onErrorContainerLightHighContrast = Color(0xFFFFFFFF)
private val backgroundLightHighContrast = Color(0xFFF6FAFD)
private val onBackgroundLightHighContrast = Color(0xFF171C1F)
private val surfaceLightHighContrast = Color(0xFFF6FAFD)
private val onSurfaceLightHighContrast = Color(0xFF000000)
private val surfaceVariantLightHighContrast = Color(0xFFDCE4E9)
private val onSurfaceVariantLightHighContrast = Color(0xFF1D2529)
private val outlineLightHighContrast = Color(0xFF3C4448)
private val outlineVariantLightHighContrast = Color(0xFF3C4448)
private val scrimLightHighContrast = Color(0xFF000000)
private val inverseSurfaceLightHighContrast = Color(0xFF2C3134)
private val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF)
private val inversePrimaryLightHighContrast = Color(0xFFD5F0FF)
private val surfaceDimLightHighContrast = Color(0xFFD6DBDE)
private val surfaceBrightLightHighContrast = Color(0xFFF6FAFD)
private val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF)
private val surfaceContainerLowLightHighContrast = Color(0xFFF0F4F8)
private val surfaceContainerLightHighContrast = Color(0xFFEAEEF2)
private val surfaceContainerHighLightHighContrast = Color(0xFFE4E9EC)
private val surfaceContainerHighestLightHighContrast = Color(0xFFDFE3E7)
private val primaryDark = Color(0xFF8BD0EF)
private val onPrimaryDark = Color(0xFF003546)
private val primaryContainerDark = Color(0xFF004D64)
private val onPrimaryContainerDark = Color(0xFFBDE9FF)
private val secondaryDark = Color(0xFFB4CAD6)
private val onSecondaryDark = Color(0xFF1F333C)
private val secondaryContainerDark = Color(0xFF354A53)
private val onSecondaryContainerDark = Color(0xFFD0E6F2)
private val tertiaryDark = Color(0xFFC6C2EA)
private val onTertiaryDark = Color(0xFF2E2D4D)
private val tertiaryContainerDark = Color(0xFF454364)
private val onTertiaryContainerDark = Color(0xFFE3DFFF)
private val errorDark = Color(0xFFFFB4AB)
private val onErrorDark = Color(0xFF690005)
private val errorContainerDark = Color(0xFF93000A)
private val onErrorContainerDark = Color(0xFFFFDAD6)
private val backgroundDark = Color(0xFF0F1417)
private val onBackgroundDark = Color(0xFFDFE3E7)
private val surfaceDark = Color(0xFF0F1417)
private val onSurfaceDark = Color(0xFFDFE3E7)
private val surfaceVariantDark = Color(0xFF40484C)
private val onSurfaceVariantDark = Color(0xFFC0C8CD)
private val outlineDark = Color(0xFF8A9297)
private val outlineVariantDark = Color(0xFF40484C)
private val scrimDark = Color(0xFF000000)
private val inverseSurfaceDark = Color(0xFFDFE3E7)
private val inverseOnSurfaceDark = Color(0xFF2C3134)
private val inversePrimaryDark = Color(0xFF116682)
private val surfaceDimDark = Color(0xFF0F1417)
private val surfaceBrightDark = Color(0xFF353A3D)
private val surfaceContainerLowestDark = Color(0xFF0A0F11)
private val surfaceContainerLowDark = Color(0xFF171C1F)
private val surfaceContainerDark = Color(0xFF1B2023)
private val surfaceContainerHighDark = Color(0xFF262B2D)
private val surfaceContainerHighestDark = Color(0xFF303538)
private val primaryDarkMediumContrast = Color(0xFF8FD4F4)
private val onPrimaryDarkMediumContrast = Color(0xFF001923)
private val primaryContainerDarkMediumContrast = Color(0xFF5399B7)
private val onPrimaryContainerDarkMediumContrast = Color(0xFF000000)
private val secondaryDarkMediumContrast = Color(0xFFB8CEDA)
private val onSecondaryDarkMediumContrast = Color(0xFF031921)
private val secondaryContainerDarkMediumContrast = Color(0xFF7F949F)
private val onSecondaryContainerDarkMediumContrast = Color(0xFF000000)
private val tertiaryDarkMediumContrast = Color(0xFFCAC7EF)
private val onTertiaryDarkMediumContrast = Color(0xFF141231)
private val tertiaryContainerDarkMediumContrast = Color(0xFF908DB2)
private val onTertiaryContainerDarkMediumContrast = Color(0xFF000000)
private val errorDarkMediumContrast = Color(0xFFFFBAB1)
private val onErrorDarkMediumContrast = Color(0xFF370001)
private val errorContainerDarkMediumContrast = Color(0xFFFF5449)
private val onErrorContainerDarkMediumContrast = Color(0xFF000000)
private val backgroundDarkMediumContrast = Color(0xFF0F1417)
private val onBackgroundDarkMediumContrast = Color(0xFFDFE3E7)
private val surfaceDarkMediumContrast = Color(0xFF0F1417)
private val onSurfaceDarkMediumContrast = Color(0xFFF7FBFF)
private val surfaceVariantDarkMediumContrast = Color(0xFF40484C)
private val onSurfaceVariantDarkMediumContrast = Color(0xFFC4CCD1)
private val outlineDarkMediumContrast = Color(0xFF9CA4A9)
private val outlineVariantDarkMediumContrast = Color(0xFF7C8489)
private val scrimDarkMediumContrast = Color(0xFF000000)
private val inverseSurfaceDarkMediumContrast = Color(0xFFDFE3E7)
private val inverseOnSurfaceDarkMediumContrast = Color(0xFF262B2E)
private val inversePrimaryDarkMediumContrast = Color(0xFF004E66)
private val surfaceDimDarkMediumContrast = Color(0xFF0F1417)
private val surfaceBrightDarkMediumContrast = Color(0xFF353A3D)
private val surfaceContainerLowestDarkMediumContrast = Color(0xFF0A0F11)
private val surfaceContainerLowDarkMediumContrast = Color(0xFF171C1F)
private val surfaceContainerDarkMediumContrast = Color(0xFF1B2023)
private val surfaceContainerHighDarkMediumContrast = Color(0xFF262B2D)
private val surfaceContainerHighestDarkMediumContrast = Color(0xFF303538)
private val primaryDarkHighContrast = Color(0xFFF7FBFF)
private val onPrimaryDarkHighContrast = Color(0xFF000000)
private val primaryContainerDarkHighContrast = Color(0xFF8FD4F4)
private val onPrimaryContainerDarkHighContrast = Color(0xFF000000)
private val secondaryDarkHighContrast = Color(0xFFF7FBFF)
private val onSecondaryDarkHighContrast = Color(0xFF000000)
private val secondaryContainerDarkHighContrast = Color(0xFFB8CEDA)
private val onSecondaryContainerDarkHighContrast = Color(0xFF000000)
private val tertiaryDarkHighContrast = Color(0xFFFEF9FF)
private val onTertiaryDarkHighContrast = Color(0xFF000000)
private val tertiaryContainerDarkHighContrast = Color(0xFFCAC7EF)
private val onTertiaryContainerDarkHighContrast = Color(0xFF000000)
private val errorDarkHighContrast = Color(0xFFFFF9F9)
private val onErrorDarkHighContrast = Color(0xFF000000)
private val errorContainerDarkHighContrast = Color(0xFFFFBAB1)
private val onErrorContainerDarkHighContrast = Color(0xFF000000)
private val backgroundDarkHighContrast = Color(0xFF0F1417)
private val onBackgroundDarkHighContrast = Color(0xFFDFE3E7)
private val surfaceDarkHighContrast = Color(0xFF0F1417)
private val onSurfaceDarkHighContrast = Color(0xFFFFFFFF)
private val surfaceVariantDarkHighContrast = Color(0xFF40484C)
private val onSurfaceVariantDarkHighContrast = Color(0xFFF7FBFF)
private val outlineDarkHighContrast = Color(0xFFC4CCD1)
private val outlineVariantDarkHighContrast = Color(0xFFC4CCD1)
private val scrimDarkHighContrast = Color(0xFF000000)
private val inverseSurfaceDarkHighContrast = Color(0xFFDFE3E7)
private val inverseOnSurfaceDarkHighContrast = Color(0xFF000000)
private val inversePrimaryDarkHighContrast = Color(0xFF002E3D)
private val surfaceDimDarkHighContrast = Color(0xFF0F1417)
private val surfaceBrightDarkHighContrast = Color(0xFF353A3D)
private val surfaceContainerLowestDarkHighContrast = Color(0xFF0A0F11)
private val surfaceContainerLowDarkHighContrast = Color(0xFF171C1F)
private val surfaceContainerDarkHighContrast = Color(0xFF1B2023)
private val surfaceContainerHighDarkHighContrast = Color(0xFF262B2D)
private val surfaceContainerHighestDarkHighContrast = Color(0xFF303538)
@Composable @Composable
fun aquaTheme(isDark: Boolean): ColorScheme { fun aquaTheme(isDark: Boolean, themeContrast: ThemeContrast): ColorScheme {
return if (isDark) { return if (isDark) {
when (themeContrast) {
ThemeContrast.STANDARD -> {
darkColorScheme( darkColorScheme(
primary = md_theme_dark_primary, primary = primaryDark,
onPrimary = md_theme_dark_onPrimary, onPrimary = onPrimaryDark,
primaryContainer = md_theme_dark_primaryContainer, primaryContainer = primaryContainerDark,
onPrimaryContainer = md_theme_dark_onPrimaryContainer, onPrimaryContainer = onPrimaryContainerDark,
secondary = md_theme_dark_secondary, secondary = secondaryDark,
onSecondary = md_theme_dark_onSecondary, onSecondary = onSecondaryDark,
secondaryContainer = md_theme_dark_secondaryContainer, secondaryContainer = secondaryContainerDark,
onSecondaryContainer = md_theme_dark_onSecondaryContainer, onSecondaryContainer = onSecondaryContainerDark,
tertiary = md_theme_dark_tertiary, tertiary = tertiaryDark,
onTertiary = md_theme_dark_onTertiary, onTertiary = onTertiaryDark,
tertiaryContainer = md_theme_dark_tertiaryContainer, tertiaryContainer = tertiaryContainerDark,
onTertiaryContainer = md_theme_dark_onTertiaryContainer, onTertiaryContainer = onTertiaryContainerDark,
error = md_theme_dark_error, error = errorDark,
errorContainer = md_theme_dark_errorContainer, onError = onErrorDark,
onError = md_theme_dark_onError, errorContainer = errorContainerDark,
onErrorContainer = md_theme_dark_onErrorContainer, onErrorContainer = onErrorContainerDark,
background = md_theme_dark_background, background = backgroundDark,
onBackground = md_theme_dark_onBackground, onBackground = onBackgroundDark,
surface = md_theme_dark_surface, surface = surfaceDark,
onSurface = md_theme_dark_onSurface, onSurface = onSurfaceDark,
surfaceVariant = md_theme_dark_surfaceVariant, surfaceVariant = surfaceVariantDark,
onSurfaceVariant = md_theme_dark_onSurfaceVariant, onSurfaceVariant = onSurfaceVariantDark,
outline = md_theme_dark_outline, outline = outlineDark,
inverseOnSurface = md_theme_dark_inverseOnSurface, outlineVariant = outlineVariantDark,
inverseSurface = md_theme_dark_inverseSurface, scrim = scrimDark,
inversePrimary = md_theme_dark_inversePrimary, inverseSurface = inverseSurfaceDark,
surfaceTint = md_theme_dark_surfaceTint, inverseOnSurface = inverseOnSurfaceDark,
outlineVariant = md_theme_dark_outlineVariant, inversePrimary = inversePrimaryDark,
scrim = md_theme_dark_scrim surfaceDim = surfaceDimDark,
) surfaceBright = surfaceBrightDark,
} else { surfaceContainerLowest = surfaceContainerLowestDark,
lightColorScheme( surfaceContainerLow = surfaceContainerLowDark,
primary = md_theme_light_primary, surfaceContainer = surfaceContainerDark,
onPrimary = md_theme_light_onPrimary, surfaceContainerHigh = surfaceContainerHighDark,
primaryContainer = md_theme_light_primaryContainer, surfaceContainerHighest = surfaceContainerHighestDark,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim
) )
} }
ThemeContrast.MEDIUM -> {
darkColorScheme(
primary = primaryDarkMediumContrast,
onPrimary = onPrimaryDarkMediumContrast,
primaryContainer = primaryContainerDarkMediumContrast,
onPrimaryContainer = onPrimaryContainerDarkMediumContrast,
secondary = secondaryDarkMediumContrast,
onSecondary = onSecondaryDarkMediumContrast,
secondaryContainer = secondaryContainerDarkMediumContrast,
onSecondaryContainer = onSecondaryContainerDarkMediumContrast,
tertiary = tertiaryDarkMediumContrast,
onTertiary = onTertiaryDarkMediumContrast,
tertiaryContainer = tertiaryContainerDarkMediumContrast,
onTertiaryContainer = onTertiaryContainerDarkMediumContrast,
error = errorDarkMediumContrast,
onError = onErrorDarkMediumContrast,
errorContainer = errorContainerDarkMediumContrast,
onErrorContainer = onErrorContainerDarkMediumContrast,
background = backgroundDarkMediumContrast,
onBackground = onBackgroundDarkMediumContrast,
surface = surfaceDarkMediumContrast,
onSurface = onSurfaceDarkMediumContrast,
surfaceVariant = surfaceVariantDarkMediumContrast,
onSurfaceVariant = onSurfaceVariantDarkMediumContrast,
outline = outlineDarkMediumContrast,
outlineVariant = outlineVariantDarkMediumContrast,
scrim = scrimDarkMediumContrast,
inverseSurface = inverseSurfaceDarkMediumContrast,
inverseOnSurface = inverseOnSurfaceDarkMediumContrast,
inversePrimary = inversePrimaryDarkMediumContrast,
surfaceDim = surfaceDimDarkMediumContrast,
surfaceBright = surfaceBrightDarkMediumContrast,
surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast,
surfaceContainerLow = surfaceContainerLowDarkMediumContrast,
surfaceContainer = surfaceContainerDarkMediumContrast,
surfaceContainerHigh = surfaceContainerHighDarkMediumContrast,
surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast,
)
}
ThemeContrast.HIGH -> {
darkColorScheme(
primary = primaryDarkHighContrast,
onPrimary = onPrimaryDarkHighContrast,
primaryContainer = primaryContainerDarkHighContrast,
onPrimaryContainer = onPrimaryContainerDarkHighContrast,
secondary = secondaryDarkHighContrast,
onSecondary = onSecondaryDarkHighContrast,
secondaryContainer = secondaryContainerDarkHighContrast,
onSecondaryContainer = onSecondaryContainerDarkHighContrast,
tertiary = tertiaryDarkHighContrast,
onTertiary = onTertiaryDarkHighContrast,
tertiaryContainer = tertiaryContainerDarkHighContrast,
onTertiaryContainer = onTertiaryContainerDarkHighContrast,
error = errorDarkHighContrast,
onError = onErrorDarkHighContrast,
errorContainer = errorContainerDarkHighContrast,
onErrorContainer = onErrorContainerDarkHighContrast,
background = backgroundDarkHighContrast,
onBackground = onBackgroundDarkHighContrast,
surface = surfaceDarkHighContrast,
onSurface = onSurfaceDarkHighContrast,
surfaceVariant = surfaceVariantDarkHighContrast,
onSurfaceVariant = onSurfaceVariantDarkHighContrast,
outline = outlineDarkHighContrast,
outlineVariant = outlineVariantDarkHighContrast,
scrim = scrimDarkHighContrast,
inverseSurface = inverseSurfaceDarkHighContrast,
inverseOnSurface = inverseOnSurfaceDarkHighContrast,
inversePrimary = inversePrimaryDarkHighContrast,
surfaceDim = surfaceDimDarkHighContrast,
surfaceBright = surfaceBrightDarkHighContrast,
surfaceContainerLowest = surfaceContainerLowestDarkHighContrast,
surfaceContainerLow = surfaceContainerLowDarkHighContrast,
surfaceContainer = surfaceContainerDarkHighContrast,
surfaceContainerHigh = surfaceContainerHighDarkHighContrast,
surfaceContainerHighest = surfaceContainerHighestDarkHighContrast,
)
}
}
} else {
when (themeContrast) {
ThemeContrast.STANDARD -> {
lightColorScheme(
primary = primaryLight,
onPrimary = onPrimaryLight,
primaryContainer = primaryContainerLight,
onPrimaryContainer = onPrimaryContainerLight,
secondary = secondaryLight,
onSecondary = onSecondaryLight,
secondaryContainer = secondaryContainerLight,
onSecondaryContainer = onSecondaryContainerLight,
tertiary = tertiaryLight,
onTertiary = onTertiaryLight,
tertiaryContainer = tertiaryContainerLight,
onTertiaryContainer = onTertiaryContainerLight,
error = errorLight,
onError = onErrorLight,
errorContainer = errorContainerLight,
onErrorContainer = onErrorContainerLight,
background = backgroundLight,
onBackground = onBackgroundLight,
surface = surfaceLight,
onSurface = onSurfaceLight,
surfaceVariant = surfaceVariantLight,
onSurfaceVariant = onSurfaceVariantLight,
outline = outlineLight,
outlineVariant = outlineVariantLight,
scrim = scrimLight,
inverseSurface = inverseSurfaceLight,
inverseOnSurface = inverseOnSurfaceLight,
inversePrimary = inversePrimaryLight,
surfaceDim = surfaceDimLight,
surfaceBright = surfaceBrightLight,
surfaceContainerLowest = surfaceContainerLowestLight,
surfaceContainerLow = surfaceContainerLowLight,
surfaceContainer = surfaceContainerLight,
surfaceContainerHigh = surfaceContainerHighLight,
surfaceContainerHighest = surfaceContainerHighestLight,
)
}
ThemeContrast.MEDIUM -> {
lightColorScheme(
primary = primaryLightMediumContrast,
onPrimary = onPrimaryLightMediumContrast,
primaryContainer = primaryContainerLightMediumContrast,
onPrimaryContainer = onPrimaryContainerLightMediumContrast,
secondary = secondaryLightMediumContrast,
onSecondary = onSecondaryLightMediumContrast,
secondaryContainer = secondaryContainerLightMediumContrast,
onSecondaryContainer = onSecondaryContainerLightMediumContrast,
tertiary = tertiaryLightMediumContrast,
onTertiary = onTertiaryLightMediumContrast,
tertiaryContainer = tertiaryContainerLightMediumContrast,
onTertiaryContainer = onTertiaryContainerLightMediumContrast,
error = errorLightMediumContrast,
onError = onErrorLightMediumContrast,
errorContainer = errorContainerLightMediumContrast,
onErrorContainer = onErrorContainerLightMediumContrast,
background = backgroundLightMediumContrast,
onBackground = onBackgroundLightMediumContrast,
surface = surfaceLightMediumContrast,
onSurface = onSurfaceLightMediumContrast,
surfaceVariant = surfaceVariantLightMediumContrast,
onSurfaceVariant = onSurfaceVariantLightMediumContrast,
outline = outlineLightMediumContrast,
outlineVariant = outlineVariantLightMediumContrast,
scrim = scrimLightMediumContrast,
inverseSurface = inverseSurfaceLightMediumContrast,
inverseOnSurface = inverseOnSurfaceLightMediumContrast,
inversePrimary = inversePrimaryLightMediumContrast,
surfaceDim = surfaceDimLightMediumContrast,
surfaceBright = surfaceBrightLightMediumContrast,
surfaceContainerLowest = surfaceContainerLowestLightMediumContrast,
surfaceContainerLow = surfaceContainerLowLightMediumContrast,
surfaceContainer = surfaceContainerLightMediumContrast,
surfaceContainerHigh = surfaceContainerHighLightMediumContrast,
surfaceContainerHighest = surfaceContainerHighestLightMediumContrast,
)
}
ThemeContrast.HIGH -> {
lightColorScheme(
primary = primaryLightHighContrast,
onPrimary = onPrimaryLightHighContrast,
primaryContainer = primaryContainerLightHighContrast,
onPrimaryContainer = onPrimaryContainerLightHighContrast,
secondary = secondaryLightHighContrast,
onSecondary = onSecondaryLightHighContrast,
secondaryContainer = secondaryContainerLightHighContrast,
onSecondaryContainer = onSecondaryContainerLightHighContrast,
tertiary = tertiaryLightHighContrast,
onTertiary = onTertiaryLightHighContrast,
tertiaryContainer = tertiaryContainerLightHighContrast,
onTertiaryContainer = onTertiaryContainerLightHighContrast,
error = errorLightHighContrast,
onError = onErrorLightHighContrast,
errorContainer = errorContainerLightHighContrast,
onErrorContainer = onErrorContainerLightHighContrast,
background = backgroundLightHighContrast,
onBackground = onBackgroundLightHighContrast,
surface = surfaceLightHighContrast,
onSurface = onSurfaceLightHighContrast,
surfaceVariant = surfaceVariantLightHighContrast,
onSurfaceVariant = onSurfaceVariantLightHighContrast,
outline = outlineLightHighContrast,
outlineVariant = outlineVariantLightHighContrast,
scrim = scrimLightHighContrast,
inverseSurface = inverseSurfaceLightHighContrast,
inverseOnSurface = inverseOnSurfaceLightHighContrast,
inversePrimary = inversePrimaryLightHighContrast,
surfaceDim = surfaceDimLightHighContrast,
surfaceBright = surfaceBrightLightHighContrast,
surfaceContainerLowest = surfaceContainerLowestLightHighContrast,
surfaceContainerLow = surfaceContainerLowLightHighContrast,
surfaceContainer = surfaceContainerLightHighContrast,
surfaceContainerHigh = surfaceContainerHighLightHighContrast,
surfaceContainerHighest = surfaceContainerHighestLightHighContrast,
)
}
}
}
} }

Some files were not shown because too many files have changed in this diff Show more