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:
parent
1689e9d7b9
commit
76e1ca3e3f
108 changed files with 5205 additions and 1851 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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>>
|
||||||
|
)
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -170,6 +170,7 @@ object Constants {
|
||||||
"",
|
"",
|
||||||
UIText.StringValue(""),
|
UIText.StringValue(""),
|
||||||
null,
|
null,
|
||||||
|
"",
|
||||||
emptyList(),
|
emptyList(),
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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>()
|
|
||||||
}
|
}
|
||||||
|
|
@ -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?,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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),
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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!!
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,18 +133,15 @@ 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,
|
||||||
|
disableOnClick = true,
|
||||||
|
enabled = isDeleteEnabled,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
) {
|
||||||
onDeleteClick()
|
onDeleteClick()
|
||||||
}
|
}
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Delete,
|
|
||||||
contentDescription = "Delete this history element",
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
@ -289,6 +290,8 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
surfaceContainerLowest = surfaceContainerLowestDark,
|
||||||
|
surfaceContainerLow = surfaceContainerLowDark,
|
||||||
|
surfaceContainer = surfaceContainerDark,
|
||||||
|
surfaceContainerHigh = surfaceContainerHighDark,
|
||||||
|
surfaceContainerHighest = surfaceContainerHighestDark,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
|
when (themeContrast) {
|
||||||
|
ThemeContrast.STANDARD -> {
|
||||||
lightColorScheme(
|
lightColorScheme(
|
||||||
primary = md_theme_light_primary,
|
primary = primaryLight,
|
||||||
onPrimary = md_theme_light_onPrimary,
|
onPrimary = onPrimaryLight,
|
||||||
primaryContainer = md_theme_light_primaryContainer,
|
primaryContainer = primaryContainerLight,
|
||||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
onPrimaryContainer = onPrimaryContainerLight,
|
||||||
secondary = md_theme_light_secondary,
|
secondary = secondaryLight,
|
||||||
onSecondary = md_theme_light_onSecondary,
|
onSecondary = onSecondaryLight,
|
||||||
secondaryContainer = md_theme_light_secondaryContainer,
|
secondaryContainer = secondaryContainerLight,
|
||||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
onSecondaryContainer = onSecondaryContainerLight,
|
||||||
tertiary = md_theme_light_tertiary,
|
tertiary = tertiaryLight,
|
||||||
onTertiary = md_theme_light_onTertiary,
|
onTertiary = onTertiaryLight,
|
||||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
tertiaryContainer = tertiaryContainerLight,
|
||||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
onTertiaryContainer = onTertiaryContainerLight,
|
||||||
error = md_theme_light_error,
|
error = errorLight,
|
||||||
errorContainer = md_theme_light_errorContainer,
|
onError = onErrorLight,
|
||||||
onError = md_theme_light_onError,
|
errorContainer = errorContainerLight,
|
||||||
onErrorContainer = md_theme_light_onErrorContainer,
|
onErrorContainer = onErrorContainerLight,
|
||||||
background = md_theme_light_background,
|
background = backgroundLight,
|
||||||
onBackground = md_theme_light_onBackground,
|
onBackground = onBackgroundLight,
|
||||||
surface = md_theme_light_surface,
|
surface = surfaceLight,
|
||||||
onSurface = md_theme_light_onSurface,
|
onSurface = onSurfaceLight,
|
||||||
surfaceVariant = md_theme_light_surfaceVariant,
|
surfaceVariant = surfaceVariantLight,
|
||||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
onSurfaceVariant = onSurfaceVariantLight,
|
||||||
outline = md_theme_light_outline,
|
outline = outlineLight,
|
||||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
outlineVariant = outlineVariantLight,
|
||||||
inverseSurface = md_theme_light_inverseSurface,
|
scrim = scrimLight,
|
||||||
inversePrimary = md_theme_light_inversePrimary,
|
inverseSurface = inverseSurfaceLight,
|
||||||
surfaceTint = md_theme_light_surfaceTint,
|
inverseOnSurface = inverseOnSurfaceLight,
|
||||||
outlineVariant = md_theme_light_outlineVariant,
|
inversePrimary = inversePrimaryLight,
|
||||||
scrim = md_theme_light_scrim
|
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
Loading…
Add table
Add a link
Reference in a new issue