0.9.2 - Massive scrolling performance improvement
|
|
@ -15,7 +15,7 @@ android {
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "0.9.0"
|
versionName = "0.9.2"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|
@ -139,4 +139,6 @@ dependencies {
|
||||||
|
|
||||||
implementation("androidx.appcompat:appcompat:latest.release")
|
implementation("androidx.appcompat:appcompat:latest.release")
|
||||||
implementation("androidx.appcompat:appcompat-resources:latest.release")
|
implementation("androidx.appcompat:appcompat-resources:latest.release")
|
||||||
|
|
||||||
|
implementation("io.coil-kt:coil-compose:2.6.0")
|
||||||
}
|
}
|
||||||
|
|
@ -35,7 +35,6 @@
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".Activity"
|
android:name=".Activity"
|
||||||
android:configChanges="screenSize|screenLayout|smallestScreenSize|locale|density|uiMode"
|
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:theme="@style/Theme.Start.Splash">
|
android:theme="@style/Theme.Start.Splash">
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
package ua.acclorite.book_story
|
package ua.acclorite.book_story
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.database.CursorWindow
|
import android.database.CursorWindow
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
|
@ -28,7 +26,6 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.presentation.components.bottom_navigation_bar.BottomNavigationBar
|
import ua.acclorite.book_story.presentation.components.bottom_navigation_bar.BottomNavigationBar
|
||||||
import ua.acclorite.book_story.presentation.components.custom_navigation_rail.CustomNavigationRail
|
import ua.acclorite.book_story.presentation.components.custom_navigation_rail.CustomNavigationRail
|
||||||
import ua.acclorite.book_story.presentation.data.MainEvent
|
|
||||||
import ua.acclorite.book_story.presentation.data.MainViewModel
|
import ua.acclorite.book_story.presentation.data.MainViewModel
|
||||||
import ua.acclorite.book_story.presentation.data.NavigationHost
|
import ua.acclorite.book_story.presentation.data.NavigationHost
|
||||||
import ua.acclorite.book_story.presentation.data.Screen
|
import ua.acclorite.book_story.presentation.data.Screen
|
||||||
|
|
@ -55,7 +52,7 @@ import java.lang.reflect.Field
|
||||||
@SuppressLint("DiscouragedPrivateApi")
|
@SuppressLint("DiscouragedPrivateApi")
|
||||||
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class Activity : ComponentActivity() {
|
class Activity : AppCompatActivity() {
|
||||||
private val mainViewModel: MainViewModel by viewModels()
|
private val mainViewModel: MainViewModel by viewModels()
|
||||||
private val libraryViewModel: LibraryViewModel by viewModels()
|
private val libraryViewModel: LibraryViewModel by viewModels()
|
||||||
private val historyViewModel: HistoryViewModel by viewModels()
|
private val historyViewModel: HistoryViewModel by viewModels()
|
||||||
|
|
@ -75,10 +72,7 @@ class Activity : ComponentActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initializing all variables
|
// Initializing all variables
|
||||||
mainViewModel.init(
|
mainViewModel.init(libraryViewModel)
|
||||||
activity = this,
|
|
||||||
libraryViewModel,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Splash screen
|
// Splash screen
|
||||||
installSplashScreen().apply {
|
installSplashScreen().apply {
|
||||||
|
|
@ -93,7 +87,6 @@ class Activity : ComponentActivity() {
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val windowClass = calculateWindowSizeClass(activity = this)
|
val windowClass = calculateWindowSizeClass(activity = this)
|
||||||
val updating = mainViewModel.updating.collectAsState().value
|
|
||||||
|
|
||||||
val theme = mainViewModel.theme.collectAsState().value ?: Theme.BLUE
|
val theme = mainViewModel.theme.collectAsState().value ?: Theme.BLUE
|
||||||
val darkTheme =
|
val darkTheme =
|
||||||
|
|
@ -104,7 +97,6 @@ class Activity : ComponentActivity() {
|
||||||
theme = theme,
|
theme = theme,
|
||||||
isDark = darkTheme.isDark()
|
isDark = darkTheme.isDark()
|
||||||
) {
|
) {
|
||||||
if (!updating) {
|
|
||||||
NavigationHost(startScreen = Screen.LIBRARY) {
|
NavigationHost(startScreen = Screen.LIBRARY) {
|
||||||
val currentScreen by this.getCurrentScreen().collectAsState()
|
val currentScreen by this.getCurrentScreen().collectAsState()
|
||||||
|
|
||||||
|
|
@ -240,21 +232,7 @@ class Activity : ComponentActivity() {
|
||||||
// StartScreen()
|
// StartScreen()
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(
|
|
||||||
MaterialTheme.colorScheme.surface
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
||||||
super.onConfigurationChanged(newConfig)
|
|
||||||
mainViewModel.onEvent(MainEvent.OnLocaleUpdate(this))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -9,7 +9,6 @@ import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.data.local.dto.BookEntity
|
import ua.acclorite.book_story.data.local.dto.BookEntity
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.domain.model.StringWithId
|
|
||||||
import ua.acclorite.book_story.util.UIText
|
import ua.acclorite.book_story.util.UIText
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
@ -22,12 +21,12 @@ class BookMapperImpl @Inject constructor() : BookMapper {
|
||||||
val stream = ByteArrayOutputStream()
|
val stream = ByteArrayOutputStream()
|
||||||
book.coverImage?.asAndroidBitmap()?.compress(
|
book.coverImage?.asAndroidBitmap()?.compress(
|
||||||
if (legacyAPI) Bitmap.CompressFormat.WEBP
|
if (legacyAPI) Bitmap.CompressFormat.WEBP
|
||||||
else Bitmap.CompressFormat.WEBP_LOSSLESS,
|
else Bitmap.CompressFormat.WEBP_LOSSY,
|
||||||
if (legacyAPI) 30 else 100,
|
0,
|
||||||
stream
|
stream
|
||||||
)
|
)
|
||||||
|
|
||||||
val text = book.text.joinToString("\n") {
|
val textAsString = book.text.joinToString("\n") {
|
||||||
it.line.trim()
|
it.line.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,7 +36,7 @@ class BookMapperImpl @Inject constructor() : BookMapper {
|
||||||
filePath = book.filePath,
|
filePath = book.filePath,
|
||||||
progress = book.progress,
|
progress = book.progress,
|
||||||
author = book.author.string,
|
author = book.author.string,
|
||||||
text = text,
|
text = textAsString,
|
||||||
description = book.description,
|
description = book.description,
|
||||||
image = stream.toByteArray(),
|
image = stream.toByteArray(),
|
||||||
category = book.category
|
category = book.category
|
||||||
|
|
@ -64,7 +63,7 @@ class BookMapperImpl @Inject constructor() : BookMapper {
|
||||||
description = bookEntity.description,
|
description = bookEntity.description,
|
||||||
progress = bookEntity.progress,
|
progress = bookEntity.progress,
|
||||||
file = if (file.exists()) file else null,
|
file = if (file.exists()) file else null,
|
||||||
text = bookEntity.text.split("\n").map { StringWithId(it) },
|
text = emptyList(),
|
||||||
filePath = bookEntity.filePath,
|
filePath = bookEntity.filePath,
|
||||||
lastOpened = null,
|
lastOpened = null,
|
||||||
category = bookEntity.category,
|
category = bookEntity.category,
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,13 @@ class EpubFileParser @Inject constructor() : FileParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (description != null) {
|
if (description != null) {
|
||||||
if (description.isEmpty()) {
|
if (description.isBlank()) {
|
||||||
description = null
|
description = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return Book(
|
return Book(
|
||||||
id = null,
|
id = null,
|
||||||
title = title,
|
title = title,
|
||||||
|
|
|
||||||
|
|
@ -32,15 +32,27 @@ class EpubTextParser @Inject constructor() : TextParser {
|
||||||
for (spineReference in book.spine.spineReferences) {
|
for (spineReference in book.spine.spineReferences) {
|
||||||
val resource = spineReference.resource
|
val resource = spineReference.resource
|
||||||
val inputStream = resource.inputStream
|
val inputStream = resource.inputStream
|
||||||
val reader =
|
val reader = BufferedReader(
|
||||||
BufferedReader(InputStreamReader(inputStream, Charset.forName("UTF-8")))
|
InputStreamReader(
|
||||||
|
inputStream,
|
||||||
|
Charset.forName("UTF-8")
|
||||||
|
)
|
||||||
|
)
|
||||||
var line: String?
|
var line: String?
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
inputStream.close()
|
||||||
|
}
|
||||||
|
|
||||||
while (withContext(Dispatchers.IO) {
|
while (withContext(Dispatchers.IO) {
|
||||||
reader.readLine()
|
reader.readLine()
|
||||||
}.also { line = it } != null) {
|
}.also { line = it } != null) {
|
||||||
unformattedText.append(line).append("\n")
|
unformattedText.append(line).append("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
reader.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val stringWithIds = mutableListOf<StringWithId>()
|
val stringWithIds = mutableListOf<StringWithId>()
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,11 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
return database.findBooksById(ids).map { bookMapper.toBook(it) }
|
return database.findBooksById(ids).map { bookMapper.toBook(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun getBookTextById(bookId: Int): String {
|
||||||
|
val book = database.findBookById(bookId)
|
||||||
|
return book.text
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun findBook(
|
override suspend fun findBook(
|
||||||
id: Int
|
id: Int
|
||||||
): BookEntity {
|
): BookEntity {
|
||||||
|
|
@ -101,11 +106,39 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun updateBooks(books: List<Book>) {
|
override suspend fun updateBooks(books: List<Book>) {
|
||||||
|
// without text
|
||||||
|
database.updateBooks(
|
||||||
|
books.map {
|
||||||
|
val book = database.findBookById(it.id ?: return)
|
||||||
|
bookMapper.toBookEntity(
|
||||||
|
it.copy(
|
||||||
|
text = book.text
|
||||||
|
.split("\n")
|
||||||
|
.map { line -> StringWithId(line.trim()) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun updateBooksWithText(books: List<Book>) {
|
||||||
database.updateBooks(books.map { bookMapper.toBookEntity(it) })
|
database.updateBooks(books.map { bookMapper.toBookEntity(it) })
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun deleteBooks(books: List<Book>) {
|
override suspend fun deleteBooks(books: List<Book>) {
|
||||||
database.deleteBooks(books.map { bookMapper.toBookEntity(it) })
|
// without text
|
||||||
|
database.deleteBooks(
|
||||||
|
books.map {
|
||||||
|
val book = database.findBookById(it.id ?: return)
|
||||||
|
bookMapper.toBookEntity(
|
||||||
|
it.copy(
|
||||||
|
text = book.text
|
||||||
|
.split("\n")
|
||||||
|
.map { line -> StringWithId(line.trim()) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun <T> retrieveDataFromDataStore(
|
override suspend fun <T> retrieveDataFromDataStore(
|
||||||
|
|
@ -119,7 +152,7 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
dataStore.putData(key, value)
|
dataStore.putData(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getFilesFromDownloads(query: String): Flow<Resource<List<File>>> {
|
override suspend fun getFilesFromDevice(query: String): Flow<Resource<List<File>>> {
|
||||||
fun getAllFilesInDirectory(directory: File): List<File> {
|
fun getAllFilesInDirectory(directory: File): List<File> {
|
||||||
val filesList = mutableListOf<File>()
|
val filesList = mutableListOf<File>()
|
||||||
|
|
||||||
|
|
@ -140,20 +173,15 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
return flow {
|
return flow {
|
||||||
emit(Resource.Loading(true))
|
|
||||||
|
|
||||||
val existingBooks = database
|
val existingBooks = database
|
||||||
.searchBooks("")
|
.searchBooks("")
|
||||||
.map { bookMapper.toBook(it) }
|
.map { bookMapper.toBook(it) }
|
||||||
.filter { it.file != null }
|
|
||||||
|
|
||||||
val allFiles = getAllFilesInDirectory(
|
val primaryDirectory = Environment.getExternalStorageDirectory()
|
||||||
Environment
|
val allFiles = getAllFilesInDirectory(primaryDirectory)
|
||||||
.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (allFiles.isEmpty()) {
|
if (allFiles.isEmpty()) {
|
||||||
emit(Resource.Loading(false))
|
emit(Resource.Success(null))
|
||||||
return@flow
|
return@flow
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,7 +196,8 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val isFileAlreadyAdded = existingBooks.all {
|
val isFileAlreadyAdded = existingBooks.all {
|
||||||
it.file != file
|
it.filePath.substringAfterLast("/") !=
|
||||||
|
file.path.substringAfterLast("/")
|
||||||
}
|
}
|
||||||
val isQuery = if (query.isEmpty()) true else file.name.lowercase()
|
val isQuery = if (query.isEmpty()) true else file.name.lowercase()
|
||||||
.contains(query.trim().lowercase())
|
.contains(query.trim().lowercase())
|
||||||
|
|
@ -193,7 +222,6 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
}.toMutableList()
|
}.toMutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(Resource.Loading(false))
|
|
||||||
emit(
|
emit(
|
||||||
Resource.Success(
|
Resource.Success(
|
||||||
data = filteredFiles
|
data = filteredFiles
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,10 @@ interface BookRepository {
|
||||||
ids: List<Int>
|
ids: List<Int>
|
||||||
): List<Book>
|
): List<Book>
|
||||||
|
|
||||||
|
suspend fun getBookTextById(
|
||||||
|
bookId: Int
|
||||||
|
): String
|
||||||
|
|
||||||
suspend fun findBook(
|
suspend fun findBook(
|
||||||
id: Int
|
id: Int
|
||||||
): BookEntity
|
): BookEntity
|
||||||
|
|
@ -36,6 +40,10 @@ interface BookRepository {
|
||||||
books: List<Book>
|
books: List<Book>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
suspend fun updateBooksWithText(
|
||||||
|
books: List<Book>
|
||||||
|
)
|
||||||
|
|
||||||
suspend fun deleteBooks(
|
suspend fun deleteBooks(
|
||||||
books: List<Book>
|
books: List<Book>
|
||||||
)
|
)
|
||||||
|
|
@ -50,7 +58,7 @@ interface BookRepository {
|
||||||
value: T
|
value: T
|
||||||
)
|
)
|
||||||
|
|
||||||
suspend fun getFilesFromDownloads(query: String = ""): Flow<Resource<List<File>>>
|
suspend fun getFilesFromDevice(query: String = ""): Flow<Resource<List<File>>>
|
||||||
|
|
||||||
suspend fun getBookTextFromFile(file: File): Flow<Resource<List<StringWithId>>>
|
suspend fun getBookTextFromFile(file: File): Flow<Resource<List<StringWithId>>>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,17 @@
|
||||||
package ua.acclorite.book_story.domain.use_case
|
package ua.acclorite.book_story.domain.use_case
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.core.os.LocaleListCompat
|
||||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||||
import ua.acclorite.book_story.util.DataStoreConstants
|
import ua.acclorite.book_story.util.DataStoreConstants
|
||||||
import java.util.Locale
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ChangeLanguage @Inject constructor(private val repository: BookRepository) {
|
class ChangeLanguage @Inject constructor(private val repository: BookRepository) {
|
||||||
@SuppressLint("AppBundleLocaleChanges")
|
@SuppressLint("AppBundleLocaleChanges")
|
||||||
suspend fun execute(language: String, activity: ComponentActivity) {
|
suspend fun execute(language: String) {
|
||||||
val config = activity.resources.configuration
|
val appLocale = LocaleListCompat.forLanguageTags(language)
|
||||||
val resources = activity.resources
|
AppCompatDelegate.setApplicationLocales(appLocale)
|
||||||
val locale = Locale(language)
|
|
||||||
|
|
||||||
Locale.setDefault(locale)
|
|
||||||
config.setLocale(locale)
|
|
||||||
|
|
||||||
activity.createConfigurationContext(config)
|
|
||||||
resources.updateConfiguration(
|
|
||||||
config,
|
|
||||||
resources.displayMetrics
|
|
||||||
)
|
|
||||||
|
|
||||||
repository.putDataToDataStore(
|
repository.putDataToDataStore(
|
||||||
DataStoreConstants.LANGUAGE,
|
DataStoreConstants.LANGUAGE,
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,11 @@ import ua.acclorite.book_story.util.Resource
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class GetFilesFromDownloads @Inject constructor(
|
class GetFilesFromDevice @Inject constructor(
|
||||||
private val repository: BookRepository
|
private val repository: BookRepository
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun execute(query: String): Flow<Resource<List<File>>> {
|
suspend fun execute(query: String): Flow<Resource<List<File>>> {
|
||||||
return repository.getFilesFromDownloads(query)
|
return repository.getFilesFromDevice(query)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,15 +1,11 @@
|
||||||
package ua.acclorite.book_story.domain.use_case
|
package ua.acclorite.book_story.domain.use_case
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import ua.acclorite.book_story.domain.model.StringWithId
|
|
||||||
import ua.acclorite.book_story.domain.repository.BookRepository
|
import ua.acclorite.book_story.domain.repository.BookRepository
|
||||||
import ua.acclorite.book_story.util.Resource
|
|
||||||
import java.io.File
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class GetText @Inject constructor(private val repository: BookRepository) {
|
class GetText @Inject constructor(private val repository: BookRepository) {
|
||||||
|
|
||||||
suspend fun execute(file: File): Flow<Resource<List<StringWithId>>> {
|
suspend fun execute(id: Int): String {
|
||||||
return repository.getBookTextFromFile(file)
|
return repository.getBookTextById(bookId = id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
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 UpdateBooksWithText @Inject constructor(private val repository: BookRepository) {
|
||||||
|
|
||||||
|
suspend fun execute(books: List<Book>) {
|
||||||
|
repository.updateBooksWithText(books)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
package ua.acclorite.book_story.presentation.components
|
package ua.acclorite.book_story.presentation.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.Spring
|
import androidx.compose.animation.core.Spring
|
||||||
import androidx.compose.animation.core.spring
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
|
@ -22,8 +25,9 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.ui.DefaultTransition
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.ui.elevation
|
import ua.acclorite.book_story.ui.elevation
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -38,7 +42,7 @@ import ua.acclorite.book_story.ui.elevation
|
||||||
@Composable
|
@Composable
|
||||||
fun AnimatedTopAppBar(
|
fun AnimatedTopAppBar(
|
||||||
containerColor: Color = MaterialTheme.colorScheme.surface,
|
containerColor: Color = MaterialTheme.colorScheme.surface,
|
||||||
scrolledContainerColor: Color? = MaterialTheme.elevation(),
|
scrolledContainerColor: Color = MaterialTheme.elevation(),
|
||||||
|
|
||||||
scrollBehavior: TopAppBarScrollBehavior?,
|
scrollBehavior: TopAppBarScrollBehavior?,
|
||||||
isTopBarScrolled: Boolean?,
|
isTopBarScrolled: Boolean?,
|
||||||
|
|
@ -58,79 +62,90 @@ fun AnimatedTopAppBar(
|
||||||
content3Title: @Composable () -> Unit = {},
|
content3Title: @Composable () -> Unit = {},
|
||||||
content3Actions: @Composable RowScope.() -> Unit = {}
|
content3Actions: @Composable RowScope.() -> Unit = {}
|
||||||
) {
|
) {
|
||||||
//todo fix lags.
|
|
||||||
Box(modifier = Modifier.fillMaxWidth()) {
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
if ((scrollBehavior != null || isTopBarScrolled != null) && scrolledContainerColor != null) {
|
val density = LocalDensity.current
|
||||||
|
val statusBarPadding = with(density) {
|
||||||
val fraction by remember {
|
WindowInsets.statusBars.getTop(density).toDp()
|
||||||
derivedStateOf {
|
|
||||||
scrollBehavior?.state?.overlappedFraction ?: 0f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val isScrolled = if (scrollBehavior != null) {
|
|
||||||
fraction > 0.01f
|
|
||||||
} else isTopBarScrolled ?: false
|
|
||||||
|
|
||||||
val color = if (isScrolled) scrolledContainerColor else containerColor
|
|
||||||
val animatedColor by animateColorAsState(
|
|
||||||
targetValue = color,
|
|
||||||
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
|
|
||||||
label = "TopAppBar color animation"
|
|
||||||
)
|
|
||||||
|
|
||||||
val statusBarPadding = with(LocalDensity.current) {
|
|
||||||
WindowInsets.statusBars.getTop(LocalDensity.current).toDp()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (containerColor != Color.Transparent) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(64.dp + statusBarPadding)
|
.height(64.dp + statusBarPadding)
|
||||||
.background(
|
.background(
|
||||||
animatedColor
|
if (isTopBarScrolled == true) scrolledContainerColor
|
||||||
|
else containerColor
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
DefaultTransition(visible = content1Visibility ?: true) {
|
val scrollableContainerColor by remember(isTopBarScrolled) {
|
||||||
|
derivedStateOf {
|
||||||
|
if (isTopBarScrolled == true) {
|
||||||
|
scrolledContainerColor
|
||||||
|
} else {
|
||||||
|
containerColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val animatedContainerColor by animateColorAsState(
|
||||||
|
targetValue = scrollableContainerColor,
|
||||||
|
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
|
||||||
|
label = stringResource(id = R.string.top_app_bar_anim_content_desc)
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = content1Visibility ?: true,
|
||||||
|
enter = fadeIn(spring(stiffness = Spring.StiffnessMediumLow)),
|
||||||
|
exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow))
|
||||||
|
) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
navigationIcon = content1NavigationIcon,
|
navigationIcon = content1NavigationIcon,
|
||||||
title = content1Title,
|
title = content1Title,
|
||||||
actions = content1Actions,
|
actions = content1Actions,
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = animatedContainerColor,
|
||||||
scrolledContainerColor = Color.Transparent
|
scrolledContainerColor = scrolledContainerColor
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content2Visibility != null) {
|
if (content2Visibility != null) {
|
||||||
DefaultTransition(visible = content2Visibility) {
|
AnimatedVisibility(
|
||||||
|
visible = content2Visibility,
|
||||||
|
enter = fadeIn(spring(stiffness = Spring.StiffnessMediumLow)),
|
||||||
|
exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow))
|
||||||
|
) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
navigationIcon = content2NavigationIcon,
|
navigationIcon = content2NavigationIcon,
|
||||||
title = content2Title,
|
title = content2Title,
|
||||||
actions = content2Actions,
|
actions = content2Actions,
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = animatedContainerColor,
|
||||||
scrolledContainerColor = Color.Transparent
|
scrolledContainerColor = scrolledContainerColor
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (content3Visibility != null) {
|
if (content3Visibility != null) {
|
||||||
DefaultTransition(visible = content3Visibility) {
|
AnimatedVisibility(
|
||||||
|
visible = content3Visibility,
|
||||||
|
enter = fadeIn(spring(stiffness = Spring.StiffnessMediumLow)),
|
||||||
|
exit = fadeOut(spring(stiffness = Spring.StiffnessMediumLow))
|
||||||
|
) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
navigationIcon = content3NavigationIcon,
|
navigationIcon = content3NavigationIcon,
|
||||||
title = content3Title,
|
title = content3Title,
|
||||||
actions = content3Actions,
|
actions = content3Actions,
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = Color.Transparent,
|
containerColor = animatedContainerColor,
|
||||||
scrolledContainerColor = Color.Transparent
|
scrolledContainerColor = scrolledContainerColor
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,17 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.ui.elevation
|
import ua.acclorite.book_story.ui.elevation
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Checkbox. Has a Circle shape.
|
* Custom Checkbox. Has a Circle shape.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomCheckbox(selected: Boolean) {
|
fun CustomCheckbox(selected: Boolean, size: Dp = 22.dp) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Check,
|
imageVector = Icons.Default.Check,
|
||||||
tint = if (selected) MaterialTheme.elevation(elevation = 2.dp) else Color.Transparent,
|
tint = if (selected) MaterialTheme.elevation(elevation = 2.dp) else Color.Transparent,
|
||||||
|
|
@ -36,7 +39,7 @@ fun CustomCheckbox(selected: Boolean) {
|
||||||
shape = CircleShape
|
shape = CircleShape
|
||||||
)
|
)
|
||||||
.padding(4.dp)
|
.padding(4.dp)
|
||||||
.size(22.dp),
|
.size(size),
|
||||||
contentDescription = "checkbox"
|
contentDescription = stringResource(id = R.string.checkbox_content_desc)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package ua.acclorite.book_story.presentation.components
|
||||||
|
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Icon Button. Can be disabled after click, so user can't press button fast 2 or 3 times.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun CustomIconButton(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
icon: ImageVector,
|
||||||
|
contentDescription: String,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
onClick()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
modifier = modifier,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
tint = color
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -127,7 +127,6 @@ private class CustomSelectionToolbar(
|
||||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
|
||||||
override var status: TextToolbarStatus by mutableStateOf(TextToolbarStatus.Hidden)
|
override var status: TextToolbarStatus by mutableStateOf(TextToolbarStatus.Hidden)
|
||||||
private set
|
|
||||||
|
|
||||||
override fun showMenu(
|
override fun showMenu(
|
||||||
rect: Rect,
|
rect: Rect,
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,7 @@ package ua.acclorite.book_story.presentation.components
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.presentation.data.Navigator
|
import ua.acclorite.book_story.presentation.data.Navigator
|
||||||
|
|
@ -19,20 +12,12 @@ import ua.acclorite.book_story.presentation.data.Navigator
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun GoBackButton(navigator: Navigator, customOnClick: () -> Unit = {}) {
|
fun GoBackButton(navigator: Navigator, customOnClick: () -> Unit = {}) {
|
||||||
var isClicked by remember { mutableStateOf(false) }
|
CustomIconButton(
|
||||||
|
icon = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
IconButton(
|
contentDescription = stringResource(id = R.string.go_back_content_desc),
|
||||||
enabled = !isClicked,
|
disableOnClick = true
|
||||||
onClick = {
|
) {
|
||||||
isClicked = true
|
|
||||||
navigator.navigateBack()
|
navigator.navigateBack()
|
||||||
customOnClick()
|
customOnClick()
|
||||||
}
|
}
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
|
||||||
contentDescription = stringResource(id = R.string.go_back_content_desc),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -2,13 +2,10 @@ package ua.acclorite.book_story.presentation.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
|
@ -16,7 +13,6 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -31,19 +27,17 @@ import ua.acclorite.book_story.presentation.data.Screen
|
||||||
fun MoreDropDown(navigator: Navigator) {
|
fun MoreDropDown(navigator: Navigator) {
|
||||||
var showDropDown by remember { mutableStateOf(false) }
|
var showDropDown by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val startPadding = 12.dp
|
val startPadding = remember { 12.dp }
|
||||||
val endPadding = 36.dp
|
val endPadding = remember { 36.dp }
|
||||||
|
|
||||||
Box {
|
Box {
|
||||||
IconButton(onClick = {
|
CustomIconButton(
|
||||||
|
icon = Icons.Default.MoreVert,
|
||||||
|
contentDescription = stringResource(id = R.string.show_dropdown_content_desc),
|
||||||
|
disableOnClick = false,
|
||||||
|
enabled = !showDropDown
|
||||||
|
) {
|
||||||
showDropDown = true
|
showDropDown = true
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.MoreVert,
|
|
||||||
contentDescription = "Show drop down",
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
|
|
@ -51,7 +45,6 @@ fun MoreDropDown(navigator: Navigator) {
|
||||||
onDismissRequest = { showDropDown = false },
|
onDismissRequest = { showDropDown = false },
|
||||||
offset = DpOffset(10.dp, 0.dp)
|
offset = DpOffset(10.dp, 0.dp)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ import ua.acclorite.book_story.ui.elevation
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bottom navigation bar, uses default [NavigationBar].
|
* Bottom navigation bar, uses default [NavigationBar].
|
||||||
|
*
|
||||||
|
* @param navigator Navigator.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun BottomNavigationBar(
|
fun BottomNavigationBar(
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import androidx.compose.material3.NavigationBarItem
|
||||||
import androidx.compose.material3.NavigationBarItemDefaults
|
import androidx.compose.material3.NavigationBarItemDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -23,6 +24,11 @@ fun RowScope.BottomNavigationBarItem(
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val icon = remember(isSelected) {
|
||||||
|
if (isSelected) item.selectedIcon
|
||||||
|
else item.unselectedIcon
|
||||||
|
}
|
||||||
|
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
label = {
|
label = {
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -37,9 +43,7 @@ fun RowScope.BottomNavigationBarItem(
|
||||||
onClick = { onClick() },
|
onClick = { onClick() },
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
painter =
|
painter = icon,
|
||||||
if (isSelected) item.selectedIcon
|
|
||||||
else item.unselectedIcon,
|
|
||||||
contentDescription = item.title,
|
contentDescription = item.title,
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
|
@ -69,6 +73,7 @@ fun CustomDialogWithContent(
|
||||||
withDivider: Boolean,
|
withDivider: Boolean,
|
||||||
customContent: @Composable (ColumnScope.() -> Unit) = {}
|
customContent: @Composable (ColumnScope.() -> Unit) = {}
|
||||||
) {
|
) {
|
||||||
|
var actionClicked by remember { mutableStateOf(false) }
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { onDismiss() },
|
onDismissRequest = { onDismiss() },
|
||||||
properties = properties
|
properties = properties
|
||||||
|
|
@ -151,7 +156,13 @@ fun CustomDialogWithContent(
|
||||||
.align(Alignment.End)
|
.align(Alignment.End)
|
||||||
.padding(horizontal = 24.dp)
|
.padding(horizontal = 24.dp)
|
||||||
) {
|
) {
|
||||||
TextButton(onClick = { onDismiss() }) {
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
actionClicked = true
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
enabled = !actionClicked
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.cancel),
|
text = stringResource(id = R.string.cancel),
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
|
@ -161,8 +172,11 @@ fun CustomDialogWithContent(
|
||||||
if (actionText != null) {
|
if (actionText != null) {
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { onAction() },
|
onClick = {
|
||||||
enabled = isActionEnabled == true
|
actionClicked = true
|
||||||
|
onAction()
|
||||||
|
},
|
||||||
|
enabled = isActionEnabled == true && !actionClicked
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = actionText,
|
text = actionText,
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
|
@ -71,6 +75,7 @@ fun CustomDialogWithLazyColumn(
|
||||||
withDivider: Boolean,
|
withDivider: Boolean,
|
||||||
items: (LazyListScope.() -> Unit) = {}
|
items: (LazyListScope.() -> Unit) = {}
|
||||||
) {
|
) {
|
||||||
|
var actionClicked by remember { mutableStateOf(false) }
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { onDismiss() },
|
onDismissRequest = { onDismiss() },
|
||||||
properties = properties
|
properties = properties
|
||||||
|
|
@ -156,7 +161,13 @@ fun CustomDialogWithLazyColumn(
|
||||||
.align(Alignment.End)
|
.align(Alignment.End)
|
||||||
.padding(horizontal = 24.dp)
|
.padding(horizontal = 24.dp)
|
||||||
) {
|
) {
|
||||||
TextButton(onClick = { onDismiss() }) {
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
actionClicked = true
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
enabled = !actionClicked
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.cancel),
|
text = stringResource(id = R.string.cancel),
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
|
@ -166,8 +177,11 @@ fun CustomDialogWithLazyColumn(
|
||||||
if (actionText != null) {
|
if (actionText != null) {
|
||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { onAction() },
|
onClick = {
|
||||||
enabled = isActionEnabled == true
|
actionClicked = true
|
||||||
|
onAction()
|
||||||
|
},
|
||||||
|
enabled = isActionEnabled == true && !actionClicked
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = actionText,
|
text = actionText,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import androidx.compose.material3.NavigationRailItem
|
||||||
import androidx.compose.material3.NavigationRailItemDefaults
|
import androidx.compose.material3.NavigationRailItemDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -22,6 +23,11 @@ fun CustomNavigationRailItem(
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val icon = remember(isSelected) {
|
||||||
|
if (isSelected) item.selectedIcon
|
||||||
|
else item.unselectedIcon
|
||||||
|
}
|
||||||
|
|
||||||
NavigationRailItem(
|
NavigationRailItem(
|
||||||
label = {
|
label = {
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -36,9 +42,7 @@ fun CustomNavigationRailItem(
|
||||||
onClick = { onClick() },
|
onClick = { onClick() },
|
||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
painter =
|
painter = icon,
|
||||||
if (isSelected) item.selectedIcon
|
|
||||||
else item.unselectedIcon,
|
|
||||||
contentDescription = item.title,
|
contentDescription = item.title,
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
package ua.acclorite.book_story.presentation.data
|
package ua.acclorite.book_story.presentation.data
|
||||||
|
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
|
|
||||||
sealed class MainEvent {
|
sealed class MainEvent {
|
||||||
data class OnLocaleUpdate(val activity: ComponentActivity) : MainEvent()
|
data class OnChangeLanguage(val lang: String) : MainEvent()
|
||||||
data class OnChangeLanguage(val lang: String, val activity: ComponentActivity) : MainEvent()
|
|
||||||
data class OnChangeTheme(val theme: String) : MainEvent()
|
data class OnChangeTheme(val theme: String) : MainEvent()
|
||||||
data class OnChangeDarkTheme(val darkTheme: String) : MainEvent()
|
data class OnChangeDarkTheme(val darkTheme: String) : MainEvent()
|
||||||
data class OnChangeFontFamily(val fontFamily: String) : MainEvent()
|
data class OnChangeFontFamily(val fontFamily: String) : MainEvent()
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
package ua.acclorite.book_story.presentation.data
|
package ua.acclorite.book_story.presentation.data
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
@ -44,12 +42,16 @@ class MainViewModel @Inject constructor(
|
||||||
private val _isReady = MutableStateFlow(false)
|
private val _isReady = MutableStateFlow(false)
|
||||||
val isReady = _isReady.asStateFlow()
|
val isReady = _isReady.asStateFlow()
|
||||||
|
|
||||||
private val _updating = MutableStateFlow(false)
|
|
||||||
val updating = _updating.asStateFlow()
|
|
||||||
|
|
||||||
/* -- Language ----------------------------------------------------- */
|
/* -- Language ----------------------------------------------------- */
|
||||||
private var _language: String =
|
private var _language: String =
|
||||||
stateHandle[Constants.LANGUAGE] ?: Locale.getDefault().language
|
stateHandle[Constants.LANGUAGE]
|
||||||
|
?: if (
|
||||||
|
Constants.LANGUAGES.any { Locale.getDefault().language.take(2) == it.first }
|
||||||
|
) {
|
||||||
|
Locale.getDefault().language.take(2)
|
||||||
|
} else {
|
||||||
|
"en"
|
||||||
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
stateHandle[Constants.LANGUAGE] = value
|
stateHandle[Constants.LANGUAGE] = value
|
||||||
|
|
@ -172,23 +174,9 @@ class MainViewModel @Inject constructor(
|
||||||
fun onEvent(event: MainEvent) {
|
fun onEvent(event: MainEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is MainEvent.OnChangeLanguage -> {
|
is MainEvent.OnChangeLanguage -> {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.Main) {
|
||||||
_updating.update { true }
|
changeLanguage.execute(event.lang)
|
||||||
|
|
||||||
changeLanguage.execute(event.lang, event.activity)
|
|
||||||
_language = event.lang
|
_language = event.lang
|
||||||
delay(100)
|
|
||||||
|
|
||||||
_updating.update { false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is MainEvent.OnLocaleUpdate -> {
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
_updating.update { true }
|
|
||||||
changeLanguage.execute(_language, event.activity)
|
|
||||||
delay(10)
|
|
||||||
_updating.update { false }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -266,7 +254,6 @@ class MainViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun init(
|
fun init(
|
||||||
activity: ComponentActivity,
|
|
||||||
libraryViewModel: LibraryViewModel,
|
libraryViewModel: LibraryViewModel,
|
||||||
) {
|
) {
|
||||||
val isViewModelsReady = combine(
|
val isViewModelsReady = combine(
|
||||||
|
|
@ -311,11 +298,11 @@ class MainViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Language
|
// Language
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.Main) {
|
||||||
getDatastore
|
getDatastore
|
||||||
.execute(DataStoreConstants.LANGUAGE, _language)
|
.execute(DataStoreConstants.LANGUAGE, _language)
|
||||||
.first {
|
.first {
|
||||||
onEvent(MainEvent.OnChangeLanguage(it, activity))
|
onEvent(MainEvent.OnChangeLanguage(it))
|
||||||
it.isNotBlank()
|
it.isNotBlank()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Done
|
import androidx.compose.material.icons.filled.Done
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.material.pullrefresh.PullRefreshDefaults
|
|
||||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||||
import androidx.compose.material.pullrefresh.pullRefresh
|
import androidx.compose.material.pullrefresh.pullRefresh
|
||||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||||
|
|
@ -34,7 +33,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
|
@ -58,6 +56,7 @@ import dagger.hilt.android.lifecycle.withCreationCallback
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
|
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
|
||||||
|
import ua.acclorite.book_story.presentation.components.CustomIconButton
|
||||||
import ua.acclorite.book_story.presentation.components.CustomSnackbar
|
import ua.acclorite.book_story.presentation.components.CustomSnackbar
|
||||||
import ua.acclorite.book_story.presentation.components.GoBackButton
|
import ua.acclorite.book_story.presentation.components.GoBackButton
|
||||||
import ua.acclorite.book_story.presentation.data.Argument
|
import ua.acclorite.book_story.presentation.data.Argument
|
||||||
|
|
@ -114,8 +113,6 @@ fun BookInfoScreen(
|
||||||
val snackbarState = remember { SnackbarHostState() }
|
val snackbarState = remember { SnackbarHostState() }
|
||||||
val refreshState = rememberPullRefreshState(
|
val refreshState = rememberPullRefreshState(
|
||||||
refreshing = state.isRefreshing,
|
refreshing = state.isRefreshing,
|
||||||
refreshThreshold = PullRefreshDefaults.RefreshThreshold + 32.dp,
|
|
||||||
refreshingOffset = PullRefreshDefaults.RefreshingOffset + 64.dp,
|
|
||||||
onRefresh = {
|
onRefresh = {
|
||||||
viewModel.onEvent(
|
viewModel.onEvent(
|
||||||
BookInfoEvent.OnUpdateBook(
|
BookInfoEvent.OnUpdateBook(
|
||||||
|
|
@ -193,9 +190,12 @@ fun BookInfoScreen(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
content1Actions = {
|
content1Actions = {
|
||||||
IconButton(
|
CustomIconButton(
|
||||||
enabled = !state.isRefreshing,
|
icon = Icons.Default.Refresh,
|
||||||
onClick = {
|
contentDescription = stringResource(id = R.string.refresh_book_content_desc),
|
||||||
|
disableOnClick = false,
|
||||||
|
enabled = !state.isRefreshing
|
||||||
|
) {
|
||||||
viewModel.onEvent(
|
viewModel.onEvent(
|
||||||
BookInfoEvent.OnUpdateBook(
|
BookInfoEvent.OnUpdateBook(
|
||||||
refreshList = {
|
refreshList = {
|
||||||
|
|
@ -207,14 +207,6 @@ fun BookInfoScreen(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Refresh,
|
|
||||||
contentDescription = stringResource(id = R.string.refresh_book_content_desc),
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Box {
|
Box {
|
||||||
DefaultTransition(
|
DefaultTransition(
|
||||||
|
|
@ -230,10 +222,18 @@ fun BookInfoScreen(
|
||||||
enter = Transitions.DefaultTransitionIn,
|
enter = Transitions.DefaultTransitionIn,
|
||||||
exit = fadeOut(tween(200))
|
exit = fadeOut(tween(200))
|
||||||
) {
|
) {
|
||||||
IconButton(
|
CustomIconButton(
|
||||||
|
icon = Icons.Default.Done,
|
||||||
|
contentDescription = stringResource(id = R.string.apply_changes_content_desc),
|
||||||
|
disableOnClick = true,
|
||||||
enabled = state.titleValue.isNotBlank() &&
|
enabled = state.titleValue.isNotBlank() &&
|
||||||
state.titleValue.trim() != state.book.title.trim(),
|
state.titleValue.trim() !=
|
||||||
onClick = {
|
state.book.title.trim(),
|
||||||
|
color = if (state.titleValue.isNotBlank()
|
||||||
|
&& state.titleValue != state.book.title
|
||||||
|
) MaterialTheme.colorScheme.onSurface
|
||||||
|
else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
) {
|
||||||
viewModel.onEvent(BookInfoEvent.OnUpdateTitle(
|
viewModel.onEvent(BookInfoEvent.OnUpdateTitle(
|
||||||
refreshList = {
|
refreshList = {
|
||||||
libraryViewModel.onEvent(
|
libraryViewModel.onEvent(
|
||||||
|
|
@ -247,18 +247,6 @@ fun BookInfoScreen(
|
||||||
Toast.LENGTH_LONG
|
Toast.LENGTH_LONG
|
||||||
).show()
|
).show()
|
||||||
}
|
}
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Done,
|
|
||||||
contentDescription = "Apply changes",
|
|
||||||
tint =
|
|
||||||
if (state.titleValue.isNotBlank()
|
|
||||||
&& state.titleValue != state.book.title
|
|
||||||
)
|
|
||||||
MaterialTheme.colorScheme.onSurface
|
|
||||||
else MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -354,7 +342,9 @@ fun BookInfoScreen(
|
||||||
PullRefreshIndicator(
|
PullRefreshIndicator(
|
||||||
state.isRefreshing,
|
state.isRefreshing,
|
||||||
refreshState,
|
refreshState,
|
||||||
Modifier.align(Alignment.TopCenter),
|
Modifier
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
.padding(top = paddingValues.calculateTopPadding()),
|
||||||
backgroundColor = MaterialTheme.colorScheme.inverseSurface,
|
backgroundColor = MaterialTheme.colorScheme.inverseSurface,
|
||||||
contentColor = MaterialTheme.colorScheme.inverseOnSurface
|
contentColor = MaterialTheme.colorScheme.inverseOnSurface
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -36,10 +36,12 @@ import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
|
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
|
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
|
||||||
|
|
@ -76,7 +78,7 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Image,
|
imageVector = Icons.Default.Image,
|
||||||
contentDescription = "Cover Image not found",
|
contentDescription = stringResource(id = R.string.cover_image_not_found_content_desc),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.Center)
|
.align(Alignment.Center)
|
||||||
.fillMaxWidth(0.7f)
|
.fillMaxWidth(0.7f)
|
||||||
|
|
@ -87,7 +89,7 @@ fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
|
||||||
if (book.coverImage != null) {
|
if (book.coverImage != null) {
|
||||||
Image(
|
Image(
|
||||||
bitmap = book.coverImage,
|
bitmap = book.coverImage,
|
||||||
contentDescription = "Cover",
|
contentDescription = stringResource(id = R.string.cover_image_content_desc),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clip(MaterialTheme.shapes.large),
|
.clip(MaterialTheme.shapes.large),
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,22 @@
|
||||||
package ua.acclorite.book_story.presentation.screens.book_info.components
|
package ua.acclorite.book_story.presentation.screens.book_info.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.DriveFileMove
|
import androidx.compose.material.icons.automirrored.outlined.DriveFileMove
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.outlined.Delete
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
import androidx.compose.material.icons.outlined.Info
|
import androidx.compose.material.icons.outlined.Info
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.presentation.components.CustomDropDownMenuItem
|
import ua.acclorite.book_story.presentation.components.CustomDropDownMenuItem
|
||||||
|
import ua.acclorite.book_story.presentation.components.CustomIconButton
|
||||||
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
|
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
|
import ua.acclorite.book_story.presentation.screens.book_info.data.BookInfoViewModel
|
||||||
|
|
||||||
|
|
@ -32,15 +28,13 @@ fun BookInfoMoreDropDown(viewModel: BookInfoViewModel, snackbarState: SnackbarHo
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
|
|
||||||
Box {
|
Box {
|
||||||
IconButton(onClick = {
|
CustomIconButton(
|
||||||
|
icon = Icons.Default.MoreVert,
|
||||||
|
contentDescription = stringResource(id = R.string.show_dropdown_content_desc),
|
||||||
|
disableOnClick = false,
|
||||||
|
enabled = !state.showMoreDropDown
|
||||||
|
) {
|
||||||
viewModel.onEvent(BookInfoEvent.OnShowHideMoreDropDown)
|
viewModel.onEvent(BookInfoEvent.OnShowHideMoreDropDown)
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.MoreVert,
|
|
||||||
contentDescription = "Show drop down",
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
@ -24,18 +25,29 @@ import ua.acclorite.book_story.presentation.data.removeTrailingZero
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun BookInfoStatisticSection(book: Book) {
|
fun BookInfoStatisticSection(book: Book) {
|
||||||
|
val progress = remember(book) {
|
||||||
|
"${
|
||||||
|
(book.progress * 100)
|
||||||
|
.toDouble()
|
||||||
|
.removeDigits(1)
|
||||||
|
.removeTrailingZero()
|
||||||
|
}%"
|
||||||
|
}
|
||||||
|
val description = if (book.progress < 1f) stringResource(
|
||||||
|
id = R.string.you_read_query,
|
||||||
|
progress
|
||||||
|
) + " " + stringResource(
|
||||||
|
if (book.progress > 0.2f) R.string.read_keep
|
||||||
|
else R.string.read_more
|
||||||
|
) else stringResource(id = R.string.read_done)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 24.dp)
|
.padding(horizontal = 24.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "${
|
text = progress,
|
||||||
(book.progress * 100)
|
|
||||||
.toDouble()
|
|
||||||
.removeDigits(1)
|
|
||||||
.removeTrailingZero()
|
|
||||||
}%",
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
style = MaterialTheme.typography.bodyLarge
|
style = MaterialTheme.typography.bodyLarge
|
||||||
)
|
)
|
||||||
|
|
@ -53,16 +65,7 @@ fun BookInfoStatisticSection(book: Book) {
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
Text(
|
Text(
|
||||||
text = if (book.progress < 1f) stringResource(
|
text = description,
|
||||||
id = R.string.you_read_query,
|
|
||||||
(book.progress * 100)
|
|
||||||
.toDouble()
|
|
||||||
.removeDigits(1)
|
|
||||||
.removeTrailingZero() + "%"
|
|
||||||
) + " " + stringResource(
|
|
||||||
if (book.progress > 0.2f) R.string.read_keep
|
|
||||||
else R.string.read_more
|
|
||||||
) else stringResource(id = R.string.read_done),
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,10 @@ import androidx.activity.result.PickVisualMediaRequest
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.HideImage
|
import androidx.compose.material.icons.filled.HideImage
|
||||||
import androidx.compose.material.icons.filled.ImageSearch
|
import androidx.compose.material.icons.filled.ImageSearch
|
||||||
|
|
@ -44,7 +46,8 @@ fun BookInfoChangeCoverBottomSheet(
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val book = state.book
|
val navigationBarPadding =
|
||||||
|
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||||
|
|
||||||
val photoPicker = rememberLauncherForActivityResult(
|
val photoPicker = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.PickVisualMedia(),
|
contract = ActivityResultContracts.PickVisualMedia(),
|
||||||
|
|
@ -89,7 +92,7 @@ fun BookInfoChangeCoverBottomSheet(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (book.coverImage != null) {
|
if (state.book.coverImage != null) {
|
||||||
BookInfoChangeCoverBottomSheetItem(
|
BookInfoChangeCoverBottomSheetItem(
|
||||||
icon = Icons.Default.HideImage,
|
icon = Icons.Default.HideImage,
|
||||||
text = stringResource(id = R.string.delete_cover),
|
text = stringResource(id = R.string.delete_cover),
|
||||||
|
|
@ -109,6 +112,10 @@ fun BookInfoChangeCoverBottomSheet(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(48.dp))
|
Spacer(
|
||||||
|
modifier = Modifier.height(
|
||||||
|
8.dp + navigationBarPadding
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,8 +3,10 @@ package ua.acclorite.book_story.presentation.screens.book_info.components.detail
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
|
@ -12,6 +14,7 @@ import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
@ -35,20 +38,32 @@ fun BookInfoDetailsBottomSheet(
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val navigationBarPadding =
|
||||||
|
WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||||
|
|
||||||
val pattern = SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault())
|
val pattern = remember {
|
||||||
val lastOpened = pattern.format(Date(state.book.lastOpened ?: 0))
|
SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault())
|
||||||
|
}
|
||||||
|
val lastOpened = remember {
|
||||||
|
pattern.format(Date(state.book.lastOpened ?: 0))
|
||||||
|
}
|
||||||
|
|
||||||
val sizeBytes = state.book.file?.length() ?: 0
|
val sizeBytes = remember {
|
||||||
|
state.book.file?.length() ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
val fileSizeKB = if (sizeBytes > 0) sizeBytes.toDouble() / 1024.0 else 0.0
|
val fileSizeKB = remember {
|
||||||
val fileSizeMB = if (sizeBytes > 0) fileSizeKB / 1024.0 else 0.0
|
if (sizeBytes > 0) sizeBytes.toDouble() / 1024.0 else 0.0
|
||||||
|
}
|
||||||
|
val fileSizeMB = remember {
|
||||||
|
if (sizeBytes > 0) fileSizeKB / 1024.0 else 0.0
|
||||||
|
}
|
||||||
|
|
||||||
val fileSize =
|
val fileSize = remember {
|
||||||
if (fileSizeMB >= 1.0) "%.2f MB".format(fileSizeMB)
|
if (fileSizeMB >= 1.0) "%.2f MB".format(fileSizeMB)
|
||||||
else if (fileSizeMB > 0.0) "%.2f KB".format(fileSizeKB)
|
else if (fileSizeMB > 0.0) "%.2f KB".format(fileSizeKB)
|
||||||
else ""
|
else ""
|
||||||
|
}
|
||||||
|
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
|
@ -129,7 +144,10 @@ fun BookInfoDetailsBottomSheet(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(
|
||||||
Spacer(modifier = Modifier.height(48.dp))
|
modifier = Modifier.height(
|
||||||
|
8.dp + navigationBarPadding
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -34,9 +34,7 @@ fun BookInfoDetailsBottomSheetItem(
|
||||||
modifier
|
modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = {
|
onClick = {},
|
||||||
|
|
||||||
},
|
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
onClick()
|
onClick()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import ua.acclorite.book_story.domain.model.NullableBook
|
||||||
import ua.acclorite.book_story.domain.use_case.DeleteBooks
|
import ua.acclorite.book_story.domain.use_case.DeleteBooks
|
||||||
import ua.acclorite.book_story.domain.use_case.GetBookFromFile
|
import ua.acclorite.book_story.domain.use_case.GetBookFromFile
|
||||||
import ua.acclorite.book_story.domain.use_case.UpdateBooks
|
import ua.acclorite.book_story.domain.use_case.UpdateBooks
|
||||||
|
import ua.acclorite.book_story.domain.use_case.UpdateBooksWithText
|
||||||
import ua.acclorite.book_story.presentation.data.Navigator
|
import ua.acclorite.book_story.presentation.data.Navigator
|
||||||
import ua.acclorite.book_story.presentation.data.Screen
|
import ua.acclorite.book_story.presentation.data.Screen
|
||||||
|
|
||||||
|
|
@ -35,6 +36,7 @@ import ua.acclorite.book_story.presentation.data.Screen
|
||||||
class BookInfoViewModel @AssistedInject constructor(
|
class BookInfoViewModel @AssistedInject constructor(
|
||||||
@Assisted book: Book,
|
@Assisted book: Book,
|
||||||
private val updateBooks: UpdateBooks,
|
private val updateBooks: UpdateBooks,
|
||||||
|
private val updateBooksWithText: UpdateBooksWithText,
|
||||||
private val deleteBooks: DeleteBooks,
|
private val deleteBooks: DeleteBooks,
|
||||||
private val getBookFromFile: GetBookFromFile
|
private val getBookFromFile: GetBookFromFile
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
@ -42,7 +44,7 @@ class BookInfoViewModel @AssistedInject constructor(
|
||||||
private val _state = MutableStateFlow(BookInfoState(book))
|
private val _state = MutableStateFlow(BookInfoState(book))
|
||||||
val state = _state.asStateFlow()
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
var job: Job? = null
|
private var job: Job? = null
|
||||||
|
|
||||||
fun onEvent(event: BookInfoEvent) {
|
fun onEvent(event: BookInfoEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
|
|
@ -286,7 +288,8 @@ class BookInfoViewModel @AssistedInject constructor(
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isRefreshing = true
|
isRefreshing = true,
|
||||||
|
editTitle = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -358,13 +361,20 @@ class BookInfoViewModel @AssistedInject constructor(
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
book = it.book.copy(
|
book = it.book.copy(
|
||||||
|
author = updatedBook.author,
|
||||||
|
description = updatedBook.description
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
updateBooksWithText.execute(
|
||||||
|
listOf(
|
||||||
|
_state.value.book.copy(
|
||||||
author = updatedBook.author,
|
author = updatedBook.author,
|
||||||
description = updatedBook.description,
|
description = updatedBook.description,
|
||||||
text = updatedBook.text
|
text = updatedBook.text
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
)
|
||||||
updateBooks.execute(listOf(_state.value.book))
|
|
||||||
event.refreshList()
|
event.refreshList()
|
||||||
|
|
||||||
onEvent(
|
onEvent(
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,10 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
import androidx.compose.foundation.lazy.items
|
||||||
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.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
|
@ -29,8 +25,6 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||||
import androidx.compose.material.pullrefresh.pullRefresh
|
import androidx.compose.material.pullrefresh.pullRefresh
|
||||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
|
@ -38,14 +32,14 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
@ -55,11 +49,9 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
import kotlinx.coroutines.FlowPreview
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.flow.debounce
|
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
|
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
|
||||||
|
import ua.acclorite.book_story.presentation.components.CustomIconButton
|
||||||
import ua.acclorite.book_story.presentation.components.MoreDropDown
|
import ua.acclorite.book_story.presentation.components.MoreDropDown
|
||||||
import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty
|
import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty
|
||||||
import ua.acclorite.book_story.presentation.components.is_messages.IsError
|
import ua.acclorite.book_story.presentation.components.is_messages.IsError
|
||||||
|
|
@ -79,7 +71,7 @@ import ua.acclorite.book_story.ui.elevation
|
||||||
ExperimentalMaterial3Api::class,
|
ExperimentalMaterial3Api::class,
|
||||||
ExperimentalPermissionsApi::class,
|
ExperimentalPermissionsApi::class,
|
||||||
ExperimentalFoundationApi::class,
|
ExperimentalFoundationApi::class,
|
||||||
ExperimentalMaterialApi::class, FlowPreview::class
|
ExperimentalMaterialApi::class
|
||||||
)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseScreen(
|
fun BrowseScreen(
|
||||||
|
|
@ -98,34 +90,21 @@ fun BrowseScreen(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val listState = rememberLazyGridState(state.scrollIndex, state.scrollOffset)
|
var showErrorMessage by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(listState) {
|
|
||||||
snapshotFlow {
|
|
||||||
listState.firstVisibleItemIndex
|
|
||||||
}
|
|
||||||
.debounce(10L)
|
|
||||||
.collectLatest {
|
|
||||||
viewModel.onEvent(BrowseEvent.OnUpdateScrollIndex(it))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(listState) {
|
|
||||||
snapshotFlow {
|
|
||||||
listState.firstVisibleItemScrollOffset
|
|
||||||
}
|
|
||||||
.debounce(10L)
|
|
||||||
.collectLatest {
|
|
||||||
viewModel.onEvent(
|
|
||||||
BrowseEvent.OnUpdateScrollOffset(it)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.onEvent(BrowseEvent.OnPermissionCheck(permissionState))
|
viewModel.onEvent(
|
||||||
|
BrowseEvent.OnPermissionCheck(
|
||||||
|
permissionState,
|
||||||
|
hideErrorMessage = { showErrorMessage = false }
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.requestPermissionDialog) {
|
if (state.requestPermissionDialog) {
|
||||||
BrowseStoragePermissionDialog(viewModel, permissionState)
|
BrowseStoragePermissionDialog(viewModel, permissionState) {
|
||||||
|
showErrorMessage = it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (state.showAddingDialog) {
|
if (state.showAddingDialog) {
|
||||||
BrowseAddingDialog(
|
BrowseAddingDialog(
|
||||||
|
|
@ -146,7 +125,7 @@ fun BrowseScreen(
|
||||||
scrolledContainerColor = MaterialTheme.elevation(),
|
scrolledContainerColor = MaterialTheme.elevation(),
|
||||||
|
|
||||||
scrollBehavior = null,
|
scrollBehavior = null,
|
||||||
isTopBarScrolled = state.scrollIndex > 0 || state.scrollOffset > 0 || state.hasSelectedItems,
|
isTopBarScrolled = state.hasSelectedItems || state.listState.canScrollBackward,
|
||||||
|
|
||||||
content1Visibility = !state.hasSelectedItems && !state.showSearch,
|
content1Visibility = !state.hasSelectedItems && !state.showSearch,
|
||||||
content1NavigationIcon = {},
|
content1NavigationIcon = {},
|
||||||
|
|
@ -160,26 +139,26 @@ fun BrowseScreen(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
content1Actions = {
|
content1Actions = {
|
||||||
IconButton(onClick = { viewModel.onEvent(BrowseEvent.OnSearchShowHide) }) {
|
CustomIconButton(
|
||||||
Icon(
|
icon = Icons.Default.Search,
|
||||||
imageVector = Icons.Default.Search,
|
contentDescription = stringResource(id = R.string.search_content_desc),
|
||||||
contentDescription = "Search files",
|
disableOnClick = false,
|
||||||
modifier = Modifier.size(24.dp),
|
enabled = !state.showSearch
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
) {
|
||||||
)
|
viewModel.onEvent(BrowseEvent.OnSearchShowHide)
|
||||||
}
|
}
|
||||||
MoreDropDown(navigator = navigator)
|
MoreDropDown(navigator = navigator)
|
||||||
},
|
},
|
||||||
|
|
||||||
content2Visibility = state.hasSelectedItems,
|
content2Visibility = state.hasSelectedItems,
|
||||||
content2NavigationIcon = {
|
content2NavigationIcon = {
|
||||||
IconButton(onClick = { viewModel.onEvent(BrowseEvent.OnClearSelectedFiles) }) {
|
CustomIconButton(
|
||||||
Icon(
|
icon = Icons.Default.Clear,
|
||||||
imageVector = Icons.Default.Clear,
|
contentDescription =
|
||||||
contentDescription = "Clear selected items",
|
stringResource(id = R.string.clear_selected_items_content_desc),
|
||||||
modifier = Modifier.size(24.dp),
|
disableOnClick = true
|
||||||
tint = MaterialTheme.colorScheme.onSurface
|
) {
|
||||||
)
|
viewModel.onEvent(BrowseEvent.OnClearSelectedFiles)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
content2Title = {
|
content2Title = {
|
||||||
|
|
@ -195,25 +174,27 @@ fun BrowseScreen(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
content2Actions = {
|
content2Actions = {
|
||||||
IconButton(onClick = { viewModel.onEvent(BrowseEvent.OnAddingDialogRequest) }) {
|
CustomIconButton(
|
||||||
Icon(
|
icon = Icons.Default.Check,
|
||||||
imageVector = Icons.Default.Check,
|
contentDescription =
|
||||||
contentDescription = "Add files to library",
|
stringResource(id = R.string.add_files_content_desc),
|
||||||
modifier = Modifier.size(24.dp),
|
disableOnClick = false,
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
enabled = !state.showAddingDialog
|
||||||
)
|
) {
|
||||||
|
viewModel.onEvent(BrowseEvent.OnAddingDialogRequest)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
content3Visibility = state.showSearch && !state.hasSelectedItems,
|
content3Visibility = state.showSearch && !state.hasSelectedItems,
|
||||||
content3NavigationIcon = {
|
content3NavigationIcon = {
|
||||||
IconButton(onClick = { viewModel.onEvent(BrowseEvent.OnSearchShowHide) }) {
|
CustomIconButton(
|
||||||
Icon(
|
icon = Icons.AutoMirrored.Default.ArrowBack,
|
||||||
imageVector = Icons.AutoMirrored.Default.ArrowBack,
|
contentDescription = stringResource(
|
||||||
contentDescription = "Exit search mode",
|
id = R.string.exit_search_content_desc
|
||||||
modifier = Modifier.size(24.dp),
|
),
|
||||||
tint = MaterialTheme.colorScheme.onSurface
|
disableOnClick = true
|
||||||
)
|
) {
|
||||||
|
viewModel.onEvent(BrowseEvent.OnSearchShowHide)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
content3Title = {
|
content3Title = {
|
||||||
|
|
@ -268,12 +249,12 @@ fun BrowseScreen(
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(top = padding.calculateTopPadding())
|
.padding(top = padding.calculateTopPadding())
|
||||||
) {
|
) {
|
||||||
LazyVerticalGrid(
|
DefaultTransition(visible = !state.isLoading) {
|
||||||
state = listState,
|
LazyColumn(
|
||||||
|
state = state.listState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
columns = GridCells.Adaptive(170.dp),
|
contentPadding = PaddingValues(vertical = 8.dp)
|
||||||
contentPadding = PaddingValues(12.dp)
|
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
state.selectableFiles,
|
state.selectableFiles,
|
||||||
|
|
@ -283,37 +264,39 @@ fun BrowseScreen(
|
||||||
file = selectableFile,
|
file = selectableFile,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.animateItemPlacement(),
|
.animateItemPlacement(),
|
||||||
|
hasSelectedFiles = state.selectableFiles.any { it.second },
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.onEvent(BrowseEvent.OnSelectFile(selectableFile))
|
viewModel.onEvent(BrowseEvent.OnSelectFile(selectableFile))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.isLoading && !state.isRefreshing && state.selectableFiles.isEmpty()) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
strokeCap = StrokeCap.Round,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.Center)
|
|
||||||
.size(36.dp)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DefaultTransition(visible = state.showErrorMessage, Modifier.align(Alignment.Center)) {
|
AnimatedVisibility(
|
||||||
|
visible = showErrorMessage,
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
enter = Transitions.DefaultTransitionIn,
|
||||||
|
exit = fadeOut(tween(0))
|
||||||
|
) {
|
||||||
IsError(
|
IsError(
|
||||||
modifier = Modifier.align(Alignment.Center),
|
modifier = Modifier.align(Alignment.Center),
|
||||||
errorMessage = stringResource(id = R.string.error_permission),
|
errorMessage = stringResource(id = R.string.error_permission),
|
||||||
icon = painterResource(id = R.drawable.error),
|
icon = painterResource(id = R.drawable.error),
|
||||||
actionTitle = stringResource(id = R.string.grant_permission)
|
actionTitle = stringResource(id = R.string.grant_permission)
|
||||||
) {
|
) {
|
||||||
viewModel.onEvent(BrowseEvent.OnPermissionCheck(permissionState))
|
viewModel.onEvent(
|
||||||
|
BrowseEvent.OnPermissionCheck(
|
||||||
|
permissionState,
|
||||||
|
hideErrorMessage = { showErrorMessage = false }
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = !state.isLoading && state.selectableFiles.isEmpty()
|
visible = !state.isLoading && state.selectableFiles.isEmpty()
|
||||||
&& !state.showErrorMessage && !state.requestPermissionDialog
|
&& !showErrorMessage && !state.requestPermissionDialog
|
||||||
&& !state.isRefreshing,
|
&& !state.isRefreshing,
|
||||||
modifier = Modifier.align(Alignment.Center),
|
modifier = Modifier.align(Alignment.Center),
|
||||||
enter = Transitions.DefaultTransitionIn,
|
enter = Transitions.DefaultTransitionIn,
|
||||||
|
|
|
||||||
|
|
@ -15,22 +15,25 @@ import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.CheckCircle
|
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
|
import ua.acclorite.book_story.presentation.components.CustomCheckbox
|
||||||
import ua.acclorite.book_story.ui.DefaultTransition
|
import ua.acclorite.book_story.ui.DefaultTransition
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
|
|
@ -41,19 +44,24 @@ import java.util.Locale
|
||||||
* Browse list element item. Can be selected.
|
* Browse list element item. Can be selected.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseFileItem(file: Pair<File, Boolean>, modifier: Modifier, onClick: () -> Unit) {
|
fun BrowseFileItem(
|
||||||
val fileExtension: String = file.first.name.substringAfterLast(".", "")
|
file: Pair<File, Boolean>,
|
||||||
|
hasSelectedFiles: Boolean,
|
||||||
|
modifier: Modifier, onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val pattern = remember { SimpleDateFormat("HH:mm dd MMM yyyy", Locale.getDefault()) }
|
||||||
|
val lastModified = remember { pattern.format(Date(file.first.lastModified())) }
|
||||||
|
val sizeBytes = remember { file.first.length() }
|
||||||
|
|
||||||
val pattern = SimpleDateFormat("HH:mm dd MMM", Locale.getDefault())
|
val fileSizeKB = remember { if (sizeBytes > 0) sizeBytes.toDouble() / 1024.0 else 0.0 }
|
||||||
|
val fileSizeMB = remember { if (sizeBytes > 0) fileSizeKB / 1024.0 else 0.0 }
|
||||||
val lastModified = pattern.format(Date(file.first.lastModified()))
|
val fileSize = remember {
|
||||||
val icon = when (fileExtension) {
|
if (fileSizeMB >= 1.0) "%.2f MB".format(fileSizeMB)
|
||||||
"txt" -> painterResource(id = R.drawable.txt)
|
else if (fileSizeMB > 0.0) "%.2f KB".format(fileSizeKB)
|
||||||
"epub" -> painterResource(id = R.drawable.epub)
|
else "0 KB"
|
||||||
"pdf" -> painterResource(id = R.drawable.pdf)
|
|
||||||
else -> painterResource(id = R.drawable.file)
|
|
||||||
}
|
}
|
||||||
val outlineColor = if (file.second) MaterialTheme.colorScheme.primary
|
|
||||||
|
val outlineColor = if (file.second) MaterialTheme.colorScheme.outline
|
||||||
else MaterialTheme.colorScheme.outlineVariant
|
else MaterialTheme.colorScheme.outlineVariant
|
||||||
val backgroundColor = if (file.second) MaterialTheme.colorScheme.secondaryContainer
|
val backgroundColor = if (file.second) MaterialTheme.colorScheme.secondaryContainer
|
||||||
else Color.Transparent
|
else Color.Transparent
|
||||||
|
|
@ -61,90 +69,82 @@ fun BrowseFileItem(file: Pair<File, Boolean>, modifier: Modifier, onClick: () ->
|
||||||
val animatedOutlineColor by animateColorAsState(
|
val animatedOutlineColor by animateColorAsState(
|
||||||
targetValue = outlineColor,
|
targetValue = outlineColor,
|
||||||
tween(300),
|
tween(300),
|
||||||
label = "Outline animation"
|
label = stringResource(id = R.string.outline_anim_content_desc)
|
||||||
)
|
)
|
||||||
val animatedBackgroundColor by animateColorAsState(
|
val animatedBackgroundColor by animateColorAsState(
|
||||||
targetValue = backgroundColor,
|
targetValue = backgroundColor,
|
||||||
tween(300),
|
tween(300),
|
||||||
label = "Background animation"
|
label = stringResource(id = R.string.background_anim_content_desc)
|
||||||
)
|
)
|
||||||
|
|
||||||
Column(
|
Row(
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.padding(6.dp)
|
.fillMaxWidth()
|
||||||
.clip(MaterialTheme.shapes.medium)
|
.padding(horizontal = 8.dp, vertical = 3.dp)
|
||||||
.border(
|
.clip(RoundedCornerShape(10.dp))
|
||||||
width = 1.dp,
|
|
||||||
color = animatedOutlineColor,
|
|
||||||
shape = MaterialTheme.shapes.medium
|
|
||||||
)
|
|
||||||
.padding(1.dp)
|
|
||||||
.background(animatedBackgroundColor)
|
.background(animatedBackgroundColor)
|
||||||
.clickable {
|
.clickable {
|
||||||
onClick()
|
onClick()
|
||||||
}
|
}
|
||||||
|
.padding(horizontal = 8.dp, vertical = 7.dp)
|
||||||
) {
|
) {
|
||||||
Icon(
|
|
||||||
painter = icon,
|
|
||||||
contentDescription = "File icon",
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(vertical = 60.dp)
|
|
||||||
.size(70.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Start,
|
modifier = Modifier.weight(0.88f)
|
||||||
modifier = Modifier
|
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
|
||||||
.padding(horizontal = 10.dp, vertical = 12.dp)
|
|
||||||
) {
|
) {
|
||||||
Box {
|
Box(
|
||||||
DefaultTransition(visible = !file.second) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.file),
|
|
||||||
contentDescription = "File",
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(28.dp),
|
.border(
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
1.dp,
|
||||||
|
animatedOutlineColor,
|
||||||
|
RoundedCornerShape(6.dp)
|
||||||
)
|
)
|
||||||
}
|
.padding(14.dp),
|
||||||
DefaultTransition(visible = file.second) {
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.CheckCircle,
|
imageVector = Icons.AutoMirrored.Filled.InsertDriveFile,
|
||||||
contentDescription = "Checked",
|
contentDescription = stringResource(id = R.string.file_icon_content_desc),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(28.dp),
|
.size(22.dp),
|
||||||
tint = MaterialTheme.colorScheme.primary
|
tint = MaterialTheme.colorScheme.primary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Column(verticalArrangement = Arrangement.Center) {
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.Center
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
file.first.name,
|
file.first.name,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
maxLines = 1,
|
maxLines = 2,
|
||||||
|
lineHeight = 18.sp,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
lastModified,
|
"$fileSize, $lastModified",
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
fontSize = 11.sp,
|
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(0.12f)
|
||||||
|
.padding(end = 6.dp),
|
||||||
|
contentAlignment = Alignment.CenterEnd
|
||||||
|
) {
|
||||||
|
DefaultTransition(
|
||||||
|
visible = hasSelectedFiles
|
||||||
|
) {
|
||||||
|
CustomCheckbox(selected = file.second, size = 18.dp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -19,7 +19,11 @@ import ua.acclorite.book_story.presentation.screens.browse.data.BrowseViewModel
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseStoragePermissionDialog(viewModel: BrowseViewModel, permissionState: PermissionState) {
|
fun BrowseStoragePermissionDialog(
|
||||||
|
viewModel: BrowseViewModel,
|
||||||
|
permissionState: PermissionState,
|
||||||
|
showErrorMessage: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
val activity = LocalContext.current as ComponentActivity
|
val activity = LocalContext.current as ComponentActivity
|
||||||
|
|
||||||
CustomDialogWithContent(
|
CustomDialogWithContent(
|
||||||
|
|
@ -27,15 +31,28 @@ fun BrowseStoragePermissionDialog(viewModel: BrowseViewModel, permissionState: P
|
||||||
description = stringResource(id = R.string.storage_permission_description),
|
description = stringResource(id = R.string.storage_permission_description),
|
||||||
actionText = stringResource(id = R.string.grant),
|
actionText = stringResource(id = R.string.grant),
|
||||||
imageVectorIcon = Icons.Default.SdStorage,
|
imageVectorIcon = Icons.Default.SdStorage,
|
||||||
onDismiss = { viewModel.onEvent(BrowseEvent.OnStoragePermissionDismiss(permissionState)) },
|
onDismiss = {
|
||||||
|
viewModel.onEvent(
|
||||||
|
BrowseEvent.OnStoragePermissionDismiss(
|
||||||
|
permissionState,
|
||||||
|
showErrorMessage = { showErrorMessage(true) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
isActionEnabled = true,
|
isActionEnabled = true,
|
||||||
withDivider = false,
|
withDivider = false,
|
||||||
onAction = {
|
onAction = {
|
||||||
viewModel.onEvent(
|
viewModel.onEvent(
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
BrowseEvent.OnStoragePermissionRequest(activity)
|
BrowseEvent.OnStoragePermissionRequest(
|
||||||
|
activity,
|
||||||
|
hideErrorMessage = { showErrorMessage(false) }
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
BrowseEvent.OnLegacyStoragePermissionRequest(permissionState)
|
BrowseEvent.OnLegacyStoragePermissionRequest(
|
||||||
|
permissionState,
|
||||||
|
hideErrorMessage = { showErrorMessage(false) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ fun BrowseAddingDialogItem(result: NullableBook, onClick: (Boolean) -> Unit) {
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = "Error",
|
contentDescription = stringResource(id = R.string.error_content_desc),
|
||||||
modifier = Modifier.size(26.dp),
|
modifier = Modifier.size(26.dp),
|
||||||
tint = MaterialTheme.colorScheme.error
|
tint = MaterialTheme.colorScheme.error
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,24 @@ import ua.acclorite.book_story.presentation.data.Navigator
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
sealed class BrowseEvent {
|
sealed class BrowseEvent {
|
||||||
data class OnStoragePermissionRequest(val activity: ComponentActivity) : BrowseEvent()
|
data class OnStoragePermissionRequest(
|
||||||
data class OnLegacyStoragePermissionRequest(val permissionState: PermissionState) :
|
val activity: ComponentActivity, val hideErrorMessage: () -> Unit
|
||||||
BrowseEvent()
|
) : BrowseEvent()
|
||||||
|
|
||||||
|
data class OnLegacyStoragePermissionRequest(
|
||||||
|
val permissionState: PermissionState, val hideErrorMessage: () -> Unit
|
||||||
|
) : BrowseEvent()
|
||||||
|
|
||||||
|
data class OnStoragePermissionDismiss(
|
||||||
|
val permissionState: PermissionState, val showErrorMessage: () -> Unit
|
||||||
|
) : BrowseEvent()
|
||||||
|
|
||||||
data class OnStoragePermissionDismiss(val permissionState: PermissionState) : BrowseEvent()
|
|
||||||
data object OnRefreshList : BrowseEvent()
|
data object OnRefreshList : BrowseEvent()
|
||||||
data class OnPermissionCheck(val permissionState: PermissionState) : BrowseEvent()
|
data class OnPermissionCheck(
|
||||||
|
val permissionState: PermissionState,
|
||||||
|
val hideErrorMessage: () -> Unit
|
||||||
|
) : BrowseEvent()
|
||||||
|
|
||||||
data class OnSelectFile(val file: Pair<File, Boolean>) : BrowseEvent()
|
data class OnSelectFile(val file: Pair<File, Boolean>) : BrowseEvent()
|
||||||
data class OnSelectBook(val book: NullableBook) : BrowseEvent()
|
data class OnSelectBook(val book: NullableBook) : BrowseEvent()
|
||||||
data object OnSearchShowHide : BrowseEvent()
|
data object OnSearchShowHide : BrowseEvent()
|
||||||
|
|
@ -29,8 +40,5 @@ sealed class BrowseEvent {
|
||||||
data object OnGetBooksFromFiles : BrowseEvent()
|
data object OnGetBooksFromFiles : BrowseEvent()
|
||||||
data class OnAddBooks(val navigator: Navigator, val resetScroll: () -> Unit) : BrowseEvent()
|
data class OnAddBooks(val navigator: Navigator, val resetScroll: () -> Unit) : BrowseEvent()
|
||||||
data object OnLoadList : BrowseEvent()
|
data object OnLoadList : BrowseEvent()
|
||||||
data class OnUpdateScrollIndex(val index: Int) : BrowseEvent()
|
|
||||||
data class OnUpdateScrollOffset(val offset: Int) : BrowseEvent()
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package ua.acclorite.book_story.presentation.screens.browse.data
|
package ua.acclorite.book_story.presentation.screens.browse.data
|
||||||
|
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import ua.acclorite.book_story.domain.model.NullableBook
|
import ua.acclorite.book_story.domain.model.NullableBook
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
@ -7,10 +8,11 @@ import java.io.File
|
||||||
@Immutable
|
@Immutable
|
||||||
data class BrowseState(
|
data class BrowseState(
|
||||||
val selectableFiles: List<Pair<File, Boolean>> = emptyList(),
|
val selectableFiles: List<Pair<File, Boolean>> = emptyList(),
|
||||||
|
val listState: LazyListState = LazyListState(0, 0),
|
||||||
|
|
||||||
val isLoading: Boolean = true,
|
val isLoading: Boolean = true,
|
||||||
val isRefreshing: Boolean = false,
|
val isRefreshing: Boolean = false,
|
||||||
val requestPermissionDialog: Boolean = false,
|
val requestPermissionDialog: Boolean = false,
|
||||||
val showErrorMessage: Boolean = false,
|
|
||||||
|
|
||||||
val selectedItemsCount: Int = 0,
|
val selectedItemsCount: Int = 0,
|
||||||
val hasSelectedItems: Boolean = false,
|
val hasSelectedItems: Boolean = false,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
|
|
@ -20,7 +21,7 @@ import kotlinx.coroutines.launch
|
||||||
import ua.acclorite.book_story.domain.model.NullableBook
|
import ua.acclorite.book_story.domain.model.NullableBook
|
||||||
import ua.acclorite.book_story.domain.use_case.FastGetBooks
|
import ua.acclorite.book_story.domain.use_case.FastGetBooks
|
||||||
import ua.acclorite.book_story.domain.use_case.GetBooksFromFiles
|
import ua.acclorite.book_story.domain.use_case.GetBooksFromFiles
|
||||||
import ua.acclorite.book_story.domain.use_case.GetFilesFromDownloads
|
import ua.acclorite.book_story.domain.use_case.GetFilesFromDevice
|
||||||
import ua.acclorite.book_story.domain.use_case.InsertBooks
|
import ua.acclorite.book_story.domain.use_case.InsertBooks
|
||||||
import ua.acclorite.book_story.presentation.data.Argument
|
import ua.acclorite.book_story.presentation.data.Argument
|
||||||
import ua.acclorite.book_story.presentation.data.Screen
|
import ua.acclorite.book_story.presentation.data.Screen
|
||||||
|
|
@ -31,7 +32,7 @@ import javax.inject.Inject
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class BrowseViewModel @Inject constructor(
|
class BrowseViewModel @Inject constructor(
|
||||||
private val getBooksFromFiles: GetBooksFromFiles,
|
private val getBooksFromFiles: GetBooksFromFiles,
|
||||||
private val getFilesFromDownloads: GetFilesFromDownloads,
|
private val getFilesFromDevice: GetFilesFromDevice,
|
||||||
private val insertBooks: InsertBooks,
|
private val insertBooks: InsertBooks,
|
||||||
private val fastGetBooks: FastGetBooks
|
private val fastGetBooks: FastGetBooks
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
@ -65,10 +66,10 @@ class BrowseViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
requestPermissionDialog = false,
|
requestPermissionDialog = false
|
||||||
showErrorMessage = false
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
event.hideErrorMessage()
|
||||||
onEvent(BrowseEvent.OnRefreshList)
|
onEvent(BrowseEvent.OnRefreshList)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -93,10 +94,10 @@ class BrowseViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
requestPermissionDialog = false,
|
requestPermissionDialog = false
|
||||||
showErrorMessage = false
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
event.hideErrorMessage()
|
||||||
onEvent(BrowseEvent.OnRefreshList)
|
onEvent(BrowseEvent.OnRefreshList)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
@ -112,8 +113,7 @@ class BrowseViewModel @Inject constructor(
|
||||||
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
requestPermissionDialog = false,
|
requestPermissionDialog = false
|
||||||
showErrorMessage = !isPermissionGranted
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,8 +121,9 @@ class BrowseViewModel @Inject constructor(
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
getFilesFromDownloads()
|
getFilesFromDownloads()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
event.showErrorMessage()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is BrowseEvent.OnRefreshList -> {
|
is BrowseEvent.OnRefreshList -> {
|
||||||
|
|
@ -131,7 +132,8 @@ class BrowseViewModel @Inject constructor(
|
||||||
it.copy(
|
it.copy(
|
||||||
isRefreshing = true,
|
isRefreshing = true,
|
||||||
hasSelectedItems = false,
|
hasSelectedItems = false,
|
||||||
showSearch = false
|
showSearch = false,
|
||||||
|
listState = LazyListState(0, 0)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -146,24 +148,33 @@ class BrowseViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is BrowseEvent.OnPermissionCheck -> {
|
is BrowseEvent.OnPermissionCheck -> {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
||||||
val isPermissionGranted =
|
val isPermissionGranted =
|
||||||
if (!legacyPermission) Environment.isExternalStorageManager()
|
if (!legacyPermission) Environment.isExternalStorageManager()
|
||||||
else event.permissionState.status.isGranted
|
else event.permissionState.status.isGranted
|
||||||
|
|
||||||
if (isPermissionGranted) {
|
if (isPermissionGranted) {
|
||||||
return
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
requestPermissionDialog = true,
|
requestPermissionDialog = true
|
||||||
showErrorMessage = false
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
event.hideErrorMessage()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is BrowseEvent.OnSelectFile -> {
|
is BrowseEvent.OnSelectFile -> {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val indexOfFile = _state.value.selectableFiles.indexOf(event.file)
|
val indexOfFile = _state.value.selectableFiles.indexOf(event.file)
|
||||||
|
|
||||||
|
if (indexOfFile == -1) {
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
val editedList = _state.value.selectableFiles.toMutableList()
|
val editedList = _state.value.selectableFiles.toMutableList()
|
||||||
editedList[indexOfFile] = editedList[indexOfFile].copy(
|
editedList[indexOfFile] = editedList[indexOfFile].copy(
|
||||||
second = !editedList[indexOfFile].second
|
second = !editedList[indexOfFile].second
|
||||||
|
|
@ -177,9 +188,16 @@ class BrowseViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
is BrowseEvent.OnSelectBook -> {
|
is BrowseEvent.OnSelectBook -> {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val indexOfFile = _state.value.selectedBooks.indexOf(event.book)
|
val indexOfFile = _state.value.selectedBooks.indexOf(event.book)
|
||||||
|
|
||||||
|
if (indexOfFile == -1) {
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
val editedList = _state.value.selectedBooks.toMutableList()
|
val editedList = _state.value.selectedBooks.toMutableList()
|
||||||
editedList[indexOfFile] = NullableBook.NotNull(
|
editedList[indexOfFile] = NullableBook.NotNull(
|
||||||
editedList[indexOfFile].book!!.copy(
|
editedList[indexOfFile].book!!.copy(
|
||||||
|
|
@ -188,7 +206,7 @@ class BrowseViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!editedList.any { it.book?.second == true }) {
|
if (!editedList.any { it.book?.second == true }) {
|
||||||
return
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
_state.update {
|
_state.update {
|
||||||
|
|
@ -197,6 +215,7 @@ class BrowseViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
is BrowseEvent.OnSearchShowHide -> {
|
is BrowseEvent.OnSearchShowHide -> {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
|
@ -331,35 +350,20 @@ class BrowseViewModel @Inject constructor(
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = true
|
isLoading = true,
|
||||||
|
listState = LazyListState(0, 0)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
getFilesFromDownloads()
|
getFilesFromDownloads()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is BrowseEvent.OnUpdateScrollIndex -> {
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
scrollIndex = event.index
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is BrowseEvent.OnUpdateScrollOffset -> {
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
scrollOffset = event.offset
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getFilesFromDownloads(
|
private suspend fun getFilesFromDownloads(
|
||||||
query: String = if (_state.value.showSearch) _state.value.searchQuery else ""
|
query: String = if (_state.value.showSearch) _state.value.searchQuery else ""
|
||||||
) {
|
) {
|
||||||
getFilesFromDownloads.execute(query).collect { result ->
|
getFilesFromDevice.execute(query).collect { result ->
|
||||||
when (result) {
|
when (result) {
|
||||||
is Resource.Success -> {
|
is Resource.Success -> {
|
||||||
_state.update {
|
_state.update {
|
||||||
|
|
@ -371,13 +375,7 @@ class BrowseViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is Resource.Loading -> {
|
is Resource.Loading -> Unit
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
isLoading = result.isLoading
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is Resource.Error -> Unit
|
is Resource.Error -> Unit
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,10 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
|
@ -28,24 +25,19 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||||
import androidx.compose.material.pullrefresh.pullRefresh
|
import androidx.compose.material.pullrefresh.pullRefresh
|
||||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.snapshotFlow
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
|
@ -54,12 +46,10 @@ import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import kotlinx.coroutines.FlowPreview
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import kotlinx.coroutines.flow.debounce
|
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
|
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
|
||||||
import ua.acclorite.book_story.presentation.components.CategoryTitle
|
import ua.acclorite.book_story.presentation.components.CategoryTitle
|
||||||
|
import ua.acclorite.book_story.presentation.components.CustomIconButton
|
||||||
import ua.acclorite.book_story.presentation.components.CustomSnackbar
|
import ua.acclorite.book_story.presentation.components.CustomSnackbar
|
||||||
import ua.acclorite.book_story.presentation.components.MoreDropDown
|
import ua.acclorite.book_story.presentation.components.MoreDropDown
|
||||||
import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty
|
import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty
|
||||||
|
|
@ -72,13 +62,14 @@ import ua.acclorite.book_story.presentation.screens.history.data.HistoryEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
|
import ua.acclorite.book_story.presentation.screens.history.data.HistoryViewModel
|
||||||
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
|
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
|
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
|
||||||
|
import ua.acclorite.book_story.ui.DefaultTransition
|
||||||
import ua.acclorite.book_story.ui.Transitions
|
import ua.acclorite.book_story.ui.Transitions
|
||||||
import ua.acclorite.book_story.ui.elevation
|
import ua.acclorite.book_story.ui.elevation
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalMaterial3Api::class,
|
ExperimentalMaterial3Api::class,
|
||||||
ExperimentalMaterialApi::class, FlowPreview::class, ExperimentalFoundationApi::class
|
ExperimentalMaterialApi::class, ExperimentalFoundationApi::class
|
||||||
)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun HistoryScreen(
|
fun HistoryScreen(
|
||||||
|
|
@ -89,7 +80,6 @@ fun HistoryScreen(
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
val books = libraryViewModel.state.collectAsState().value.books.map { it.first }
|
val books = libraryViewModel.state.collectAsState().value.books.map { it.first }
|
||||||
|
|
||||||
val refreshState = rememberPullRefreshState(
|
val refreshState = rememberPullRefreshState(
|
||||||
refreshing = state.isRefreshing,
|
refreshing = state.isRefreshing,
|
||||||
onRefresh = {
|
onRefresh = {
|
||||||
|
|
@ -98,28 +88,6 @@ fun HistoryScreen(
|
||||||
)
|
)
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val snackbarState = remember { SnackbarHostState() }
|
val snackbarState = remember { SnackbarHostState() }
|
||||||
val listState = rememberLazyListState(state.scrollIndex, state.scrollOffset)
|
|
||||||
|
|
||||||
LaunchedEffect(listState) {
|
|
||||||
snapshotFlow {
|
|
||||||
listState.firstVisibleItemIndex
|
|
||||||
}
|
|
||||||
.debounce(10L)
|
|
||||||
.collectLatest {
|
|
||||||
viewModel.onEvent(HistoryEvent.OnUpdateScrollIndex(it))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(listState) {
|
|
||||||
snapshotFlow {
|
|
||||||
listState.firstVisibleItemScrollOffset
|
|
||||||
}
|
|
||||||
.debounce(10L)
|
|
||||||
.collectLatest {
|
|
||||||
viewModel.onEvent(
|
|
||||||
HistoryEvent.OnUpdateScrollOffset(it)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.showDeleteWholeHistoryDialog) {
|
if (state.showDeleteWholeHistoryDialog) {
|
||||||
HistoryDeleteWholeHistoryDialog(viewModel = viewModel, libraryViewModel = libraryViewModel)
|
HistoryDeleteWholeHistoryDialog(viewModel = viewModel, libraryViewModel = libraryViewModel)
|
||||||
|
|
@ -134,7 +102,7 @@ fun HistoryScreen(
|
||||||
AnimatedTopAppBar(
|
AnimatedTopAppBar(
|
||||||
scrolledContainerColor = MaterialTheme.elevation(),
|
scrolledContainerColor = MaterialTheme.elevation(),
|
||||||
scrollBehavior = null,
|
scrollBehavior = null,
|
||||||
isTopBarScrolled = (state.scrollIndex > 0 || state.scrollOffset > 0) && !state.isLoading,
|
isTopBarScrolled = state.listState.canScrollBackward,
|
||||||
|
|
||||||
content1Visibility = !state.showSearch,
|
content1Visibility = !state.showSearch,
|
||||||
content1NavigationIcon = {},
|
content1NavigationIcon = {},
|
||||||
|
|
@ -148,43 +116,39 @@ fun HistoryScreen(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
content1Actions = {
|
content1Actions = {
|
||||||
IconButton(
|
CustomIconButton(
|
||||||
enabled = !state.isRefreshing,
|
icon = Icons.Default.Search,
|
||||||
onClick = { viewModel.onEvent(HistoryEvent.OnSearchShowHide) }
|
contentDescription = stringResource(id = R.string.search_content_desc),
|
||||||
|
disableOnClick = false,
|
||||||
|
enabled = !state.showSearch
|
||||||
) {
|
) {
|
||||||
Icon(
|
viewModel.onEvent(HistoryEvent.OnSearchShowHide)
|
||||||
imageVector = Icons.Default.Search,
|
|
||||||
contentDescription = "Search history",
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
IconButton(
|
CustomIconButton(
|
||||||
|
icon = Icons.Outlined.DeleteSweep,
|
||||||
|
contentDescription = stringResource(
|
||||||
|
id = R.string.delete_whole_history_content_desc
|
||||||
|
),
|
||||||
|
disableOnClick = false,
|
||||||
enabled = !state.isLoading
|
enabled = !state.isLoading
|
||||||
&& !state.isRefreshing
|
&& !state.isRefreshing
|
||||||
&& state.history.isNotEmpty(),
|
&& state.history.isNotEmpty()
|
||||||
onClick = {
|
|
||||||
viewModel.onEvent(HistoryEvent.OnShowHideDeleteWholeHistoryDialog)
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
viewModel.onEvent(HistoryEvent.OnShowHideDeleteWholeHistoryDialog)
|
||||||
imageVector = Icons.Outlined.DeleteSweep,
|
|
||||||
contentDescription = "Delete whole history",
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
MoreDropDown(navigator = navigator)
|
MoreDropDown(navigator = navigator)
|
||||||
},
|
},
|
||||||
|
|
||||||
content2Visibility = state.showSearch,
|
content2Visibility = state.showSearch,
|
||||||
content2NavigationIcon = {
|
content2NavigationIcon = {
|
||||||
IconButton(onClick = { viewModel.onEvent(HistoryEvent.OnSearchShowHide) }) {
|
CustomIconButton(
|
||||||
Icon(
|
icon = Icons.AutoMirrored.Default.ArrowBack,
|
||||||
imageVector = Icons.AutoMirrored.Default.ArrowBack,
|
contentDescription = stringResource(
|
||||||
contentDescription = "Exit search mode",
|
id = R.string.exit_search_content_desc
|
||||||
modifier = Modifier.size(24.dp),
|
),
|
||||||
tint = MaterialTheme.colorScheme.onSurface
|
disableOnClick = true
|
||||||
)
|
) {
|
||||||
|
viewModel.onEvent(HistoryEvent.OnSearchShowHide)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
content2Title = {
|
content2Title = {
|
||||||
|
|
@ -243,10 +207,11 @@ fun HistoryScreen(
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(top = paddingValues.calculateTopPadding())
|
.padding(top = paddingValues.calculateTopPadding())
|
||||||
) {
|
) {
|
||||||
|
DefaultTransition(visible = !state.isLoading) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
state = listState,
|
state = state.listState,
|
||||||
contentPadding = PaddingValues(vertical = 12.dp)
|
contentPadding = PaddingValues(vertical = 12.dp)
|
||||||
) {
|
) {
|
||||||
if (!state.isLoading) {
|
if (!state.isLoading) {
|
||||||
|
|
@ -271,7 +236,9 @@ fun HistoryScreen(
|
||||||
items(
|
items(
|
||||||
groupedHistory.history, key = { it.id ?: UUID.randomUUID() }
|
groupedHistory.history, key = { it.id ?: UUID.randomUUID() }
|
||||||
) {
|
) {
|
||||||
val book = books.find { book -> book.id == it.bookId } ?: return@items
|
val book = remember {
|
||||||
|
books.find { book -> book.id == it.bookId }
|
||||||
|
} ?: return@items
|
||||||
|
|
||||||
HistoryItem(
|
HistoryItem(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
|
|
@ -309,6 +276,7 @@ fun HistoryScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = !state.isLoading && state.history.isEmpty() && !state.isRefreshing,
|
visible = !state.isLoading && state.history.isEmpty() && !state.isRefreshing,
|
||||||
|
|
@ -322,15 +290,6 @@ fun HistoryScreen(
|
||||||
icon = painterResource(id = R.drawable.empty_history)
|
icon = painterResource(id = R.drawable.empty_history)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (state.isLoading && !state.isRefreshing) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
strokeCap = StrokeCap.Round,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.Center)
|
|
||||||
.size(36.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
PullRefreshIndicator(
|
PullRefreshIndicator(
|
||||||
state.isRefreshing,
|
state.isRefreshing,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
|
@ -51,8 +52,10 @@ fun HistoryItem(
|
||||||
isDeleteEnabled: Boolean,
|
isDeleteEnabled: Boolean,
|
||||||
onDeleteClick: () -> Unit
|
onDeleteClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val date = Date(history.time)
|
val date = remember(history) { Date(history.time) }
|
||||||
val pattern = SimpleDateFormat("HH:mm", Locale.getDefault())
|
val pattern = remember {
|
||||||
|
SimpleDateFormat("HH:mm", Locale.getDefault())
|
||||||
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|
@ -65,7 +68,7 @@ fun HistoryItem(
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.weight(0.9f)
|
modifier = Modifier.weight(0.89f)
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -76,7 +79,9 @@ fun HistoryItem(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Image,
|
imageVector = Icons.Default.Image,
|
||||||
contentDescription = "Cover Image not found",
|
contentDescription = stringResource(
|
||||||
|
id = R.string.cover_image_not_found_content_desc
|
||||||
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.Center)
|
.align(Alignment.Center)
|
||||||
.fillMaxWidth(0.7f)
|
.fillMaxWidth(0.7f)
|
||||||
|
|
@ -128,7 +133,7 @@ fun HistoryItem(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Box(modifier = Modifier.weight(0.1f), contentAlignment = Alignment.CenterEnd) {
|
Box(modifier = Modifier.weight(0.11f), contentAlignment = Alignment.CenterEnd) {
|
||||||
IconButton(
|
IconButton(
|
||||||
enabled = isDeleteEnabled,
|
enabled = isDeleteEnabled,
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,4 @@ sealed class HistoryEvent {
|
||||||
data class OnSearchQueryChange(val query: String) : HistoryEvent()
|
data class OnSearchQueryChange(val query: String) : HistoryEvent()
|
||||||
data object OnSearchShowHide : HistoryEvent()
|
data object OnSearchShowHide : HistoryEvent()
|
||||||
data class OnRequestFocus(val focusRequester: FocusRequester) : HistoryEvent()
|
data class OnRequestFocus(val focusRequester: FocusRequester) : HistoryEvent()
|
||||||
data class OnUpdateScrollIndex(val index: Int) : HistoryEvent()
|
|
||||||
data class OnUpdateScrollOffset(val offset: Int) : HistoryEvent()
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
package ua.acclorite.book_story.presentation.screens.history.data
|
package ua.acclorite.book_story.presentation.screens.history.data
|
||||||
|
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import ua.acclorite.book_story.domain.model.GroupedHistory
|
import ua.acclorite.book_story.domain.model.GroupedHistory
|
||||||
|
|
||||||
data class HistoryState(
|
data class HistoryState(
|
||||||
val history: List<GroupedHistory> = emptyList(),
|
val history: List<GroupedHistory> = emptyList(),
|
||||||
|
val listState: LazyListState = LazyListState(0, 0),
|
||||||
|
|
||||||
val isRefreshing: Boolean = false,
|
val isRefreshing: Boolean = false,
|
||||||
val isLoading: Boolean = true,
|
val isLoading: Boolean = true,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package ua.acclorite.book_story.presentation.screens.history.data
|
package ua.acclorite.book_story.presentation.screens.history.data
|
||||||
|
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.material3.SnackbarResult
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
|
@ -58,7 +59,8 @@ class HistoryViewModel @Inject constructor(
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isRefreshing = true,
|
isRefreshing = true,
|
||||||
showSearch = false
|
showSearch = false,
|
||||||
|
listState = LazyListState(0, 0),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,12 +78,11 @@ class HistoryViewModel @Inject constructor(
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = true
|
isLoading = true,
|
||||||
|
listState = LazyListState(0, 0),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
getHistoryFromDatabase()
|
getHistoryFromDatabase()
|
||||||
onEvent(HistoryEvent.OnUpdateScrollIndex(0))
|
|
||||||
onEvent(HistoryEvent.OnUpdateScrollOffset(0))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -89,7 +90,8 @@ class HistoryViewModel @Inject constructor(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
showDeleteWholeHistoryDialog = false
|
showDeleteWholeHistoryDialog = false,
|
||||||
|
isLoading = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,7 +111,7 @@ class HistoryViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is HistoryEvent.OnDeleteHistoryElement -> {
|
is HistoryEvent.OnDeleteHistoryElement -> {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
deleteHistory.execute(
|
deleteHistory.execute(
|
||||||
listOf(event.historyToDelete)
|
listOf(event.historyToDelete)
|
||||||
)
|
)
|
||||||
|
|
@ -186,22 +188,6 @@ class HistoryViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is HistoryEvent.OnUpdateScrollIndex -> {
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
scrollIndex = event.index
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is HistoryEvent.OnUpdateScrollOffset -> {
|
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
scrollOffset = event.offset
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -224,16 +210,12 @@ class HistoryViewModel @Inject constructor(
|
||||||
getHistory.execute().collect { result ->
|
getHistory.execute().collect { result ->
|
||||||
when (result) {
|
when (result) {
|
||||||
is Resource.Success -> {
|
is Resource.Success -> {
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
history = emptyList()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val history = result.data?.sortedByDescending { it.time } ?: emptyList()
|
val history = result.data?.sortedByDescending { it.time } ?: emptyList()
|
||||||
|
|
||||||
if (history.isEmpty()) {
|
if (history.isEmpty()) {
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
history = emptyList(),
|
||||||
isLoading = false
|
isLoading = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.grid.GridCells
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
|
@ -26,7 +25,6 @@ import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
|
@ -38,8 +36,6 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||||
import androidx.compose.material.pullrefresh.pullRefresh
|
import androidx.compose.material.pullrefresh.pullRefresh
|
||||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
|
@ -58,7 +54,6 @@ import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
|
@ -74,6 +69,7 @@ import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.domain.model.Category
|
import ua.acclorite.book_story.domain.model.Category
|
||||||
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
|
import ua.acclorite.book_story.presentation.components.AnimatedTopAppBar
|
||||||
|
import ua.acclorite.book_story.presentation.components.CustomIconButton
|
||||||
import ua.acclorite.book_story.presentation.components.MoreDropDown
|
import ua.acclorite.book_story.presentation.components.MoreDropDown
|
||||||
import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty
|
import ua.acclorite.book_story.presentation.components.is_messages.IsEmpty
|
||||||
import ua.acclorite.book_story.presentation.data.Argument
|
import ua.acclorite.book_story.presentation.data.Argument
|
||||||
|
|
@ -87,6 +83,7 @@ import ua.acclorite.book_story.presentation.screens.library.components.dialog.Li
|
||||||
import ua.acclorite.book_story.presentation.screens.library.components.dialog.LibraryMoveDialog
|
import ua.acclorite.book_story.presentation.screens.library.components.dialog.LibraryMoveDialog
|
||||||
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
|
import ua.acclorite.book_story.presentation.screens.library.data.LibraryEvent
|
||||||
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
|
import ua.acclorite.book_story.presentation.screens.library.data.LibraryViewModel
|
||||||
|
import ua.acclorite.book_story.ui.DefaultTransition
|
||||||
import ua.acclorite.book_story.ui.Transitions
|
import ua.acclorite.book_story.ui.Transitions
|
||||||
import ua.acclorite.book_story.ui.elevation
|
import ua.acclorite.book_story.ui.elevation
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
@ -178,26 +175,26 @@ fun LibraryScreen(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
content1Actions = {
|
content1Actions = {
|
||||||
IconButton(onClick = { viewModel.onEvent(LibraryEvent.OnSearchShowHide) }) {
|
CustomIconButton(
|
||||||
Icon(
|
icon = Icons.Default.Search,
|
||||||
imageVector = Icons.Default.Search,
|
contentDescription = stringResource(id = R.string.search_content_desc),
|
||||||
contentDescription = "Search books",
|
disableOnClick = false,
|
||||||
modifier = Modifier.size(24.dp),
|
enabled = !state.showSearch
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
) {
|
||||||
)
|
viewModel.onEvent(LibraryEvent.OnSearchShowHide)
|
||||||
}
|
}
|
||||||
MoreDropDown(navigator = navigator)
|
MoreDropDown(navigator = navigator)
|
||||||
},
|
},
|
||||||
|
|
||||||
content2Visibility = state.hasSelectedItems,
|
content2Visibility = state.hasSelectedItems,
|
||||||
content2NavigationIcon = {
|
content2NavigationIcon = {
|
||||||
IconButton(onClick = { viewModel.onEvent(LibraryEvent.OnClearSelectedBooks) }) {
|
CustomIconButton(
|
||||||
Icon(
|
icon = Icons.Default.Clear,
|
||||||
imageVector = Icons.Default.Clear,
|
contentDescription =
|
||||||
contentDescription = "Clear selected items",
|
stringResource(id = R.string.clear_selected_items_content_desc),
|
||||||
modifier = Modifier.size(24.dp),
|
disableOnClick = true
|
||||||
tint = MaterialTheme.colorScheme.onSurface
|
) {
|
||||||
)
|
viewModel.onEvent(LibraryEvent.OnClearSelectedBooks)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
content2Title = {
|
content2Title = {
|
||||||
|
|
@ -213,37 +210,38 @@ fun LibraryScreen(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
content2Actions = {
|
content2Actions = {
|
||||||
IconButton(onClick = { viewModel.onEvent(LibraryEvent.OnShowHideMoveDialog) }) {
|
CustomIconButton(
|
||||||
Icon(
|
icon = Icons.AutoMirrored.Outlined.DriveFileMove,
|
||||||
imageVector = Icons.AutoMirrored.Outlined.DriveFileMove,
|
contentDescription = stringResource(
|
||||||
contentDescription = "Move books to another category",
|
id = R.string.move_books_content_desc
|
||||||
modifier = Modifier.size(24.dp),
|
),
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
disableOnClick = true
|
||||||
)
|
) {
|
||||||
|
viewModel.onEvent(LibraryEvent.OnShowHideMoveDialog)
|
||||||
}
|
}
|
||||||
IconButton(onClick = { viewModel.onEvent(LibraryEvent.OnShowHideDeleteDialog) }) {
|
CustomIconButton(
|
||||||
Icon(
|
icon = Icons.Outlined.Delete,
|
||||||
imageVector = Icons.Outlined.Delete,
|
contentDescription = stringResource(
|
||||||
contentDescription = "Delete books from database",
|
id = R.string.delete_books_content_desc
|
||||||
modifier = Modifier.size(24.dp),
|
),
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
disableOnClick = true
|
||||||
)
|
) {
|
||||||
|
viewModel.onEvent(LibraryEvent.OnShowHideDeleteDialog)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
content3Visibility = state.showSearch && !state.hasSelectedItems,
|
content3Visibility = state.showSearch && !state.hasSelectedItems,
|
||||||
content3NavigationIcon = {
|
content3NavigationIcon = {
|
||||||
IconButton(onClick = {
|
CustomIconButton(
|
||||||
|
icon = Icons.AutoMirrored.Default.ArrowBack,
|
||||||
|
contentDescription = stringResource(
|
||||||
|
id = R.string.exit_search_content_desc
|
||||||
|
),
|
||||||
|
disableOnClick = true
|
||||||
|
) {
|
||||||
viewModel.onEvent(
|
viewModel.onEvent(
|
||||||
LibraryEvent.OnSearchShowHide
|
LibraryEvent.OnSearchShowHide
|
||||||
)
|
)
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.AutoMirrored.Default.ArrowBack,
|
|
||||||
contentDescription = "Exit search mode",
|
|
||||||
modifier = Modifier.size(24.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
content3Title = {
|
content3Title = {
|
||||||
|
|
@ -307,7 +305,7 @@ fun LibraryScreen(
|
||||||
HorizontalPager(state = pagerState, userScrollEnabled = !state.isRefreshing) { index ->
|
HorizontalPager(state = pagerState, userScrollEnabled = !state.isRefreshing) { index ->
|
||||||
var categoryIsLoading by remember { mutableStateOf(true) }
|
var categoryIsLoading by remember { mutableStateOf(true) }
|
||||||
val categorizedBooks = remember { mutableStateListOf<Pair<Book, Boolean>>() }
|
val categorizedBooks = remember { mutableStateListOf<Pair<Book, Boolean>>() }
|
||||||
val category = Category.entries[index]
|
val category = remember { Category.entries[index] }
|
||||||
|
|
||||||
LaunchedEffect(state.books) {
|
LaunchedEffect(state.books) {
|
||||||
categorizedBooks.clear()
|
categorizedBooks.clear()
|
||||||
|
|
@ -321,13 +319,13 @@ fun LibraryScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
DefaultTransition(visible = !state.isLoading && !categoryIsLoading) {
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Adaptive(120.dp),
|
columns = GridCells.Adaptive(120.dp),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(8.dp)
|
contentPadding = PaddingValues(8.dp)
|
||||||
) {
|
) {
|
||||||
if (!state.isLoading) {
|
|
||||||
items(
|
items(
|
||||||
categorizedBooks,
|
categorizedBooks,
|
||||||
key = { it.first.id ?: UUID.randomUUID() }
|
key = { it.first.id ?: UUID.randomUUID() }
|
||||||
|
|
@ -363,16 +361,6 @@ fun LibraryScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.isLoading && !state.isRefreshing) {
|
|
||||||
CircularProgressIndicator(
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
strokeCap = StrokeCap.Round,
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.Center)
|
|
||||||
.size(36.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = !state.isLoading && !state.isRefreshing && categorizedBooks.isEmpty()
|
visible = !state.isLoading && !state.isRefreshing && categorizedBooks.isEmpty()
|
||||||
&& !categoryIsLoading,
|
&& !categoryIsLoading,
|
||||||
|
|
@ -404,7 +392,7 @@ fun LibraryScreen(
|
||||||
|
|
||||||
val activity = LocalContext.current as ComponentActivity
|
val activity = LocalContext.current as ComponentActivity
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var shouldExit = false
|
var shouldExit = remember { false }
|
||||||
BackHandler {
|
BackHandler {
|
||||||
if (state.hasSelectedItems) {
|
if (state.hasSelectedItems) {
|
||||||
viewModel.onEvent(LibraryEvent.OnClearSelectedBooks)
|
viewModel.onEvent(LibraryEvent.OnClearSelectedBooks)
|
||||||
|
|
|
||||||
|
|
@ -25,14 +25,17 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.presentation.data.removeDigits
|
import ua.acclorite.book_story.presentation.data.removeDigits
|
||||||
import ua.acclorite.book_story.presentation.data.removeTrailingZero
|
import ua.acclorite.book_story.presentation.data.removeTrailingZero
|
||||||
|
|
@ -55,6 +58,15 @@ fun LibraryBookItem(
|
||||||
val fontColor = if (book.second) MaterialTheme.colorScheme.onPrimary
|
val fontColor = if (book.second) MaterialTheme.colorScheme.onPrimary
|
||||||
else MaterialTheme.colorScheme.onSurface
|
else MaterialTheme.colorScheme.onSurface
|
||||||
|
|
||||||
|
val progress = remember(book.first) {
|
||||||
|
"${
|
||||||
|
(book.first.progress * 100)
|
||||||
|
.toDouble()
|
||||||
|
.removeDigits(1)
|
||||||
|
.removeTrailingZero()
|
||||||
|
}%"
|
||||||
|
}
|
||||||
|
|
||||||
val animatedBackgroundColor by animateColorAsState(
|
val animatedBackgroundColor by animateColorAsState(
|
||||||
targetValue = backgroundColor,
|
targetValue = backgroundColor,
|
||||||
tween(300),
|
tween(300),
|
||||||
|
|
@ -93,7 +105,9 @@ fun LibraryBookItem(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Image,
|
imageVector = Icons.Default.Image,
|
||||||
contentDescription = "Cover Image not found",
|
contentDescription = stringResource(
|
||||||
|
id = R.string.cover_image_not_found_content_desc
|
||||||
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.Center)
|
.align(Alignment.Center)
|
||||||
.fillMaxWidth(0.7f)
|
.fillMaxWidth(0.7f)
|
||||||
|
|
@ -104,7 +118,7 @@ fun LibraryBookItem(
|
||||||
if (book.first.coverImage != null) {
|
if (book.first.coverImage != null) {
|
||||||
Image(
|
Image(
|
||||||
bitmap = book.first.coverImage!!,
|
bitmap = book.first.coverImage!!,
|
||||||
contentDescription = "Cover",
|
contentDescription = stringResource(id = R.string.cover_image_content_desc),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clip(MaterialTheme.shapes.large),
|
.clip(MaterialTheme.shapes.large),
|
||||||
|
|
@ -113,12 +127,7 @@ fun LibraryBookItem(
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
"${
|
progress,
|
||||||
(book.first.progress * 100)
|
|
||||||
.toDouble()
|
|
||||||
.removeDigits(1)
|
|
||||||
.removeTrailingZero()
|
|
||||||
}%",
|
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
color = MaterialTheme.colorScheme.onPrimary,
|
color = MaterialTheme.colorScheme.onPrimary,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,12 @@ import androidx.compose.material3.TabRowDefaults
|
||||||
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
|
import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
|
|
@ -37,24 +38,27 @@ import ua.acclorite.book_story.ui.elevation
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryTabRow(viewModel: LibraryViewModel, books: List<Book>, pagerState: PagerState) {
|
fun LibraryTabRow(viewModel: LibraryViewModel, books: List<Book>, pagerState: PagerState) {
|
||||||
val tabItems = listOf(
|
val context = LocalContext.current
|
||||||
|
val tabItems = remember(books) {
|
||||||
|
listOf(
|
||||||
Pair(
|
Pair(
|
||||||
stringResource(id = R.string.reading_tab),
|
context.getString(R.string.reading_tab),
|
||||||
books.filter { it.category == Category.READING }.size
|
books.filter { it.category == Category.READING }.size
|
||||||
),
|
),
|
||||||
Pair(
|
Pair(
|
||||||
stringResource(id = R.string.already_read_tab),
|
context.getString(R.string.already_read_tab),
|
||||||
books.filter { it.category == Category.ALREADY_READ }.size
|
books.filter { it.category == Category.ALREADY_READ }.size
|
||||||
),
|
),
|
||||||
Pair(
|
Pair(
|
||||||
stringResource(id = R.string.planning_tab),
|
context.getString(R.string.planning_tab),
|
||||||
books.filter { it.category == Category.PLANNING }.size
|
books.filter { it.category == Category.PLANNING }.size
|
||||||
),
|
),
|
||||||
Pair(
|
Pair(
|
||||||
stringResource(id = R.string.dropped_tab),
|
context.getString(R.string.dropped_tab),
|
||||||
books.filter { it.category == Category.DROPPED }.size
|
books.filter { it.category == Category.DROPPED }.size
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (LocalConfiguration.current.screenWidthDp > 450) {
|
if (LocalConfiguration.current.screenWidthDp > 450) {
|
||||||
TabRow(
|
TabRow(
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,13 @@ class LibraryViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is LibraryEvent.OnSelectBook -> {
|
is LibraryEvent.OnSelectBook -> {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val indexOfBook = _state.value.books.indexOf(event.book)
|
val indexOfBook = _state.value.books.indexOf(event.book)
|
||||||
|
|
||||||
|
if (indexOfBook == -1) {
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
val editedList = _state.value.books.toMutableList()
|
val editedList = _state.value.books.toMutableList()
|
||||||
editedList[indexOfBook] = editedList[indexOfBook].copy(
|
editedList[indexOfBook] = editedList[indexOfBook].copy(
|
||||||
second = event.select ?: !editedList[indexOfBook].second
|
second = event.select ?: !editedList[indexOfBook].second
|
||||||
|
|
@ -170,6 +176,7 @@ class LibraryViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
is LibraryEvent.OnClearSelectedBooks -> {
|
is LibraryEvent.OnClearSelectedBooks -> {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
|
@ -252,7 +259,8 @@ class LibraryViewModel @Inject constructor(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
showDeleteDialog = false
|
showDeleteDialog = false,
|
||||||
|
isLoading = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -269,13 +277,15 @@ class LibraryViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is LibraryEvent.OnUpdateBook -> {
|
is LibraryEvent.OnUpdateBook -> {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val books = _state.value.books.toMutableList()
|
val books = _state.value.books.toMutableList()
|
||||||
|
|
||||||
val index = books.indexOfFirst { it.first.id == event.book.id }
|
val index = books.indexOfFirst { it.first.id == event.book.id }
|
||||||
if (index != -1) {
|
|
||||||
books[index] = Pair(event.book, books[index].second)
|
if (index == -1) {
|
||||||
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
books[index] = Pair(event.book, books[index].second)
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
books = books
|
books = books
|
||||||
|
|
@ -284,6 +294,7 @@ class LibraryViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun getBooksFromDatabase(
|
private suspend fun getBooksFromDatabase(
|
||||||
query: String = if (_state.value.showSearch) _state.value.searchQuery else ""
|
query: String = if (_state.value.showSearch) _state.value.searchQuery else ""
|
||||||
|
|
@ -300,14 +311,7 @@ class LibraryViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is Resource.Loading -> {
|
is Resource.Loading -> Unit
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
isLoading = result.isLoading
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is Resource.Error -> Unit
|
is Resource.Error -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -150,16 +150,19 @@ fun ReaderScreen(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(listState) {
|
LaunchedEffect(listState) {
|
||||||
snapshotFlow {
|
snapshotFlow {
|
||||||
listState.firstVisibleItemIndex
|
listState.firstVisibleItemIndex
|
||||||
}.debounce(50).collectLatest {
|
}.debounce(1000).collectLatest {
|
||||||
if (!loading) {
|
if (!loading) {
|
||||||
|
val lastVisibleItemIndex = listState.layoutInfo.visibleItemsInfo.last().index
|
||||||
|
|
||||||
val progress = if (it > 0) {
|
val progress = if (it > 0) {
|
||||||
if ((it + listState.layoutInfo.visibleItemsInfo.size) >= state.book.text.lastIndex) {
|
if (lastVisibleItemIndex >= (listState.layoutInfo.totalItemsCount - 1)) {
|
||||||
1f
|
1f
|
||||||
} else {
|
} else {
|
||||||
(it.toFloat() / (state.book.text.size - 1).toFloat())
|
(it.toFloat() / (state.book.text.lastIndex).toFloat())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
0f
|
0f
|
||||||
|
|
@ -264,10 +267,10 @@ fun ReaderScreen(
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
.then(
|
.then(
|
||||||
if (!loading && toolbarShowed) {
|
if (!loading && toolbarShowed) {
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
|
||||||
.clickable(
|
.clickable(
|
||||||
interactionSource = null,
|
interactionSource = null,
|
||||||
indication = null,
|
indication = null,
|
||||||
|
|
@ -279,7 +282,6 @@ fun ReaderScreen(
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
|
@ -293,7 +295,11 @@ fun ReaderScreen(
|
||||||
|
|
||||||
itemsIndexed(
|
itemsIndexed(
|
||||||
state.book.text, key = { _, key -> key.id }
|
state.book.text, key = { _, key -> key.id }
|
||||||
) { index, text ->
|
) { index, line ->
|
||||||
|
val text = remember { "${if (paragraphIndentation) " " else ""}${line.line}" }
|
||||||
|
val color = remember { Color(fontColor.toULong()) }
|
||||||
|
val lineHeightSp = remember { (fontSize + lineHeight).sp }
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
.background(Color(backgroundColor.toULong()))
|
.background(Color(backgroundColor.toULong()))
|
||||||
|
|
@ -307,8 +313,8 @@ fun ReaderScreen(
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "${if (paragraphIndentation) " " else ""}${text.line}",
|
text = text,
|
||||||
color = Color(fontColor.toULong()),
|
color = color,
|
||||||
|
|
||||||
style = TextStyle(
|
style = TextStyle(
|
||||||
lineBreak = LineBreak.Paragraph
|
lineBreak = LineBreak.Paragraph
|
||||||
|
|
@ -316,7 +322,7 @@ fun ReaderScreen(
|
||||||
fontFamily = fontFamily.font,
|
fontFamily = fontFamily.font,
|
||||||
fontStyle = fontStyle,
|
fontStyle = fontStyle,
|
||||||
fontSize = fontSize.sp,
|
fontSize = fontSize.sp,
|
||||||
lineHeight = (fontSize + lineHeight).sp
|
lineHeight = lineHeightSp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ fun ReaderStartItem(viewModel: ReaderViewModel) {
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Image,
|
imageVector = Icons.Default.Image,
|
||||||
contentDescription = "Cover Image not found",
|
contentDescription = stringResource(id = R.string.cover_image_not_found_content_desc),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.Center)
|
.align(Alignment.Center)
|
||||||
.fillMaxWidth(0.7f)
|
.fillMaxWidth(0.7f)
|
||||||
|
|
@ -75,7 +75,7 @@ fun ReaderStartItem(viewModel: ReaderViewModel) {
|
||||||
if (state.book.coverImage != null) {
|
if (state.book.coverImage != null) {
|
||||||
Image(
|
Image(
|
||||||
bitmap = state.book.coverImage!!,
|
bitmap = state.book.coverImage!!,
|
||||||
contentDescription = "Cover",
|
contentDescription = stringResource(id = R.string.cover_image_content_desc),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clip(MaterialTheme.shapes.medium),
|
.clip(MaterialTheme.shapes.medium),
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,9 @@ import androidx.compose.material3.SliderDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
@ -39,7 +41,15 @@ fun ReaderBottomBar(
|
||||||
systemBarsColor: Color
|
systemBarsColor: Color
|
||||||
) {
|
) {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
val book = state.book
|
val progress by remember(state.book.progress) {
|
||||||
|
derivedStateOf {
|
||||||
|
(state.book.progress * 100)
|
||||||
|
.toDouble()
|
||||||
|
.removeDigits(4)
|
||||||
|
.removeTrailingZero()
|
||||||
|
.dropWhile { it == '-' } + "%"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
|
|
@ -57,17 +67,12 @@ fun ReaderBottomBar(
|
||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text =
|
text = progress,
|
||||||
(book.progress * 100)
|
|
||||||
.toDouble()
|
|
||||||
.removeDigits(4)
|
|
||||||
.removeTrailingZero()
|
|
||||||
.dropWhile { it == '-' } + "%",
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
style = MaterialTheme.typography.titleLarge
|
style = MaterialTheme.typography.titleLarge
|
||||||
)
|
)
|
||||||
Slider(
|
Slider(
|
||||||
value = book.progress,
|
value = state.book.progress,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
viewModel.onEvent(
|
viewModel.onEvent(
|
||||||
ReaderEvent.OnChangeProgress(
|
ReaderEvent.OnChangeProgress(
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,18 @@ package ua.acclorite.book_story.presentation.screens.reader.components.app_bar
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.foundation.basicMarquee
|
import androidx.compose.foundation.basicMarquee
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -27,6 +25,7 @@ import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
|
import ua.acclorite.book_story.presentation.components.CustomIconButton
|
||||||
import ua.acclorite.book_story.presentation.components.GoBackButton
|
import ua.acclorite.book_story.presentation.components.GoBackButton
|
||||||
import ua.acclorite.book_story.presentation.data.Argument
|
import ua.acclorite.book_story.presentation.data.Argument
|
||||||
import ua.acclorite.book_story.presentation.data.Navigator
|
import ua.acclorite.book_story.presentation.data.Navigator
|
||||||
|
|
@ -44,7 +43,16 @@ import ua.acclorite.book_story.presentation.screens.reader.data.ReaderViewModel
|
||||||
fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColor: Color) {
|
fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColor: Color) {
|
||||||
val context = LocalContext.current as ComponentActivity
|
val context = LocalContext.current as ComponentActivity
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
val book = state.book
|
|
||||||
|
val progress by remember(state.book.progress) {
|
||||||
|
derivedStateOf {
|
||||||
|
(state.book.progress * 100)
|
||||||
|
.toDouble()
|
||||||
|
.removeDigits(2)
|
||||||
|
.removeTrailingZero()
|
||||||
|
.dropWhile { it == '-' } + "%"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
|
|
@ -55,7 +63,7 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo
|
||||||
title = {
|
title = {
|
||||||
Column(verticalArrangement = Arrangement.Center) {
|
Column(verticalArrangement = Arrangement.Center) {
|
||||||
Text(
|
Text(
|
||||||
book.title,
|
state.book.title,
|
||||||
fontFamily = FontFamily.Default,
|
fontFamily = FontFamily.Default,
|
||||||
fontWeight = FontWeight.Normal,
|
fontWeight = FontWeight.Normal,
|
||||||
fontSize = 20.sp,
|
fontSize = 20.sp,
|
||||||
|
|
@ -63,7 +71,7 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(
|
.clickable(
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
interactionSource = null,
|
||||||
indication = null,
|
indication = null,
|
||||||
onClick = {
|
onClick = {
|
||||||
navigator.navigateWithoutBackStack(
|
navigator.navigateWithoutBackStack(
|
||||||
|
|
@ -71,7 +79,7 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo
|
||||||
true,
|
true,
|
||||||
Argument(
|
Argument(
|
||||||
"book",
|
"book",
|
||||||
book
|
state.book
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -83,11 +91,7 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo
|
||||||
Text(
|
Text(
|
||||||
stringResource(
|
stringResource(
|
||||||
id = R.string.read_query,
|
id = R.string.read_query,
|
||||||
(book.progress * 100)
|
progress
|
||||||
.toDouble()
|
|
||||||
.removeDigits(2)
|
|
||||||
.removeTrailingZero()
|
|
||||||
.dropWhile { it == '-' } + "%"
|
|
||||||
),
|
),
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
style = MaterialTheme.typography.bodyLarge
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
|
@ -95,13 +99,12 @@ fun ReaderTopBar(viewModel: ReaderViewModel, navigator: Navigator, containerColo
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = {
|
CustomIconButton(
|
||||||
viewModel.onEvent(ReaderEvent.OnShowHideSettingsBottomSheet)
|
icon = Icons.Default.Settings,
|
||||||
}) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Settings,
|
|
||||||
contentDescription = stringResource(id = R.string.open_reader_settings_content_desc),
|
contentDescription = stringResource(id = R.string.open_reader_settings_content_desc),
|
||||||
)
|
disableOnClick = false
|
||||||
|
) {
|
||||||
|
viewModel.onEvent(ReaderEvent.OnShowHideSettingsBottomSheet)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
|
|
||||||
|
|
@ -74,15 +74,18 @@ fun ReaderSettingsBottomSheet(mainViewModel: MainViewModel, viewModel: ReaderVie
|
||||||
val backgroundColor = mainViewModel.backgroundColor.collectAsState().value!!
|
val backgroundColor = mainViewModel.backgroundColor.collectAsState().value!!
|
||||||
val fontColor = mainViewModel.fontColor.collectAsState().value!!
|
val fontColor = mainViewModel.fontColor.collectAsState().value!!
|
||||||
|
|
||||||
val scrimColor =
|
val scrimColor = if (currentPage == 1) Color.Transparent
|
||||||
if (currentPage == 1) Color.Transparent else BottomSheetDefaults.ScrimColor
|
else BottomSheetDefaults.ScrimColor
|
||||||
|
|
||||||
val animatedScrimColor by animateColorAsState(
|
val animatedScrimColor by animateColorAsState(
|
||||||
targetValue = scrimColor,
|
targetValue = scrimColor,
|
||||||
animationSpec = tween(300),
|
animationSpec = tween(300),
|
||||||
label = "Scrim animation"
|
label = "Scrim animation"
|
||||||
)
|
)
|
||||||
|
|
||||||
val height = if (currentPage == 1) 0.5f else 0.7f
|
val height = remember(currentPage) {
|
||||||
|
if (currentPage == 1) 0.5f else 0.7f
|
||||||
|
}
|
||||||
val animatedHeight by animateFloatAsState(
|
val animatedHeight by animateFloatAsState(
|
||||||
targetValue = height,
|
targetValue = height,
|
||||||
animationSpec = tween(300),
|
animationSpec = tween(300),
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ sealed class ReaderEvent {
|
||||||
val scrollState: LazyListState,
|
val scrollState: LazyListState,
|
||||||
val navigator: Navigator,
|
val navigator: Navigator,
|
||||||
val refreshList: (Book) -> Unit,
|
val refreshList: (Book) -> Unit,
|
||||||
val onLoaded: () -> Unit
|
val onLoaded: () -> Unit,
|
||||||
|
val onTextIsEmpty: () -> Unit
|
||||||
) : ReaderEvent()
|
) : ReaderEvent()
|
||||||
|
|
||||||
data class OnShowHideMenu(val show: Boolean? = null, val context: ComponentActivity) :
|
data class OnShowHideMenu(val show: Boolean? = null, val context: ComponentActivity) :
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,6 @@ data class ReaderState(
|
||||||
val errorMessage: UIText? = null,
|
val errorMessage: UIText? = null,
|
||||||
|
|
||||||
val showMenu: Boolean = false,
|
val showMenu: Boolean = false,
|
||||||
|
|
||||||
val showSettingsBottomSheet: Boolean = false,
|
val showSettingsBottomSheet: Boolean = false,
|
||||||
val currentPage: Int = 0,
|
val currentPage: Int = 0,
|
||||||
|
)
|
||||||
|
|
||||||
)
|
|
||||||
|
|
@ -26,6 +26,8 @@ import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.Book
|
import ua.acclorite.book_story.domain.model.Book
|
||||||
import ua.acclorite.book_story.domain.model.Category
|
import ua.acclorite.book_story.domain.model.Category
|
||||||
import ua.acclorite.book_story.domain.model.History
|
import ua.acclorite.book_story.domain.model.History
|
||||||
|
import ua.acclorite.book_story.domain.model.StringWithId
|
||||||
|
import ua.acclorite.book_story.domain.use_case.GetText
|
||||||
import ua.acclorite.book_story.domain.use_case.InsertHistory
|
import ua.acclorite.book_story.domain.use_case.InsertHistory
|
||||||
import ua.acclorite.book_story.domain.use_case.UpdateBooks
|
import ua.acclorite.book_story.domain.use_case.UpdateBooks
|
||||||
import ua.acclorite.book_story.presentation.data.Argument
|
import ua.acclorite.book_story.presentation.data.Argument
|
||||||
|
|
@ -40,7 +42,8 @@ import kotlin.math.roundToInt
|
||||||
class ReaderViewModel @AssistedInject constructor(
|
class ReaderViewModel @AssistedInject constructor(
|
||||||
@Assisted book: Book,
|
@Assisted book: Book,
|
||||||
private val updateBooks: UpdateBooks,
|
private val updateBooks: UpdateBooks,
|
||||||
private val insertHistory: InsertHistory
|
private val insertHistory: InsertHistory,
|
||||||
|
private val getText: GetText
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _state = MutableStateFlow(ReaderState(book))
|
private val _state = MutableStateFlow(ReaderState(book))
|
||||||
|
|
@ -59,14 +62,27 @@ class ReaderViewModel @AssistedInject constructor(
|
||||||
|
|
||||||
is ReaderEvent.OnLoadText -> {
|
is ReaderEvent.OnLoadText -> {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val text = getText.execute(_state.value.book.id!!)
|
||||||
|
|
||||||
|
if (text.isBlank()) {
|
||||||
|
event.onTextIsEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
val time = Date().time
|
val time = Date().time
|
||||||
val text = _state.value.book.text.joinToString(separator = " ") { it.line }
|
val letters = text
|
||||||
val letters = text.replace(" ", "").length
|
.replace("\n", "")
|
||||||
val words = text.split("\\s+".toRegex()).size
|
.length
|
||||||
|
val words = text
|
||||||
|
.replace("\n", " ")
|
||||||
|
.split("\\s+".toRegex())
|
||||||
|
.size
|
||||||
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
book = it.book.copy(
|
book = it.book.copy(
|
||||||
|
text = text
|
||||||
|
.split("\n")
|
||||||
|
.map { line -> StringWithId(line.trim()) },
|
||||||
lastOpened = time
|
lastOpened = time
|
||||||
),
|
),
|
||||||
letters = letters,
|
letters = letters,
|
||||||
|
|
@ -98,14 +114,20 @@ class ReaderViewModel @AssistedInject constructor(
|
||||||
val scrollTo = (itemsCount * _state.value.book.progress).roundToInt()
|
val scrollTo = (itemsCount * _state.value.book.progress).roundToInt()
|
||||||
if (itemsCount >= _state.value.book.text.size) {
|
if (itemsCount >= _state.value.book.text.size) {
|
||||||
if (scrollTo > 0) {
|
if (scrollTo > 0) {
|
||||||
while (true) {
|
var loaded = false
|
||||||
|
for (i in 1..100) {
|
||||||
try {
|
try {
|
||||||
event.scrollState.scrollToItem(scrollTo)
|
event.scrollState.scrollToItem(scrollTo)
|
||||||
|
loaded = true
|
||||||
break
|
break
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
delay(50)
|
delay(100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!loaded) {
|
||||||
|
event.onTextIsEmpty()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delay(100)
|
delay(100)
|
||||||
|
|
@ -172,12 +194,12 @@ class ReaderViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBooks.execute(
|
updateBooks.execute(
|
||||||
listOf(_state.value.book)
|
listOf(_state.value.book.copy(progress = event.progress))
|
||||||
)
|
)
|
||||||
event.navigator.putArgument(
|
event.navigator.putArgument(
|
||||||
Argument("book", _state.value.book)
|
Argument("book", _state.value.book.copy(progress = event.progress))
|
||||||
)
|
)
|
||||||
event.refreshList(_state.value.book)
|
event.refreshList(_state.value.book.copy(progress = event.progress))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -290,9 +312,6 @@ class ReaderViewModel @AssistedInject constructor(
|
||||||
ReaderState(book = book)
|
ReaderState(book = book)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (book.text.isEmpty()) {
|
|
||||||
onEvent(ReaderEvent.OnTextIsEmpty(onLoaded = { onLoaded() }))
|
|
||||||
} else {
|
|
||||||
onEvent(ReaderEvent.OnShowHideMenu(false, context))
|
onEvent(ReaderEvent.OnShowHideMenu(false, context))
|
||||||
onEvent(
|
onEvent(
|
||||||
ReaderEvent.OnLoadText(
|
ReaderEvent.OnLoadText(
|
||||||
|
|
@ -301,12 +320,14 @@ class ReaderViewModel @AssistedInject constructor(
|
||||||
refreshList = { refreshList(it) },
|
refreshList = { refreshList(it) },
|
||||||
onLoaded = {
|
onLoaded = {
|
||||||
onLoaded()
|
onLoaded()
|
||||||
|
},
|
||||||
|
onTextIsEmpty = {
|
||||||
|
onEvent(ReaderEvent.OnTextIsEmpty(onLoaded = { onLoaded() }))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
package ua.acclorite.book_story.presentation.screens.settings
|
package ua.acclorite.book_story.presentation.screens.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
|
@ -21,6 +23,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.presentation.components.GoBackButton
|
import ua.acclorite.book_story.presentation.components.GoBackButton
|
||||||
import ua.acclorite.book_story.presentation.data.Navigator
|
import ua.acclorite.book_story.presentation.data.Navigator
|
||||||
|
|
@ -97,6 +100,8 @@ fun SettingsScreen(
|
||||||
navigator.navigate(Screen.READER_SETTINGS, false)
|
navigator.navigate(Screen.READER_SETTINGS, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item { Spacer(modifier = Modifier.height(48.dp)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -119,7 +119,7 @@ private fun RevertibleSlider(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.History,
|
imageVector = Icons.Default.History,
|
||||||
contentDescription = "Revert to initial",
|
contentDescription = stringResource(id = R.string.revert_content_desc),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(28.dp),
|
.size(28.dp),
|
||||||
tint = if (initialValue == value.first) MaterialTheme.colorScheme.onSurfaceVariant
|
tint = if (initialValue == value.first) MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,8 @@ fun AppearanceSettings(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item { Spacer(modifier = Modifier.height(48.dp)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -10,6 +10,7 @@ import androidx.compose.foundation.lazy.LazyRow
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -31,8 +32,10 @@ fun AppearanceSettingsThemeSwitcher(
|
||||||
val theme = mainViewModel.theme.collectAsState().value!!
|
val theme = mainViewModel.theme.collectAsState().value!!
|
||||||
val darkTheme = mainViewModel.darkTheme.collectAsState().value!!
|
val darkTheme = mainViewModel.darkTheme.collectAsState().value!!
|
||||||
|
|
||||||
val themes = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Constants.THEMES
|
val themes = remember {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Constants.THEMES
|
||||||
else Constants.THEMES.dropWhile { it.first == Theme.DYNAMIC }
|
else Constants.THEMES.dropWhile { it.first == Theme.DYNAMIC }
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
|
|
|
||||||
|
|
@ -27,8 +27,10 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.ui.Theme
|
import ua.acclorite.book_story.ui.Theme
|
||||||
import ua.acclorite.book_story.ui.colorScheme
|
import ua.acclorite.book_story.ui.colorScheme
|
||||||
import ua.acclorite.book_story.util.UIText
|
import ua.acclorite.book_story.util.UIText
|
||||||
|
|
@ -83,7 +85,7 @@ fun AppearanceSettingsThemeSwitcherItem(
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.CheckCircle,
|
imageVector = Icons.Default.CheckCircle,
|
||||||
contentDescription = null,
|
contentDescription = stringResource(id = R.string.selected_content_desc),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(26.dp),
|
.size(26.dp),
|
||||||
tint =
|
tint =
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
package ua.acclorite.book_story.presentation.screens.settings.nested.general
|
package ua.acclorite.book_story.presentation.screens.settings.nested.general
|
||||||
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
|
@ -18,8 +19,8 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import ua.acclorite.book_story.R
|
import ua.acclorite.book_story.R
|
||||||
import ua.acclorite.book_story.domain.model.ChipItem
|
import ua.acclorite.book_story.domain.model.ChipItem
|
||||||
import ua.acclorite.book_story.presentation.components.GoBackButton
|
import ua.acclorite.book_story.presentation.components.GoBackButton
|
||||||
|
|
@ -42,7 +43,6 @@ fun GeneralSettings(
|
||||||
listState.canScrollForward
|
listState.canScrollForward
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
val context = LocalContext.current as ComponentActivity
|
|
||||||
|
|
||||||
val language = mainViewModel.language.collectAsState().value!!
|
val language = mainViewModel.language.collectAsState().value!!
|
||||||
|
|
||||||
|
|
@ -84,16 +84,17 @@ fun GeneralSettings(
|
||||||
MaterialTheme.typography.labelLarge,
|
MaterialTheme.typography.labelLarge,
|
||||||
it.first == language
|
it.first == language
|
||||||
)
|
)
|
||||||
}
|
}.sortedBy { it.title }
|
||||||
) {
|
) {
|
||||||
mainViewModel.onEvent(
|
mainViewModel.onEvent(
|
||||||
MainEvent.OnChangeLanguage(
|
MainEvent.OnChangeLanguage(
|
||||||
it.id,
|
it.id
|
||||||
context
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item { Spacer(modifier = Modifier.height(48.dp)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -212,6 +212,8 @@ fun ReaderSettings(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item { Spacer(modifier = Modifier.height(48.dp)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 4 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
|
@ -21,7 +21,7 @@
|
||||||
|
|
||||||
<!-- Dialog Descriptions -->
|
<!-- Dialog Descriptions -->
|
||||||
<string name="storage_permission_description">
|
<string name="storage_permission_description">
|
||||||
Нам потрібен дозвіл на сховище, щоб просканувати вашу теку Завантажень.
|
Нам потрібен дозвіл на сховище, щоб просканувати Ваш Девайс на наявність книг.
|
||||||
Без цього дозволу ви не зможете додати книгу.
|
Без цього дозволу ви не зможете додати книгу.
|
||||||
</string>
|
</string>
|
||||||
<string name="add_books_description">
|
<string name="add_books_description">
|
||||||
|
|
@ -50,14 +50,11 @@
|
||||||
|
|
||||||
<!-- Errors -->
|
<!-- Errors -->
|
||||||
<string name="error_permission">
|
<string name="error_permission">
|
||||||
Нам потрібен дозвіл на сховище, щоб просканувати вашу теку Завантажень та отримати книги
|
Нам потрібен дозвіл на сховище, щоб просканувати Ваш Девайс на наявність книг
|
||||||
</string>
|
</string>
|
||||||
<string name="error_no_description">
|
<string name="error_no_description">
|
||||||
Немає опису.
|
Немає опису.
|
||||||
</string>
|
</string>
|
||||||
<string name="error_file_not_found">
|
|
||||||
Файл не знайдено. Будь ласка, перевірте чи файл цієї книги все ще існує.
|
|
||||||
</string>
|
|
||||||
<string name="error_something_went_wrong">
|
<string name="error_something_went_wrong">
|
||||||
Щось пішло не так. Будь ласка, перевірте чи файл цієї книги не пошкоджений.
|
Щось пішло не так. Будь ласка, перевірте чи файл цієї книги не пошкоджений.
|
||||||
</string>
|
</string>
|
||||||
|
|
@ -186,9 +183,9 @@
|
||||||
|
|
||||||
<!-- Toast messages -->
|
<!-- Toast messages -->
|
||||||
<string name="press_again_toast">Натисніть ще раз, щоб вийти</string>
|
<string name="press_again_toast">Натисніть ще раз, щоб вийти</string>
|
||||||
<string name="books_added">Всі книги були успішно додані.</string>
|
<string name="books_added">Всі обрані книги були успішно додані.</string>
|
||||||
<string name="books_moved">Всі книги були успішно переміщені.</string>
|
<string name="books_moved">Всі обрані книги були успішно переміщені.</string>
|
||||||
<string name="books_deleted">Всі книги були успішно видалені.</string>
|
<string name="books_deleted">Всі обрані книги були успішно видалені.</string>
|
||||||
<string name="cover_image_changed">Обкладинка була успішно змінена.</string>
|
<string name="cover_image_changed">Обкладинка була успішно змінена.</string>
|
||||||
<string name="cover_image_deleted">Обкладинка була успішно видалена.</string>
|
<string name="cover_image_deleted">Обкладинка була успішно видалена.</string>
|
||||||
<string name="title_changed">Назва була успішно змінена.</string>
|
<string name="title_changed">Назва була успішно змінена.</string>
|
||||||
|
|
@ -236,5 +233,23 @@
|
||||||
<string name="go_back_content_desc">Назад</string>
|
<string name="go_back_content_desc">Назад</string>
|
||||||
<string name="cover_image_content_desc">Обкладинка</string>
|
<string name="cover_image_content_desc">Обкладинка</string>
|
||||||
<string name="open_reader_settings_content_desc">Відкрити налаштування читача</string>
|
<string name="open_reader_settings_content_desc">Відкрити налаштування читача</string>
|
||||||
|
<string name="background_anim_content_desc">Анімація фону</string>
|
||||||
|
<string name="outline_anim_content_desc">Анімація контуру</string>
|
||||||
|
<string name="top_app_bar_anim_content_desc">Анімація верхньої панелі</string>
|
||||||
|
<string name="file_icon_content_desc">Файл</string>
|
||||||
|
<string name="checkbox_content_desc">Галочка</string>
|
||||||
|
<string name="show_dropdown_content_desc">Показати випадаючий список</string>
|
||||||
|
<string name="cover_image_not_found_content_desc">Обкладенка не знайдена</string>
|
||||||
|
<string name="apply_changes_content_desc">Застосувати зміни</string>
|
||||||
|
<string name="error_content_desc">Помилка</string>
|
||||||
|
<string name="search_content_desc">Пошук</string>
|
||||||
|
<string name="clear_selected_items_content_desc">Очистити вибрані предмети</string>
|
||||||
|
<string name="add_files_content_desc">Додати файли</string>
|
||||||
|
<string name="exit_search_content_desc">Вийти з пошуку</string>
|
||||||
|
<string name="delete_whole_history_content_desc">Видалити всю історію</string>
|
||||||
|
<string name="move_books_content_desc">Перемістити книгу в іншу категорію</string>
|
||||||
|
<string name="delete_books_content_desc">Видалити обрані книги</string>
|
||||||
|
<string name="revert_content_desc">Повернути</string>
|
||||||
|
<string name="selected_content_desc">Вибрано</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<resources>
|
<resources>
|
||||||
<!-- Basic Strings -->
|
<!-- Basic Strings -->
|
||||||
<string name="app_version" translatable="false">0.9.0</string>
|
<string name="app_version" translatable="false">0.9.2</string>
|
||||||
<string name="app_name">Book\'s Story</string>
|
<string name="app_name">Book\'s Story</string>
|
||||||
|
|
||||||
<!-- Screens -->
|
<!-- Screens -->
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
|
|
||||||
<!-- Dialog Descriptions -->
|
<!-- Dialog Descriptions -->
|
||||||
<string name="storage_permission_description">
|
<string name="storage_permission_description">
|
||||||
We need storage permission to scan your Downloads folder.
|
We need storage permission to scan Your Device for books.
|
||||||
Without this permission, you will not be able to add a book.
|
Without this permission, you will not be able to add a book.
|
||||||
</string>
|
</string>
|
||||||
<string name="add_books_description">
|
<string name="add_books_description">
|
||||||
|
|
@ -51,14 +51,11 @@
|
||||||
|
|
||||||
<!-- Errors -->
|
<!-- Errors -->
|
||||||
<string name="error_permission">
|
<string name="error_permission">
|
||||||
We need storage permission to scan your Downloads directory and get books
|
We need storage permission to scan Your Device for books
|
||||||
</string>
|
</string>
|
||||||
<string name="error_no_description">
|
<string name="error_no_description">
|
||||||
No description.
|
No description.
|
||||||
</string>
|
</string>
|
||||||
<string name="error_file_not_found">
|
|
||||||
File was not found. Please check whether this book\'s file still exists.
|
|
||||||
</string>
|
|
||||||
<string name="error_something_went_wrong">
|
<string name="error_something_went_wrong">
|
||||||
Something went wrong. Please check whether this book\'s file is not corrupted.
|
Something went wrong. Please check whether this book\'s file is not corrupted.
|
||||||
</string>
|
</string>
|
||||||
|
|
@ -186,9 +183,9 @@
|
||||||
|
|
||||||
<!-- Toast messages -->
|
<!-- Toast messages -->
|
||||||
<string name="press_again_toast">Press back again to exit</string>
|
<string name="press_again_toast">Press back again to exit</string>
|
||||||
<string name="books_added">All books were successfully added.</string>
|
<string name="books_added">All selected books were successfully added.</string>
|
||||||
<string name="books_moved">All books were successfully moved.</string>
|
<string name="books_moved">All selected books were successfully moved.</string>
|
||||||
<string name="books_deleted">All books were successfully deleted.</string>
|
<string name="books_deleted">All selected books were successfully deleted.</string>
|
||||||
<string name="cover_image_changed">Cover Image was successfully changed.</string>
|
<string name="cover_image_changed">Cover Image was successfully changed.</string>
|
||||||
<string name="cover_image_deleted">Cover Image was successfully deleted.</string>
|
<string name="cover_image_deleted">Cover Image was successfully deleted.</string>
|
||||||
<string name="title_changed">Title was successfully changed.</string>
|
<string name="title_changed">Title was successfully changed.</string>
|
||||||
|
|
@ -236,5 +233,23 @@
|
||||||
<string name="go_back_content_desc">Go back</string>
|
<string name="go_back_content_desc">Go back</string>
|
||||||
<string name="cover_image_content_desc">Cover image</string>
|
<string name="cover_image_content_desc">Cover image</string>
|
||||||
<string name="open_reader_settings_content_desc">Open reader settings</string>
|
<string name="open_reader_settings_content_desc">Open reader settings</string>
|
||||||
|
<string name="background_anim_content_desc">Background animation</string>
|
||||||
|
<string name="outline_anim_content_desc">Outline animation</string>
|
||||||
|
<string name="top_app_bar_anim_content_desc">Top app bar animation</string>
|
||||||
|
<string name="file_icon_content_desc">File</string>
|
||||||
|
<string name="checkbox_content_desc">Checkbox</string>
|
||||||
|
<string name="show_dropdown_content_desc">Show dropdown</string>
|
||||||
|
<string name="cover_image_not_found_content_desc">Cover image not found</string>
|
||||||
|
<string name="apply_changes_content_desc">Apply changes</string>
|
||||||
|
<string name="error_content_desc">Error</string>
|
||||||
|
<string name="search_content_desc">Search</string>
|
||||||
|
<string name="clear_selected_items_content_desc">Clear selected items</string>
|
||||||
|
<string name="add_files_content_desc">Add files</string>
|
||||||
|
<string name="exit_search_content_desc">Exit search</string>
|
||||||
|
<string name="delete_whole_history_content_desc">Delete whole history</string>
|
||||||
|
<string name="move_books_content_desc">Move books to another category</string>
|
||||||
|
<string name="delete_books_content_desc">Delete selected books</string>
|
||||||
|
<string name="revert_content_desc">Revert</string>
|
||||||
|
<string name="selected_content_desc">Selected</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||