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

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

View file

@ -15,7 +15,7 @@ android {
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "0.9.3"
versionName = "0.9.6"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@ -66,13 +66,19 @@ dependencies {
implementation("androidx.activity:activity-compose:1.8.2")
// 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-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
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.material:material-icons-extended")
implementation("androidx.compose.material:material")
// Unit tests
@ -84,7 +90,7 @@ dependencies {
testImplementation("androidx.test.ext:truth:1.5.0")
testImplementation("com.squareup.okhttp3:mockwebserver: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
androidTestImplementation("com.google.dagger:hilt-android-testing:latest.release")
@ -100,7 +106,6 @@ dependencies {
// All dependencies
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")
// Dagger - Hilt

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,14 +1,12 @@
package ua.acclorite.book_story.domain.use_case
import kotlinx.coroutines.flow.Flow
import ua.acclorite.book_story.domain.model.Book
import ua.acclorite.book_story.domain.repository.BookRepository
import ua.acclorite.book_story.domain.util.Resource
import javax.inject.Inject
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)
}
}

View file

@ -1,11 +1,12 @@
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 javax.inject.Inject
class GetText @Inject constructor(private val repository: BookRepository) {
suspend fun execute(id: Int): String {
return repository.getBookTextById(bookId = id)
suspend fun execute(textPath: String): List<StringWithId> {
return repository.getBookTextById(textPath = textPath)
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -28,7 +28,6 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
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.
@ -42,7 +41,7 @@ import ua.acclorite.book_story.presentation.ui.elevation
@Composable
fun AnimatedTopAppBar(
containerColor: Color = MaterialTheme.colorScheme.surface,
scrolledContainerColor: Color = MaterialTheme.elevation(),
scrolledContainerColor: Color = MaterialTheme.colorScheme.surfaceContainer,
scrollBehavior: TopAppBarScrollBehavior?,
isTopBarScrolled: Boolean?,

View file

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

View file

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

View file

@ -1,5 +1,6 @@
package ua.acclorite.book_story.presentation.components
import androidx.annotation.StringRes
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
@ -11,6 +12,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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.
@ -19,27 +21,29 @@ import androidx.compose.ui.graphics.vector.ImageVector
fun CustomIconButton(
modifier: Modifier = Modifier,
icon: ImageVector,
contentDescription: String,
@StringRes contentDescription: Int,
disableOnClick: Boolean,
enabled: Boolean = true,
color: Color = LocalContentColor.current,
onClick: () -> Unit
) {
var isClicked by remember { mutableStateOf(false) }
IconButton(
enabled = enabled && !isClicked,
onClick = {
if (disableOnClick) {
isClicked = true
CustomTooltip(text = stringResource(id = contentDescription)) {
IconButton(
enabled = enabled && !isClicked,
onClick = {
if (disableOnClick) {
isClicked = true
}
onClick()
}
onClick()
) {
Icon(
imageVector = icon,
modifier = modifier,
contentDescription = stringResource(id = contentDescription),
tint = color
)
}
) {
Icon(
imageVector = icon,
modifier = modifier,
contentDescription = contentDescription,
tint = color
)
}
}

View file

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

View file

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

View file

@ -3,7 +3,6 @@ package ua.acclorite.book_story.presentation.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import ua.acclorite.book_story.R
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 = {}) {
CustomIconButton(
icon = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.go_back_content_desc),
contentDescription = R.string.go_back_content_desc,
disableOnClick = true,
enabled = enabled
) {

View file

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

View file

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

View file

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

View file

@ -12,9 +12,9 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Divider
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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.DialogWindowProvider
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.
@ -72,7 +70,8 @@ fun CustomDialogWithContent(
customContent: @Composable (ColumnScope.() -> Unit) = {}
) {
var actionClicked by remember { mutableStateOf(false) }
AlertDialog(
BasicAlertDialog(
onDismissRequest = { onDismiss() },
properties = DialogProperties(
dismissOnBackPress = !actionClicked,
@ -88,7 +87,7 @@ fun CustomDialogWithContent(
.navigationBarsPadding()
.statusBarsPadding()
.clip(MaterialTheme.shapes.extraLarge)
.background(MaterialTheme.elevation(ElevationDefaults.Dialog))
.background(MaterialTheme.colorScheme.surfaceContainerHigh)
.padding(top = 24.dp, bottom = 12.dp)
) {
if (drawableIcon != null) {
@ -140,7 +139,7 @@ fun CustomDialogWithContent(
}
if (withDivider) {
Spacer(modifier = Modifier.height(12.dp))
Divider(
HorizontalDivider(
modifier = Modifier.padding(horizontal = 24.dp),
color = MaterialTheme.colorScheme.outlineVariant
)

View file

@ -13,9 +13,9 @@ import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Divider
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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.DialogWindowProvider
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) = {}
) {
var actionClicked by remember { mutableStateOf(false) }
AlertDialog(
BasicAlertDialog(
onDismissRequest = { onDismiss() },
properties = DialogProperties(
dismissOnBackPress = !actionClicked,
@ -90,7 +88,7 @@ fun CustomDialogWithLazyColumn(
.navigationBarsPadding()
.statusBarsPadding()
.clip(MaterialTheme.shapes.extraLarge)
.background(MaterialTheme.elevation(ElevationDefaults.Dialog))
.background(MaterialTheme.colorScheme.surfaceContainerHigh)
.padding(top = 24.dp, bottom = 12.dp)
) {
if (drawableIcon != null) {
@ -142,7 +140,7 @@ fun CustomDialogWithLazyColumn(
}
if (withDivider) {
Spacer(modifier = Modifier.height(12.dp))
Divider(
HorizontalDivider(
modifier = Modifier.padding(horizontal = 24.dp),
color = MaterialTheme.colorScheme.outlineVariant
)

View file

@ -29,7 +29,6 @@ import ua.acclorite.book_story.R
import ua.acclorite.book_story.domain.model.NavigationItem
import ua.acclorite.book_story.presentation.data.Navigator
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.
@ -54,7 +53,7 @@ fun BoxScope.CustomNavigationRail(
.align(
Alignment.CenterStart
)
.background(MaterialTheme.elevation())
.background(MaterialTheme.colorScheme.surfaceContainer)
.padding(
start = WindowInsets.navigationBars
.asPaddingValues()
@ -62,7 +61,7 @@ fun BoxScope.CustomNavigationRail(
)
.width(80.dp)
.padding(horizontal = 12.dp),
containerColor = MaterialTheme.elevation(),
containerColor = MaterialTheme.colorScheme.surfaceContainer,
windowInsets = WindowInsets(0, 0, 0, 0)
) {
Column(

View file

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

View file

@ -1,9 +1,5 @@
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 {
if (!this.contains('.'))
return this
@ -20,11 +16,3 @@ fun calculateFamiliarity(string: String, target: String): Int {
return familiarity
}
fun Path.standardQuadFromTo(from: Offset, to: Offset) {
quadraticTo(
from.x,
from.y,
abs(from.x + to.x) / 2f,
abs(from.y + to.y) / 2f
)
}

View file

@ -4,6 +4,7 @@ sealed class MainEvent {
data class OnChangeLanguage(val lang: String) : MainEvent()
data class OnChangeTheme(val theme: 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 OnChangeFontStyle(val fontStyle: Boolean) : MainEvent()
data class OnChangeFontSize(val fontSize: Int) : MainEvent()

View file

@ -5,6 +5,7 @@ import android.os.Parcelable
import androidx.compose.runtime.Immutable
import ua.acclorite.book_story.presentation.ui.DarkTheme
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.
@ -14,6 +15,7 @@ data class MainState(
val language: String? = null,
val theme: Theme? = null,
val darkTheme: DarkTheme? = null,
val themeContrast: ThemeContrast? = null,
val fontFamily: String? = null,
val isItalic: Boolean? = null,
val fontSize: Int? = null,
@ -32,6 +34,8 @@ data class MainState(
// String
darkTheme = DarkTheme.valueOf(parcel.readString() ?: DarkTheme.FOLLOW_SYSTEM.name),
// String
themeContrast = ThemeContrast.valueOf(parcel.readString() ?: ThemeContrast.STANDARD.name),
// String
fontFamily = parcel.readString(),
// Boolean
isItalic = if (parcel.readByte().toInt() != 0) parcel.readByte().toInt() == 1 else null,
@ -60,6 +64,8 @@ data class MainState(
parcel.writeString(theme?.name)
// Dark Theme
parcel.writeString(darkTheme?.name)
// Contrast Level
parcel.writeString(themeContrast?.name)
// Font Family
parcel.writeString(fontFamily)
// Is Italic

View file

@ -21,8 +21,10 @@ import ua.acclorite.book_story.domain.util.DataStoreConstants
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
import ua.acclorite.book_story.presentation.ui.DarkTheme
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.toTheme
import ua.acclorite.book_story.presentation.ui.toThemeContrast
import java.util.Locale
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 -> {
viewModelScope.launch(Dispatchers.IO) {
setDatastore.execute(DataStoreConstants.THEME, event.theme)
@ -200,6 +213,7 @@ class MainViewModel @Inject constructor(
return@combine value.language != null &&
value.theme != null &&
value.darkTheme != null &&
value.themeContrast != null &&
value.fontFamily != null &&
value.isItalic != 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
viewModelScope.launch(Dispatchers.IO) {
getDatastore

View file

@ -1,6 +1,7 @@
package ua.acclorite.book_story.presentation.data
import android.annotation.SuppressLint
import android.os.Parcelable
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.viewModels
@ -14,7 +15,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
@ -29,12 +29,15 @@ import dagger.hilt.android.lifecycle.withCreationCallback
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import ua.acclorite.book_story.presentation.ui.Transitions
import java.io.Serializable
private const val CURRENT_SCREEN = "current_screen"
private const val BACKSTACK = "back_stack"
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.
@ -57,11 +60,15 @@ enum class Screen {
START
}
/**
* Navigation Argument.
*/
@Immutable
data class Argument(
@Parcelize
data class Argument<out T : Serializable>(
val key: String,
val arg: Any?
)
val value: T
) : Parcelable
/**
* Navigator. Using for navigation between screens.
@ -78,38 +85,39 @@ class Navigator @AssistedInject constructor(
private val currentScreen = savedStateHandle.getStateFlow(CURRENT_SCREEN, startScreen)
private val useBackAnimation = savedStateHandle.getStateFlow(USE_BACK_ANIM, false)
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
for ((index, arg) in arguments.withIndex()) {
for ((index, arg) in arguments.value.withIndex()) {
if (arg.key == argument.key) {
arguments[index] = argument
arguments.value[index] = argument
found = true
break
}
}
if (!found) {
arguments.add(argument)
arguments.value.add(argument)
}
}
fun retrieveArgument(key: String): Any? {
for (arg in arguments) {
fun retrieveArgument(key: String): Serializable? {
for (arg in arguments.value) {
if (arg.key == key) {
return arg.arg
return arg.value
}
}
return null
}
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) {
backStack.value.add(currentScreen.value)
@ -121,20 +129,23 @@ class Navigator @AssistedInject constructor(
savedStateHandle[CURRENT_SCREEN] = screen
}
fun navigateWithoutBackStack(screen: Screen, useBackAnimation: Boolean, vararg args: Argument) =
viewModelScope.launch(Dispatchers.Default) {
if (backStack.value.last() == screen) {
backStack.value.removeLast()
}
args.forEach {
putArgument(it)
}
savedStateHandle[USE_BACK_ANIM] = useBackAnimation
savedStateHandle[CURRENT_SCREEN] = screen
fun navigateWithoutBackStack(
screen: Screen,
useBackAnimation: Boolean,
vararg args: Argument<Serializable>
) = viewModelScope.launch(Dispatchers.Default) {
if (backStack.value.last() == screen) {
backStack.value.removeLast()
}
args.forEach {
putArgument(it)
}
savedStateHandle[USE_BACK_ANIM] = useBackAnimation
savedStateHandle[CURRENT_SCREEN] = screen
}
fun navigateBack(useBackAnimation: Boolean = true) =
viewModelScope.launch(Dispatchers.Default) {
if (canGoBack()) {
@ -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 enterAnim Enter Animation.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -31,8 +31,6 @@ import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
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.
@ -79,7 +77,7 @@ fun BookInfoChangeCoverBottomSheet(
},
sheetState = rememberModalBottomSheetState(true),
windowInsets = WindowInsets(0, 0, 0, 0),
containerColor = MaterialTheme.elevation(ElevationDefaults.BottomSheet)
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
) {
BookInfoChangeCoverBottomSheetItem(

View file

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

View file

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

View file

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

View file

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

View file

@ -8,7 +8,13 @@ import ua.acclorite.book_story.domain.util.Constants
@Immutable
data class BookInfoState(
val book: Book = Constants.EMPTY_BOOK,
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 hasFocused: Boolean = false,
@ -17,9 +23,7 @@ data class BookInfoState(
val showChangeCoverBottomSheet: Boolean = false,
val showDetailsBottomSheet: Boolean = false,
val showMoreDropDown: Boolean = false,
val showDeleteDialog: Boolean = false,
val showMoveDialog: Boolean = false,
val selectedCategory: Category = Category.READING,
)

View file

@ -16,14 +16,15 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
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.History
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.GetBookFromFile
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.UpdateBooks
import ua.acclorite.book_story.domain.use_case.UpdateBooksWithText
@ -43,7 +44,8 @@ class BookInfoViewModel @Inject constructor(
private val insertHistory: InsertHistory,
private val deleteBooks: DeleteBooks,
private val getBookFromFile: GetBookFromFile,
private val getBookById: GetBooksById
private val getBookById: GetBooksById,
private val getText: GetText
) : ViewModel() {
private val _state = MutableStateFlow(BookInfoState())
@ -74,7 +76,7 @@ class BookInfoViewModel @Inject constructor(
val newCoverImage = getBookById.execute(
listOf(
_state.value.book.id ?: return@launch
_state.value.book.id
)
).first().coverImage
@ -174,14 +176,6 @@ class BookInfoViewModel @Inject constructor(
}
}
is BookInfoEvent.OnShowHideMoreDropDown -> {
_state.update {
it.copy(
showMoreDropDown = !it.showMoreDropDown
)
}
}
is BookInfoEvent.OnShowHideDeleteDialog -> {
_state.update {
it.copy(
@ -274,7 +268,9 @@ class BookInfoViewModel @Inject constructor(
if (event.durationMillis > 0) {
job = viewModelScope.launch(Dispatchers.IO) {
yield()
delay(event.durationMillis)
yield()
event.snackbarState.currentSnackbarData?.dismiss()
}
}
@ -293,7 +289,7 @@ class BookInfoViewModel @Inject constructor(
}
}
is BookInfoEvent.OnUpdateBook -> {
is BookInfoEvent.OnLoadUpdate -> {
viewModelScope.launch(Dispatchers.IO) {
_state.update {
it.copy(
@ -314,8 +310,7 @@ class BookInfoViewModel @Inject constructor(
action = event.context.getString(R.string.retry),
onAction = {
onEvent(
BookInfoEvent.OnUpdateBook(
refreshList = event.refreshList,
BookInfoEvent.OnLoadUpdate(
snackbarState = event.snackbarState,
context = event.context
)
@ -335,17 +330,15 @@ class BookInfoViewModel @Inject constructor(
}
val nullableBook = getBookFromFile.execute(_state.value.book.file!!)
if (nullableBook is NullableBook.Null) {
onEvent(
BookInfoEvent.OnShowSnackbar(
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),
onAction = {
onEvent(
BookInfoEvent.OnUpdateBook(
refreshList = event.refreshList,
BookInfoEvent.OnLoadUpdate(
snackbarState = event.snackbarState,
context = event.context
)
@ -366,24 +359,212 @@ class BookInfoViewModel @Inject constructor(
}
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 {
it.copy(
book = it.book.copy(
author = updatedBook.author,
description = updatedBook.description
author = author,
description = description,
text = text
)
)
}
updateBooksWithText.execute(
listOf(
_state.value.book.copy(
author = updatedBook.author,
description = updatedBook.description,
text = updatedBook.text
if (_state.value.textChanged) {
val isSuccess = updateBooksWithText.execute(
listOf(
_state.value.book
)
)
)
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)
onEvent(
@ -406,14 +587,13 @@ class BookInfoViewModel @Inject constructor(
is BookInfoEvent.OnNavigateToReaderScreen -> {
viewModelScope.launch {
_state.value.book.id?.let {
_state.value.book.id.let {
insertHistory.execute(
listOf(
History(
null,
it,
null,
Date().time
bookId = it,
book = null,
time = Date().time
)
)
)
@ -422,7 +602,7 @@ class BookInfoViewModel @Inject constructor(
Screen.READER,
false,
Argument(
"book", _state.value.book
"book", _state.value.book.id
)
)
}
@ -432,15 +612,22 @@ class BookInfoViewModel @Inject constructor(
fun init(navigator: Navigator) {
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()
return@launch
}
_state.update {
BookInfoState(book = book)
BookInfoState(book = book.first())
}
}
}

View file

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

View file

@ -1,7 +1,5 @@
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.background
import androidx.compose.foundation.border
@ -23,7 +21,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -71,24 +68,13 @@ fun BrowseFileItem(
val backgroundColor = if (file.second) MaterialTheme.colorScheme.secondaryContainer
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(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 3.dp)
.clip(RoundedCornerShape(10.dp))
.background(animatedBackgroundColor)
.background(backgroundColor)
.combinedClickable(
onLongClick = {
onLongClick()
@ -107,7 +93,7 @@ fun BrowseFileItem(
modifier = Modifier
.border(
1.dp,
animatedOutlineColor,
outlineColor,
RoundedCornerShape(6.dp)
)
.padding(14.dp),
@ -153,7 +139,11 @@ fun BrowseFileItem(
DefaultTransition(
visible = hasSelectedFiles
) {
CustomCheckbox(selected = file.second, size = 18.dp)
CustomCheckbox(
selected = file.second,
containerColor = MaterialTheme.colorScheme.surface,
size = 18.dp
)
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,6 @@
package ua.acclorite.book_story.presentation.screens.library.components.dialog
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.PagerState
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.
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LibraryMoveDialog(
viewModel: LibraryViewModel,

View file

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

View file

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

View file

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

View file

@ -21,7 +21,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
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.Scaffold
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.data.ReaderEvent
import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
import ua.acclorite.book_story.presentation.ui.elevation
@OptIn(FlowPreview::class)
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@ -84,7 +83,7 @@ fun ReaderScreen(
navigator: Navigator
) {
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) }
val listState = rememberLazyListState()
@ -100,7 +99,7 @@ fun ReaderScreen(
): Offset {
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))
}
@ -140,7 +139,7 @@ fun ReaderScreen(
snapshotFlow {
listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset
}.debounce(500).collectLatest { items ->
if (!loading) {
if (!loading && state.book.text.isNotEmpty() && listState.layoutInfo.totalItemsCount > 0) {
val lastVisibleItemIndex = listState.layoutInfo.visibleItemsInfo.last().index
val progress = if (items.first > 0) {
@ -386,8 +385,7 @@ fun ReaderScreen(
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth(0.55f),
strokeCap = StrokeCap.Round,
color = MaterialTheme.colorScheme.primary,
backgroundColor = MaterialTheme.elevation(2.dp)
trackColor = MaterialTheme.colorScheme.secondaryContainer.copy(0.7f)
)
}
}

View file

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

View file

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

View file

@ -4,7 +4,9 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
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.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState
@ -74,35 +76,34 @@ fun ReaderBottomBar(
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(3.dp))
Slider(
value = state.book.progress,
enabled = !state.lockMenu,
onValueChange = {
viewModel.onEvent(ReaderEvent.OnScroll(listState, it))
viewModel.onEvent(
ReaderEvent.OnChangeProgress(
progress = it,
navigator = navigator,
firstVisibleItemIndex = listState.firstVisibleItemIndex,
firstVisibleItemOffset = 0,
refreshList = { book ->
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(book))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(book))
}
if (listState.layoutInfo.totalItemsCount > 0) {
viewModel.onEvent(ReaderEvent.OnScroll(listState, it))
viewModel.onEvent(
ReaderEvent.OnChangeProgress(
progress = it,
navigator = navigator,
firstVisibleItemIndex = listState.firstVisibleItemIndex,
firstVisibleItemOffset = 0,
refreshList = { book ->
libraryViewModel.onEvent(LibraryEvent.OnUpdateBook(book))
historyViewModel.onEvent(HistoryEvent.OnUpdateBook(book))
}
)
)
)
}
},
colors = SliderDefaults.colors(
activeTrackColor = MaterialTheme.colorScheme.primary,
thumbColor = MaterialTheme.colorScheme.primary,
inactiveTrackColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(0.15f),
disabledActiveTickColor = Color.Transparent,
disabledActiveTrackColor = Color.Transparent,
disabledInactiveTickColor = Color.Transparent,
disabledInactiveTrackColor = Color.Transparent,
disabledThumbColor = Color.Transparent,
activeTickColor = Color.Transparent,
inactiveTickColor = Color.Transparent
inactiveTrackColor = MaterialTheme.colorScheme.secondary.copy(0.15f),
disabledActiveTrackColor = MaterialTheme.colorScheme.primary,
disabledThumbColor = MaterialTheme.colorScheme.primary,
disabledInactiveTrackColor = MaterialTheme.colorScheme.secondary.copy(0.15f),
)
)
Spacer(modifier = Modifier.height(5.dp))
}
}

View file

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

View file

@ -4,7 +4,7 @@ import androidx.activity.ComponentActivity
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
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.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
@ -27,6 +27,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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.unit.dp
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.presentation.components.CategoryTitle
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.ColorPickerWithTitle
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.
*/
@OptIn(
ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class
ExperimentalMaterial3Api::class
)
@Composable
fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderViewModel) {
@ -98,195 +97,199 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
)
}
ModalBottomSheet(
scrimColor = animatedScrimColor,
dragHandle = {},
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(animatedHeight),
onDismissRequest = {
viewModel.onEvent(ReaderEvent.OnShowHideSettingsBottomSheet)
},
sheetState = rememberModalBottomSheetState(true),
windowInsets = WindowInsets(0, 0, 0, 0),
containerColor = MaterialTheme.elevation(ElevationDefaults.BottomSheet)
) {
ReaderSettingsBottomSheetTabRow(viewModel = viewModel, pagerState = pagerState)
Box(modifier = Modifier.fillMaxSize()) {
ModalBottomSheet(
scrimColor = animatedScrimColor,
dragHandle = {},
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.fillMaxHeight(animatedHeight),
onDismissRequest = {
viewModel.onEvent(ReaderEvent.OnShowHideSettingsBottomSheet)
},
sheetState = rememberModalBottomSheetState(true),
windowInsets = WindowInsets(0, 0, 0, 0),
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
) {
ReaderSettingsBottomSheetTabRow(viewModel = viewModel, pagerState = pagerState)
HorizontalPager(state = pagerState, modifier = Modifier.fillMaxSize()) { page ->
LazyColumn(Modifier.fillMaxSize()) {
if (page == 0) {
item {
Spacer(modifier = Modifier.height(22.dp))
}
item {
CategoryTitle(
title = stringResource(id = R.string.font_reader_settings),
color = MaterialTheme.colorScheme.onSurface
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
}
item {
ChipsWithTitle(
title = stringResource(id = R.string.font_family_option),
chips = Constants.FONTS
.map {
ChipItem(
it.id,
it.fontName.asString(),
HorizontalPager(state = pagerState) { page ->
LazyColumn {
if (page == 0) {
item {
Spacer(modifier = Modifier.height(22.dp))
}
item {
CategoryTitle(
title = stringResource(id = R.string.font_reader_settings),
color = MaterialTheme.colorScheme.onSurface
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
}
item {
ChipsWithTitle(
title = stringResource(id = R.string.font_family_option),
chips = Constants.FONTS
.map {
ButtonItem(
it.id,
it.fontName.asString(),
MaterialTheme.typography.labelLarge.copy(
fontFamily = it.font
),
it.id == fontFamily.id
)
},
onClick = {
mainViewModel.onEvent(MainEvent.OnChangeFontFamily(it.id))
}
)
}
item {
ChipsWithTitle(
title = stringResource(id = R.string.font_style_option),
chips = listOf(
ButtonItem(
"normal",
stringResource(id = R.string.font_style_normal),
MaterialTheme.typography.labelLarge.copy(
fontFamily = it.font
fontFamily = fontFamily.font,
fontStyle = FontStyle.Normal
),
it.id == fontFamily.id
)
},
onClick = {
mainViewModel.onEvent(MainEvent.OnChangeFontFamily(it.id))
}
)
}
item {
ChipsWithTitle(
title = stringResource(id = R.string.font_style_option),
chips = listOf(
ChipItem(
"normal",
stringResource(id = R.string.font_style_normal),
MaterialTheme.typography.labelLarge.copy(
fontFamily = fontFamily.font,
fontStyle = FontStyle.Normal
!state.isItalic!!
),
!state.isItalic!!
),
ChipItem(
"italic",
stringResource(id = R.string.font_style_italic),
MaterialTheme.typography.labelLarge.copy(
fontFamily = fontFamily.font,
fontStyle = FontStyle.Italic
ButtonItem(
"italic",
stringResource(id = R.string.font_style_italic),
MaterialTheme.typography.labelLarge.copy(
fontFamily = fontFamily.font,
fontStyle = FontStyle.Italic
),
state.isItalic!!
),
state.isItalic!!
),
),
onClick = {
mainViewModel.onEvent(
MainEvent.OnChangeFontStyle(
when (it.id) {
"italic" -> true
else -> false
}
onClick = {
mainViewModel.onEvent(
MainEvent.OnChangeFontStyle(
when (it.id) {
"italic" -> true
else -> false
}
)
)
)
}
)
}
item {
SliderWithTitle(
value = state.fontSize!! to "pt",
fromValue = 10,
toValue = 35,
title = stringResource(id = R.string.font_size_option),
onValueChange = {
}
)
}
item {
SliderWithTitle(
value = state.fontSize!! to "pt",
fromValue = 10,
toValue = 35,
title = stringResource(id = R.string.font_size_option),
onValueChange = {
mainViewModel.onEvent(
MainEvent.OnChangeFontSize(it)
)
}
)
}
item {
Spacer(modifier = Modifier.height(22.dp))
}
item {
CategoryTitle(
title = stringResource(id = R.string.text_reader_settings),
color = MaterialTheme.colorScheme.onSurface
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
}
item {
SliderWithTitle(
value = state.lineHeight!! to "pt",
fromValue = 1,
toValue = 16,
title = stringResource(id = R.string.line_height_option),
onValueChange = {
mainViewModel.onEvent(
MainEvent.OnChangeLineHeight(it)
)
}
)
}
item {
SliderWithTitle(
value = state.paragraphHeight!! to "pt",
fromValue = 0,
toValue = 24,
title = stringResource(id = R.string.paragraph_height_option),
onValueChange = {
mainViewModel.onEvent(
MainEvent.OnChangeParagraphHeight(it)
)
}
)
}
item {
CheckboxWithTitle(
selected = state.paragraphIndentation!!,
title = stringResource(id = R.string.paragraph_indentation_option)
) {
mainViewModel.onEvent(
MainEvent.OnChangeFontSize(it)
MainEvent.OnChangeParagraphIndentation(!state.paragraphIndentation!!)
)
}
)
}
}
item {
Spacer(modifier = Modifier.height(22.dp))
}
item {
CategoryTitle(
title = stringResource(id = R.string.text_reader_settings),
color = MaterialTheme.colorScheme.onSurface
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
}
item {
SliderWithTitle(
value = state.lineHeight!! to "pt",
fromValue = 1,
toValue = 16,
title = stringResource(id = R.string.line_height_option),
onValueChange = {
mainViewModel.onEvent(
MainEvent.OnChangeLineHeight(it)
)
}
)
}
item {
SliderWithTitle(
value = state.paragraphHeight!! to "pt",
fromValue = 0,
toValue = 24,
title = stringResource(id = R.string.paragraph_height_option),
onValueChange = {
mainViewModel.onEvent(
MainEvent.OnChangeParagraphHeight(it)
)
}
)
}
item {
CheckboxWithTitle(
selected = state.paragraphIndentation!!,
title = stringResource(id = R.string.paragraph_indentation_option)
) {
mainViewModel.onEvent(
MainEvent.OnChangeParagraphIndentation(!state.paragraphIndentation!!)
if (page == 1) {
item {
Spacer(modifier = Modifier.height(14.dp))
}
item {
ColorPickerWithTitle(
value = Color(state.backgroundColor!!.toULong()),
title = stringResource(id = R.string.background_color_option),
onValueChange = {
mainViewModel.onEvent(
MainEvent.OnChangeBackgroundColor(
it.value.toLong()
)
)
}
)
}
item {
ColorPickerWithTitle(
value = Color(state.fontColor!!.toULong()),
title = stringResource(id = R.string.font_color_option),
onValueChange = {
mainViewModel.onEvent(
MainEvent.OnChangeFontColor(
it.value.toLong()
)
)
}
)
}
}
}
if (page == 1) {
item {
Spacer(modifier = Modifier.height(14.dp))
}
item {
ColorPickerWithTitle(
value = Color(state.backgroundColor!!.toULong()),
title = stringResource(id = R.string.background_color_option),
onValueChange = {
mainViewModel.onEvent(
MainEvent.OnChangeBackgroundColor(
it.value.toLong()
)
)
}
Spacer(
modifier = Modifier.height(
8.dp + navigationBarPadding
)
)
}
item {
ColorPickerWithTitle(
value = Color(state.fontColor!!.toULong()),
title = stringResource(id = R.string.font_color_option),
onValueChange = {
mainViewModel.onEvent(
MainEvent.OnChangeFontColor(
it.value.toLong()
)
)
}
)
}
}
item {
Spacer(
modifier = Modifier.height(
8.dp + navigationBarPadding
)
)
}
}
}
}
}

View file

@ -1,22 +1,16 @@
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.padding
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PrimaryTabRow
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.runtime.Composable
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.unit.dp
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.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.
*/
@OptIn(ExperimentalFoundationApi::class)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ReaderSettingsBottomSheetTabRow(viewModel: ReaderViewModel, pagerState: PagerState) {
val tabItems = listOf(
@ -32,27 +26,11 @@ fun ReaderSettingsBottomSheetTabRow(viewModel: ReaderViewModel, pagerState: Page
stringResource(id = R.string.color_tab)
)
TabRow(
PrimaryTabRow(
modifier = Modifier
.fillMaxWidth(),
selectedTabIndex = pagerState.currentPage,
divider = {
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
)
}
containerColor = Color.Transparent
) {
tabItems.forEachIndexed { index, tabItem ->
Tab(
@ -62,7 +40,7 @@ fun ReaderSettingsBottomSheetTabRow(viewModel: ReaderViewModel, pagerState: Page
viewModel.onEvent(ReaderEvent.OnScrollToSettingsPage(index, pagerState))
},
selectedContentColor = MaterialTheme.colorScheme.primary,
unselectedContentColor = MaterialTheme.colorScheme.onSurface,
unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant,
text = {
Text(
tabItem,

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
package ua.acclorite.book_story.presentation.screens.settings.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ChipDefaults
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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
/**
* 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
fun ChipsWithTitle(
title: String,
chips: List<ChipItem>,
chips: List<ButtonItem>,
horizontalPadding: Dp = 18.dp,
verticalPadding: Dp = 8.dp,
onClick: (ChipItem) -> Unit
onClick: (ButtonItem) -> Unit
) {
Column(
Modifier
@ -50,29 +45,15 @@ fun ChipsWithTitle(
FilterChip(
modifier = Modifier.height(36.dp),
selected = item.selected,
enabled = !item.selected,
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
),
label = {
Text(
item.title,
style = item.textStyle,
maxLines = 1
)
},
onClick = { onClick(item) },
) {
Text(
item.title,
style = item.textStyle,
color = if (item.selected) {
MaterialTheme.colorScheme.onSecondaryContainer
} else {
MaterialTheme.colorScheme.onSurfaceVariant
},
maxLines = 1
)
}
)
}
},
)

View file

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

View file

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

View file

@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -54,7 +55,11 @@ fun SliderWithTitle(
onValueChange = {
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,
onValueChange = {
onValueChange(it)
}
},
colors = SliderDefaults.colors(
activeTickColor = MaterialTheme.colorScheme.onPrimary,
inactiveTickColor = MaterialTheme.colorScheme.onSurfaceVariant,
)
)
}
}

View file

@ -24,17 +24,18 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
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.GoBackButton
import ua.acclorite.book_story.presentation.data.MainEvent
import ua.acclorite.book_story.presentation.data.MainViewModel
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.SegmentedButtonWithTitle
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.elevation
import ua.acclorite.book_story.presentation.ui.Theme
import ua.acclorite.book_story.presentation.ui.ThemeContrast
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -68,7 +69,7 @@ fun AppearanceSettings(
scrollBehavior = scrollBehavior,
colors = TopAppBarDefaults.largeTopAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
scrolledContainerColor = MaterialTheme.elevation()
scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer
)
)
}
@ -93,10 +94,11 @@ fun AppearanceSettings(
}
item {
ChipsWithTitle(
SegmentedButtonWithTitle(
title = stringResource(id = R.string.dark_theme_option),
chips = DarkTheme.entries.map {
ChipItem(
enabled = true,
buttons = DarkTheme.entries.map {
ButtonItem(
it.toString(),
when (it) {
DarkTheme.OFF -> stringResource(id = R.string.dark_theme_off)
@ -106,7 +108,7 @@ fun AppearanceSettings(
MaterialTheme.typography.labelLarge,
it == state.darkTheme
)
},
}
) {
mainViewModel.onEvent(
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 {
AppearanceSettingsThemeSwitcher(mainViewModel = mainViewModel)
}

View file

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

View file

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

View file

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

View file

@ -25,7 +25,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
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.presentation.components.CategoryTitle
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.ChipsWithTitle
import ua.acclorite.book_story.presentation.screens.settings.components.SliderWithTitle
import ua.acclorite.book_story.presentation.ui.elevation
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -74,7 +73,7 @@ fun ReaderSettings(
scrollBehavior = scrollBehavior,
colors = TopAppBarDefaults.largeTopAppBarColors(
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),
chips = Constants.FONTS
.map {
ChipItem(
ButtonItem(
it.id,
it.fontName.asString(),
MaterialTheme.typography.labelLarge.copy(
@ -120,7 +119,7 @@ fun ReaderSettings(
ChipsWithTitle(
title = stringResource(id = R.string.font_style_option),
chips = listOf(
ChipItem(
ButtonItem(
"normal",
stringResource(id = R.string.font_style_normal),
MaterialTheme.typography.labelLarge.copy(
@ -129,7 +128,7 @@ fun ReaderSettings(
),
!state.isItalic!!
),
ChipItem(
ButtonItem(
"italic",
stringResource(id = R.string.font_style_italic),
MaterialTheme.typography.labelLarge.copy(

View file

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

View file

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

View file

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

View file

@ -5,132 +5,472 @@ import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
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 md_theme_dark_onPrimary = Color(0xFF00363E)
private val md_theme_dark_primaryContainer = Color(0xFF004E59)
private val md_theme_dark_onPrimaryContainer = Color(0xFFA1EFFF)
private val md_theme_dark_secondary = Color(0xFFB1CBD1)
private val md_theme_dark_onSecondary = Color(0xFF1C3439)
private val md_theme_dark_secondaryContainer = Color(0xFF334A50)
private val md_theme_dark_onSecondaryContainer = Color(0xFFCDE7ED)
private val md_theme_dark_tertiary = Color(0xFFBCC5EB)
private val md_theme_dark_onTertiary = Color(0xFF262F4D)
private val md_theme_dark_tertiaryContainer = Color(0xFF3C4665)
private val md_theme_dark_onTertiaryContainer = Color(0xFFDBE1FF)
private val md_theme_dark_error = Color(0xFFFFB4AB)
private val md_theme_dark_errorContainer = Color(0xFF93000A)
private val md_theme_dark_onError = Color(0xFF690005)
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
private val md_theme_dark_background = Color(0xFF191C1D)
private val md_theme_dark_onBackground = Color(0xFFE1E3E3)
private val md_theme_dark_surface = Color(0xFF191C1D)
private val md_theme_dark_onSurface = Color(0xFFE1E3E3)
private val md_theme_dark_surfaceVariant = Color(0xFF3F484A)
private val md_theme_dark_onSurfaceVariant = Color(0xFFBFC8CA)
private val md_theme_dark_outline = Color(0xFF899295)
private val md_theme_dark_inverseOnSurface = Color(0xFF191C1D)
private val md_theme_dark_inverseSurface = Color(0xFFE1E3E3)
private val md_theme_dark_inversePrimary = Color(0xFF006876)
private val md_theme_dark_surfaceTint = Color(0xFF51D7EF)
private val md_theme_dark_outlineVariant = Color(0xFF3F484A)
private val md_theme_dark_scrim = Color(0xFF000000)
private val primaryLight = Color(0xFF116682)
private val onPrimaryLight = Color(0xFFFFFFFF)
private val primaryContainerLight = Color(0xFFBDE9FF)
private val onPrimaryContainerLight = Color(0xFF001F2A)
private val secondaryLight = Color(0xFF4D616C)
private val onSecondaryLight = Color(0xFFFFFFFF)
private val secondaryContainerLight = Color(0xFFD0E6F2)
private val onSecondaryContainerLight = Color(0xFF081E27)
private val tertiaryLight = Color(0xFF5D5B7D)
private val onTertiaryLight = Color(0xFFFFFFFF)
private val tertiaryContainerLight = Color(0xFFE3DFFF)
private val onTertiaryContainerLight = Color(0xFF191836)
private val errorLight = Color(0xFFBA1A1A)
private val onErrorLight = Color(0xFFFFFFFF)
private val errorContainerLight = Color(0xFFFFDAD6)
private val onErrorContainerLight = Color(0xFF410002)
private val backgroundLight = Color(0xFFF6FAFD)
private val onBackgroundLight = Color(0xFF171C1F)
private val surfaceLight = Color(0xFFF6FAFD)
private val onSurfaceLight = Color(0xFF171C1F)
private val surfaceVariantLight = Color(0xFFDCE4E9)
private val onSurfaceVariantLight = Color(0xFF40484C)
private val outlineLight = Color(0xFF70787D)
private val outlineVariantLight = Color(0xFFC0C8CD)
private val scrimLight = Color(0xFF000000)
private val inverseSurfaceLight = Color(0xFF2C3134)
private val inverseOnSurfaceLight = Color(0xFFEDF1F5)
private val inversePrimaryLight = Color(0xFF8BD0EF)
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
fun aquaTheme(isDark: Boolean): ColorScheme {
fun aquaTheme(isDark: Boolean, themeContrast: ThemeContrast): ColorScheme {
return if (isDark) {
darkColorScheme(
primary = md_theme_dark_primary,
onPrimary = md_theme_dark_onPrimary,
primaryContainer = md_theme_dark_primaryContainer,
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
secondary = md_theme_dark_secondary,
onSecondary = md_theme_dark_onSecondary,
secondaryContainer = md_theme_dark_secondaryContainer,
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
tertiary = md_theme_dark_tertiary,
onTertiary = md_theme_dark_onTertiary,
tertiaryContainer = md_theme_dark_tertiaryContainer,
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
error = md_theme_dark_error,
errorContainer = md_theme_dark_errorContainer,
onError = md_theme_dark_onError,
onErrorContainer = md_theme_dark_onErrorContainer,
background = md_theme_dark_background,
onBackground = md_theme_dark_onBackground,
surface = md_theme_dark_surface,
onSurface = md_theme_dark_onSurface,
surfaceVariant = md_theme_dark_surfaceVariant,
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
outline = md_theme_dark_outline,
inverseOnSurface = md_theme_dark_inverseOnSurface,
inverseSurface = md_theme_dark_inverseSurface,
inversePrimary = md_theme_dark_inversePrimary,
surfaceTint = md_theme_dark_surfaceTint,
outlineVariant = md_theme_dark_outlineVariant,
scrim = md_theme_dark_scrim
)
when (themeContrast) {
ThemeContrast.STANDARD -> {
darkColorScheme(
primary = primaryDark,
onPrimary = onPrimaryDark,
primaryContainer = primaryContainerDark,
onPrimaryContainer = onPrimaryContainerDark,
secondary = secondaryDark,
onSecondary = onSecondaryDark,
secondaryContainer = secondaryContainerDark,
onSecondaryContainer = onSecondaryContainerDark,
tertiary = tertiaryDark,
onTertiary = onTertiaryDark,
tertiaryContainer = tertiaryContainerDark,
onTertiaryContainer = onTertiaryContainerDark,
error = errorDark,
onError = onErrorDark,
errorContainer = errorContainerDark,
onErrorContainer = onErrorContainerDark,
background = backgroundDark,
onBackground = onBackgroundDark,
surface = surfaceDark,
onSurface = onSurfaceDark,
surfaceVariant = surfaceVariantDark,
onSurfaceVariant = onSurfaceVariantDark,
outline = outlineDark,
outlineVariant = outlineVariantDark,
scrim = scrimDark,
inverseSurface = inverseSurfaceDark,
inverseOnSurface = inverseOnSurfaceDark,
inversePrimary = inversePrimaryDark,
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 {
lightColorScheme(
primary = md_theme_light_primary,
onPrimary = md_theme_light_onPrimary,
primaryContainer = md_theme_light_primaryContainer,
onPrimaryContainer = md_theme_light_onPrimaryContainer,
secondary = md_theme_light_secondary,
onSecondary = md_theme_light_onSecondary,
secondaryContainer = md_theme_light_secondaryContainer,
onSecondaryContainer = md_theme_light_onSecondaryContainer,
tertiary = md_theme_light_tertiary,
onTertiary = md_theme_light_onTertiary,
tertiaryContainer = md_theme_light_tertiaryContainer,
onTertiaryContainer = md_theme_light_onTertiaryContainer,
error = md_theme_light_error,
errorContainer = md_theme_light_errorContainer,
onError = md_theme_light_onError,
onErrorContainer = md_theme_light_onErrorContainer,
background = md_theme_light_background,
onBackground = md_theme_light_onBackground,
surface = md_theme_light_surface,
onSurface = md_theme_light_onSurface,
surfaceVariant = md_theme_light_surfaceVariant,
onSurfaceVariant = md_theme_light_onSurfaceVariant,
outline = md_theme_light_outline,
inverseOnSurface = md_theme_light_inverseOnSurface,
inverseSurface = md_theme_light_inverseSurface,
inversePrimary = md_theme_light_inversePrimary,
surfaceTint = md_theme_light_surfaceTint,
outlineVariant = md_theme_light_outlineVariant,
scrim = md_theme_light_scrim
)
when (themeContrast) {
ThemeContrast.STANDARD -> {
lightColorScheme(
primary = primaryLight,
onPrimary = onPrimaryLight,
primaryContainer = primaryContainerLight,
onPrimaryContainer = onPrimaryContainerLight,
secondary = secondaryLight,
onSecondary = onSecondaryLight,
secondaryContainer = secondaryContainerLight,
onSecondaryContainer = onSecondaryContainerLight,
tertiary = tertiaryLight,
onTertiary = onTertiaryLight,
tertiaryContainer = tertiaryContainerLight,
onTertiaryContainer = onTertiaryContainerLight,
error = errorLight,
onError = onErrorLight,
errorContainer = errorContainerLight,
onErrorContainer = onErrorContainerLight,
background = backgroundLight,
onBackground = onBackgroundLight,
surface = surfaceLight,
onSurface = onSurfaceLight,
surfaceVariant = surfaceVariantLight,
onSurfaceVariant = onSurfaceVariantLight,
outline = outlineLight,
outlineVariant = outlineVariantLight,
scrim = scrimLight,
inverseSurface = inverseSurfaceLight,
inverseOnSurface = inverseOnSurfaceLight,
inversePrimary = inversePrimaryLight,
surfaceDim = surfaceDimLight,
surfaceBright = surfaceBrightLight,
surfaceContainerLowest = surfaceContainerLowestLight,
surfaceContainerLow = surfaceContainerLowLight,
surfaceContainer = surfaceContainerLight,
surfaceContainerHigh = surfaceContainerHighLight,
surfaceContainerHighest = surfaceContainerHighestLight,
)
}
ThemeContrast.MEDIUM -> {
lightColorScheme(
primary = primaryLightMediumContrast,
onPrimary = onPrimaryLightMediumContrast,
primaryContainer = primaryContainerLightMediumContrast,
onPrimaryContainer = onPrimaryContainerLightMediumContrast,
secondary = secondaryLightMediumContrast,
onSecondary = onSecondaryLightMediumContrast,
secondaryContainer = secondaryContainerLightMediumContrast,
onSecondaryContainer = onSecondaryContainerLightMediumContrast,
tertiary = tertiaryLightMediumContrast,
onTertiary = onTertiaryLightMediumContrast,
tertiaryContainer = tertiaryContainerLightMediumContrast,
onTertiaryContainer = onTertiaryContainerLightMediumContrast,
error = errorLightMediumContrast,
onError = onErrorLightMediumContrast,
errorContainer = errorContainerLightMediumContrast,
onErrorContainer = onErrorContainerLightMediumContrast,
background = backgroundLightMediumContrast,
onBackground = onBackgroundLightMediumContrast,
surface = surfaceLightMediumContrast,
onSurface = onSurfaceLightMediumContrast,
surfaceVariant = surfaceVariantLightMediumContrast,
onSurfaceVariant = onSurfaceVariantLightMediumContrast,
outline = outlineLightMediumContrast,
outlineVariant = outlineVariantLightMediumContrast,
scrim = scrimLightMediumContrast,
inverseSurface = inverseSurfaceLightMediumContrast,
inverseOnSurface = inverseOnSurfaceLightMediumContrast,
inversePrimary = inversePrimaryLightMediumContrast,
surfaceDim = surfaceDimLightMediumContrast,
surfaceBright = surfaceBrightLightMediumContrast,
surfaceContainerLowest = surfaceContainerLowestLightMediumContrast,
surfaceContainerLow = surfaceContainerLowLightMediumContrast,
surfaceContainer = surfaceContainerLightMediumContrast,
surfaceContainerHigh = surfaceContainerHighLightMediumContrast,
surfaceContainerHighest = surfaceContainerHighestLightMediumContrast,
)
}
ThemeContrast.HIGH -> {
lightColorScheme(
primary = primaryLightHighContrast,
onPrimary = onPrimaryLightHighContrast,
primaryContainer = primaryContainerLightHighContrast,
onPrimaryContainer = onPrimaryContainerLightHighContrast,
secondary = secondaryLightHighContrast,
onSecondary = onSecondaryLightHighContrast,
secondaryContainer = secondaryContainerLightHighContrast,
onSecondaryContainer = onSecondaryContainerLightHighContrast,
tertiary = tertiaryLightHighContrast,
onTertiary = onTertiaryLightHighContrast,
tertiaryContainer = tertiaryContainerLightHighContrast,
onTertiaryContainer = onTertiaryContainerLightHighContrast,
error = errorLightHighContrast,
onError = onErrorLightHighContrast,
errorContainer = errorContainerLightHighContrast,
onErrorContainer = onErrorContainerLightHighContrast,
background = backgroundLightHighContrast,
onBackground = onBackgroundLightHighContrast,
surface = surfaceLightHighContrast,
onSurface = onSurfaceLightHighContrast,
surfaceVariant = surfaceVariantLightHighContrast,
onSurfaceVariant = onSurfaceVariantLightHighContrast,
outline = outlineLightHighContrast,
outlineVariant = outlineVariantLightHighContrast,
scrim = scrimLightHighContrast,
inverseSurface = inverseSurfaceLightHighContrast,
inverseOnSurface = inverseOnSurfaceLightHighContrast,
inversePrimary = inversePrimaryLightHighContrast,
surfaceDim = surfaceDimLightHighContrast,
surfaceBright = surfaceBrightLightHighContrast,
surfaceContainerLowest = surfaceContainerLowestLightHighContrast,
surfaceContainerLow = surfaceContainerLowLightHighContrast,
surfaceContainer = surfaceContainerLightHighContrast,
surfaceContainerHigh = surfaceContainerHighLightHighContrast,
surfaceContainerHighest = surfaceContainerHighestLightHighContrast,
)
}
}
}
}

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