0.6.0 - BookInfo Screen
This commit is contained in:
parent
090d5fc003
commit
03b1ef48c2
74 changed files with 2410 additions and 498 deletions
45
.idea/deploymentTargetDropDown.xml
generated
45
.idea/deploymentTargetDropDown.xml
generated
|
|
@ -4,7 +4,29 @@
|
|||
<value>
|
||||
<entry key="app">
|
||||
<State>
|
||||
<targetSelectedWithDropDown>
|
||||
<runningDeviceTargetSelectedWithDropDown>
|
||||
<Target>
|
||||
<type value="RUNNING_DEVICE_TARGET" />
|
||||
<deviceKey>
|
||||
<Key>
|
||||
<type value="VIRTUAL_DEVICE_PATH" />
|
||||
<value value="$USER_HOME$/.android/avd/Phone_13_.avd" />
|
||||
</Key>
|
||||
</deviceKey>
|
||||
</Target>
|
||||
</runningDeviceTargetSelectedWithDropDown>
|
||||
<timeTargetWasSelectedWithDropDown value="2024-02-22T17:49:53.931710327Z" />
|
||||
<multipleDevicesSelectedInDropDown value="true" />
|
||||
<targetsSelectedWithDialog>
|
||||
<Target>
|
||||
<type value="QUICK_BOOT_TARGET" />
|
||||
<deviceKey>
|
||||
<Key>
|
||||
<type value="VIRTUAL_DEVICE_PATH" />
|
||||
<value value="$USER_HOME$/.android/avd/Pixel_3_XL_API_26.avd" />
|
||||
</Key>
|
||||
</deviceKey>
|
||||
</Target>
|
||||
<Target>
|
||||
<type value="QUICK_BOOT_TARGET" />
|
||||
<deviceKey>
|
||||
|
|
@ -14,27 +36,6 @@
|
|||
</Key>
|
||||
</deviceKey>
|
||||
</Target>
|
||||
</targetSelectedWithDropDown>
|
||||
<timeTargetWasSelectedWithDropDown value="2024-02-15T11:58:02.950581796Z" />
|
||||
<targetsSelectedWithDialog>
|
||||
<Target>
|
||||
<type value="QUICK_BOOT_TARGET" />
|
||||
<deviceKey>
|
||||
<Key>
|
||||
<type value="VIRTUAL_DEVICE_PATH" />
|
||||
<value value="C:\Users\Administrator\.android\avd\Resizable_Experimental_API_33.avd" />
|
||||
</Key>
|
||||
</deviceKey>
|
||||
</Target>
|
||||
<Target>
|
||||
<type value="QUICK_BOOT_TARGET" />
|
||||
<deviceKey>
|
||||
<Key>
|
||||
<type value="VIRTUAL_DEVICE_PATH" />
|
||||
<value value="C:\Users\Administrator\.android\avd\Pixel_2_API_26.avd" />
|
||||
</Key>
|
||||
</deviceKey>
|
||||
</Target>
|
||||
</targetsSelectedWithDialog>
|
||||
</State>
|
||||
</entry>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ android {
|
|||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "0.5.0"
|
||||
versionName = "0.6.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
|
@ -27,14 +27,35 @@ android {
|
|||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
getByName("release") {
|
||||
// Enables code shrinking, obfuscation, and optimization for only
|
||||
// your project's release build type. Make sure to use a build
|
||||
// variant with `isDebuggable=false`.
|
||||
isMinifyEnabled = true
|
||||
|
||||
// Enables resource shrinking, which is performed by the
|
||||
// Android Gradle plugin.
|
||||
isShrinkResources = true
|
||||
|
||||
// Includes the default ProGuard rules files that are packaged with
|
||||
// the Android Gradle plugin. To learn more, go to the section about
|
||||
// R8 configuration files.
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
|
||||
|
||||
// release {
|
||||
// isMinifyEnabled = false
|
||||
// proguardFiles(
|
||||
// getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
// "proguard-rules.pro"
|
||||
// )
|
||||
// signingConfig = signingConfigs.getByName("debug")
|
||||
// }
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
|
|
|
|||
62
app/proguard-rules.pro
vendored
62
app/proguard-rules.pro
vendored
|
|
@ -1,21 +1,43 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
# Proguard-rules
|
||||
-dontwarn org.xmlpull.v1.**
|
||||
-dontwarn org.kxml2.io.**
|
||||
-dontwarn android.content.res.**
|
||||
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||
-dontwarn com.gemalto.jp2.JP2Decoder
|
||||
-dontwarn com.google.auto.service.AutoService
|
||||
-dontwarn javax.annotation.processing.AbstractProcessor
|
||||
-dontwarn javax.annotation.processing.Filer
|
||||
-dontwarn javax.annotation.processing.Messager
|
||||
-dontwarn javax.annotation.processing.ProcessingEnvironment
|
||||
-dontwarn javax.lang.model.SourceVersion
|
||||
-dontwarn javax.lang.model.element.AnnotationMirror
|
||||
-dontwarn javax.lang.model.element.AnnotationValue
|
||||
-dontwarn javax.lang.model.element.Element
|
||||
-dontwarn javax.lang.model.element.ElementKind
|
||||
-dontwarn javax.lang.model.element.ElementVisitor
|
||||
-dontwarn javax.lang.model.element.ExecutableElement
|
||||
-dontwarn javax.lang.model.element.Modifier
|
||||
-dontwarn javax.lang.model.element.Name
|
||||
-dontwarn javax.lang.model.element.PackageElement
|
||||
-dontwarn javax.lang.model.element.TypeElement
|
||||
-dontwarn javax.lang.model.element.TypeParameterElement
|
||||
-dontwarn javax.lang.model.element.VariableElement
|
||||
-dontwarn javax.lang.model.type.ArrayType
|
||||
-dontwarn javax.lang.model.type.DeclaredType
|
||||
-dontwarn javax.lang.model.type.ExecutableType
|
||||
-dontwarn javax.lang.model.type.TypeKind
|
||||
-dontwarn javax.lang.model.type.TypeMirror
|
||||
-dontwarn javax.lang.model.type.TypeVariable
|
||||
-dontwarn javax.lang.model.type.TypeVisitor
|
||||
-dontwarn javax.lang.model.util.AbstractAnnotationValueVisitor8
|
||||
-dontwarn javax.lang.model.util.AbstractTypeVisitor8
|
||||
-dontwarn javax.lang.model.util.ElementFilter
|
||||
-dontwarn javax.lang.model.util.Elements
|
||||
-dontwarn javax.lang.model.util.SimpleElementVisitor8
|
||||
-dontwarn javax.lang.model.util.SimpleTypeVisitor7
|
||||
-dontwarn javax.lang.model.util.SimpleTypeVisitor8
|
||||
-dontwarn javax.lang.model.util.Types
|
||||
-dontwarn javax.tools.Diagnostic$Kind
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
-keep class org.xmlpull.** { *; }
|
||||
-keepclassmembers class org.xmlpull.** { *; }
|
||||
|
|
@ -1,13 +1,11 @@
|
|||
package com.acclorite.books_history
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
<uses-permission
|
||||
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name=".util.BookApplication"
|
||||
|
|
|
|||
|
|
@ -17,12 +17,14 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.acclorite.books_history.domain.model.Book
|
||||
import com.acclorite.books_history.presentation.NavigationHost
|
||||
import com.acclorite.books_history.presentation.Screen
|
||||
import com.acclorite.books_history.presentation.components.BottomNavigationBar
|
||||
import com.acclorite.books_history.presentation.main.MainEvent
|
||||
import com.acclorite.books_history.presentation.main.MainViewModel
|
||||
import com.acclorite.books_history.presentation.screens.book_info.BookInfoScreen
|
||||
import com.acclorite.books_history.presentation.screens.browse.BrowseScreen
|
||||
import com.acclorite.books_history.presentation.screens.library.LibraryScreen
|
||||
import com.acclorite.books_history.presentation.screens.settings.SettingsScreen
|
||||
|
|
@ -41,6 +43,7 @@ class MainActivity : ComponentActivity() {
|
|||
private val mainViewModel: MainViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
mainViewModel.init(this)
|
||||
|
|
@ -49,7 +52,9 @@ class MainActivity : ComponentActivity() {
|
|||
!mainViewModel.isReady.value
|
||||
}
|
||||
}
|
||||
|
||||
enableEdgeToEdge()
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
setContent {
|
||||
val updating = mainViewModel.updating.collectAsState().value
|
||||
|
|
@ -95,6 +100,18 @@ class MainActivity : ComponentActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
// Book Info
|
||||
composable(
|
||||
screen = Screen.BOOK_INFO,
|
||||
enterAnim = Transitions.SlidingTransitionIn,
|
||||
exitAnim = Transitions.SlidingTransitionOut
|
||||
) {
|
||||
|
||||
BookInfoScreen(
|
||||
navigator = this@NavigationHost
|
||||
)
|
||||
}
|
||||
|
||||
// Settings
|
||||
composable(
|
||||
screen = Screen.SETTINGS,
|
||||
|
|
@ -107,6 +124,7 @@ class MainActivity : ComponentActivity() {
|
|||
)
|
||||
}
|
||||
|
||||
// Nested categories
|
||||
composable(
|
||||
screen = Screen.GENERAL_SETTINGS,
|
||||
enterAnim = Transitions.SlidingTransitionIn,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ import javax.inject.Singleton
|
|||
object AppModule {
|
||||
|
||||
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideBookDao(app: Application): BookDao {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,6 @@ import androidx.datastore.preferences.core.Preferences
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface DataStore {
|
||||
suspend fun <T> getData (key: Preferences.Key<T>, defaultValue: T): Flow<T>
|
||||
suspend fun <T> putData (key: Preferences.Key<T>, value: T)
|
||||
suspend fun <T> getData(key: Preferences.Key<T>, defaultValue: T): Flow<T>
|
||||
suspend fun <T> putData(key: Preferences.Key<T>, value: T)
|
||||
}
|
||||
|
|
@ -14,18 +14,18 @@ import javax.inject.Inject
|
|||
|
||||
private val Context.dataStore by preferencesDataStore("data_store")
|
||||
|
||||
class DataStoreImpl @Inject constructor(context: Application): DataStore {
|
||||
class DataStoreImpl @Inject constructor(context: Application) : DataStore {
|
||||
private val dataStore = context.dataStore
|
||||
|
||||
override suspend fun <T> getData(key: Preferences.Key<T>, defaultValue: T): Flow<T> =
|
||||
dataStore.data.catch { exception ->
|
||||
if (exception is IOException){
|
||||
if (exception is IOException) {
|
||||
emit(emptyPreferences())
|
||||
} else{
|
||||
} else {
|
||||
throw exception
|
||||
}
|
||||
}.map { preferences ->
|
||||
val result = preferences[key]?: defaultValue
|
||||
val result = preferences[key] ?: defaultValue
|
||||
result
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
package com.acclorite.books_history.data.mapper
|
||||
|
||||
import com.acclorite.books_history.data.local.dto.BookEntity
|
||||
import com.acclorite.books_history.data.local.room.BookDao
|
||||
import com.acclorite.books_history.domain.model.Book
|
||||
|
||||
interface BookMapper {
|
||||
suspend fun toBookEntity(book: Book, database: BookDao): BookEntity
|
||||
suspend fun toBookEntity(book: Book): BookEntity
|
||||
|
||||
suspend fun toBook(bookEntity: BookEntity): Book
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import android.graphics.Bitmap
|
|||
import android.graphics.BitmapFactory
|
||||
import com.acclorite.books_history.R
|
||||
import com.acclorite.books_history.data.local.dto.BookEntity
|
||||
import com.acclorite.books_history.data.local.room.BookDao
|
||||
import com.acclorite.books_history.data.parser.EpubFileParser
|
||||
import com.acclorite.books_history.data.parser.PdfFileParser
|
||||
import com.acclorite.books_history.data.parser.TxtFileParser
|
||||
|
|
@ -20,28 +19,14 @@ class BookMapperImpl @Inject constructor(
|
|||
private val pdfFileParser: PdfFileParser,
|
||||
private val epubFileParser: EpubFileParser
|
||||
) : BookMapper {
|
||||
override suspend fun toBookEntity(book: Book, database: BookDao): BookEntity {
|
||||
override suspend fun toBookEntity(book: Book): BookEntity {
|
||||
val stream = ByteArrayOutputStream()
|
||||
book.coverImage?.compress(Bitmap.CompressFormat.JPEG, 50, stream)
|
||||
|
||||
if (book.file == null) {
|
||||
val bookWithoutFile = database.findBookById(book.id!!)
|
||||
book.coverImage?.compress(Bitmap.CompressFormat.JPEG, 30, stream)
|
||||
|
||||
return BookEntity(
|
||||
id = book.id,
|
||||
title = book.title,
|
||||
filePath = bookWithoutFile.filePath,
|
||||
progress = book.progress,
|
||||
lastOpened = book.lastOpened,
|
||||
image = stream.toByteArray(),
|
||||
category = book.category
|
||||
)
|
||||
}
|
||||
|
||||
return BookEntity(
|
||||
id = book.id,
|
||||
title = book.title,
|
||||
filePath = book.file.path,
|
||||
filePath = book.filePath,
|
||||
progress = book.progress,
|
||||
lastOpened = book.lastOpened,
|
||||
image = stream.toByteArray(),
|
||||
|
|
@ -73,6 +58,7 @@ class BookMapperImpl @Inject constructor(
|
|||
text = emptyList(),
|
||||
progress = bookEntity.progress,
|
||||
file = file,
|
||||
filePath = bookEntity.filePath,
|
||||
lastOpened = bookEntity.lastOpened,
|
||||
category = bookEntity.category,
|
||||
coverImage = if (bookEntity.image.isNotEmpty()) image else null
|
||||
|
|
@ -86,6 +72,7 @@ class BookMapperImpl @Inject constructor(
|
|||
description = null,
|
||||
progress = bookEntity.progress,
|
||||
file = null,
|
||||
filePath = bookEntity.filePath,
|
||||
lastOpened = bookEntity.lastOpened,
|
||||
category = bookEntity.category,
|
||||
coverImage = if (bookEntity.image.isNotEmpty()) image else null
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class EpubFileParser @Inject constructor() : FileParser {
|
|||
text = emptyList(),
|
||||
progress = 0f,
|
||||
file = file,
|
||||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = coverImage
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ class PdfFileParser @Inject constructor(private val application: Application) :
|
|||
text = text,
|
||||
progress = 0f,
|
||||
file = file,
|
||||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ class TxtFileParser @Inject constructor() : FileParser {
|
|||
text = text,
|
||||
progress = 0f,
|
||||
file = file,
|
||||
filePath = file.path,
|
||||
lastOpened = null,
|
||||
category = Category.entries[0],
|
||||
coverImage = null
|
||||
|
|
|
|||
|
|
@ -66,15 +66,15 @@ class BookRepositoryImpl @Inject constructor(
|
|||
}
|
||||
|
||||
override suspend fun insertBooks(books: List<Book>) {
|
||||
database.insertBooks(books.map { bookMapper.toBookEntity(it, database) })
|
||||
database.insertBooks(books.map { bookMapper.toBookEntity(it) })
|
||||
}
|
||||
|
||||
override suspend fun updateBooks(books: List<Book>) {
|
||||
database.updateBooks(books.map { bookMapper.toBookEntity(it, database) })
|
||||
database.updateBooks(books.map { bookMapper.toBookEntity(it) })
|
||||
}
|
||||
|
||||
override suspend fun deleteBooks(books: List<Book>) {
|
||||
database.deleteBooks(books.map { bookMapper.toBookEntity(it, database) })
|
||||
database.deleteBooks(books.map { bookMapper.toBookEntity(it) })
|
||||
}
|
||||
|
||||
override suspend fun <T> retrieveDataFromDataStore(
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ data class Book(
|
|||
val text: List<StringWithId> = emptyList(),
|
||||
val progress: Float,
|
||||
val file: File?,
|
||||
val filePath: String,
|
||||
val lastOpened: Long?,
|
||||
val category: Category,
|
||||
val coverImage: Bitmap?
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
package com.acclorite.books_history.domain.use_case
|
||||
|
||||
import com.acclorite.books_history.domain.model.Book
|
||||
import com.acclorite.books_history.domain.repository.BookRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class DeleteBooks @Inject constructor(private val repository: BookRepository) {
|
||||
|
||||
suspend fun execute(books: List<Book>) {
|
||||
repository.deleteBooks(books)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import javax.inject.Inject
|
|||
|
||||
class GetDatastore @Inject constructor(private val repository: BookRepository) {
|
||||
|
||||
suspend fun <T> execute (key: Preferences.Key<T>, defaultValue: T): Flow<T> {
|
||||
suspend fun <T> execute(key: Preferences.Key<T>, defaultValue: T): Flow<T> {
|
||||
return repository.retrieveDataFromDataStore(key, defaultValue)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ import javax.inject.Inject
|
|||
|
||||
class SetDatastore @Inject constructor(private val repository: BookRepository) {
|
||||
|
||||
suspend fun <T> execute (key: Preferences.Key<T>, value: T) {
|
||||
suspend fun <T> execute(key: Preferences.Key<T>, value: T) {
|
||||
repository.putDataToDataStore(key, value)
|
||||
}
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ enum class Screen {
|
|||
HISTORY,
|
||||
BROWSE,
|
||||
|
||||
BOOKS_INFO,
|
||||
BOOK_INFO,
|
||||
READER,
|
||||
|
||||
SETTINGS,
|
||||
|
|
@ -134,10 +134,6 @@ class Navigator @AssistedInject constructor(
|
|||
return currentScreen
|
||||
}
|
||||
|
||||
fun init(startScreen: Screen) {
|
||||
savedStateHandle[CURRENT_SCREEN] = startScreen
|
||||
}
|
||||
|
||||
/**
|
||||
* Animated Screen. Using in [NavigationHost]. Be sure to not use the same [screen] parameter twice, it'll override the highest one in your code.
|
||||
*
|
||||
|
|
@ -187,10 +183,11 @@ fun NavigationHost(
|
|||
) {
|
||||
val activity = LocalContext.current as ComponentActivity
|
||||
|
||||
activity.apply {
|
||||
val navigator by viewModels<Navigator>(
|
||||
val navigator by activity.viewModels<Navigator>(
|
||||
extrasProducer = {
|
||||
defaultViewModelCreationExtras.withCreationCallback<Navigator.Factory> { factory ->
|
||||
activity
|
||||
.defaultViewModelCreationExtras
|
||||
.withCreationCallback<Navigator.Factory> { factory ->
|
||||
factory.create(startScreen)
|
||||
}
|
||||
}
|
||||
|
|
@ -211,5 +208,4 @@ fun NavigationHost(
|
|||
)
|
||||
|
||||
content(navigator)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
package com.acclorite.books_history.presentation.components
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
|
|
@ -13,19 +16,21 @@ import androidx.compose.material3.TopAppBar
|
|||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.acclorite.books_history.ui.DefaultTransition
|
||||
import com.acclorite.books_history.ui.elevation
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AnimatedTopAppBar(
|
||||
containerColor: Color,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surface,
|
||||
scrolledContainerColor: Color?,
|
||||
|
||||
scrollBehavior: TopAppBarScrollBehavior?,
|
||||
isTopBarScrolled: Boolean?,
|
||||
|
||||
content1Visibility: Boolean,
|
||||
content1NavigationIcon: @Composable () -> Unit,
|
||||
|
|
@ -42,6 +47,16 @@ fun AnimatedTopAppBar(
|
|||
content3Title: @Composable () -> Unit,
|
||||
content3Actions: @Composable RowScope.() -> Unit
|
||||
) {
|
||||
val color =
|
||||
if (isTopBarScrolled == true && scrolledContainerColor != null) scrolledContainerColor
|
||||
else containerColor
|
||||
|
||||
val animatedColor by animateColorAsState(
|
||||
targetValue = color,
|
||||
animationSpec = spring(stiffness = Spring.StiffnessMediumLow),
|
||||
label = "TopAppBar color animation"
|
||||
)
|
||||
|
||||
if (scrollBehavior != null && scrolledContainerColor != null) {
|
||||
val isScrolled = scrollBehavior.state.overlappedFraction > 0.01f
|
||||
val statusBarPadding = with(LocalDensity.current) {
|
||||
|
|
@ -67,8 +82,8 @@ fun AnimatedTopAppBar(
|
|||
actions = content1Actions,
|
||||
scrollBehavior = scrollBehavior,
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = containerColor,
|
||||
scrolledContainerColor = MaterialTheme.elevation()
|
||||
containerColor = animatedColor,
|
||||
scrolledContainerColor = scrolledContainerColor ?: Color.Transparent
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -80,8 +95,8 @@ fun AnimatedTopAppBar(
|
|||
actions = content2Actions,
|
||||
scrollBehavior = scrollBehavior,
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = containerColor,
|
||||
scrolledContainerColor = MaterialTheme.elevation()
|
||||
containerColor = animatedColor,
|
||||
scrolledContainerColor = scrolledContainerColor ?: Color.Transparent
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -93,8 +108,8 @@ fun AnimatedTopAppBar(
|
|||
actions = content3Actions,
|
||||
scrollBehavior = scrollBehavior,
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = containerColor,
|
||||
scrolledContainerColor = MaterialTheme.elevation()
|
||||
containerColor = animatedColor,
|
||||
scrolledContainerColor = scrolledContainerColor ?: Color.Transparent
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import androidx.compose.material3.NavigationBar
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.acclorite.books_history.R
|
||||
|
|
@ -27,7 +26,6 @@ fun BottomNavigationBar(
|
|||
containerColor = MaterialTheme.elevation()
|
||||
) {
|
||||
BottomNavigationBarItem(
|
||||
Modifier.weight(1f),
|
||||
item = NavigationBarItem(
|
||||
stringResource(id = R.string.library_screen),
|
||||
selectedIcon = painterResource(id = R.drawable.library_screen_filled),
|
||||
|
|
@ -38,7 +36,6 @@ fun BottomNavigationBar(
|
|||
navigator.navigate(Screen.LIBRARY)
|
||||
}
|
||||
BottomNavigationBarItem(
|
||||
Modifier.weight(1f),
|
||||
item = NavigationBarItem(
|
||||
stringResource(id = R.string.history_screen),
|
||||
selectedIcon = painterResource(id = R.drawable.history_screen_filled),
|
||||
|
|
@ -49,7 +46,6 @@ fun BottomNavigationBar(
|
|||
navigator.navigate(Screen.HISTORY)
|
||||
}
|
||||
BottomNavigationBarItem(
|
||||
Modifier.weight(1f),
|
||||
item = NavigationBarItem(
|
||||
stringResource(id = R.string.browse_screen),
|
||||
selectedIcon = painterResource(id = R.drawable.browse_screen_filled),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package com.acclorite.books_history.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -18,13 +17,12 @@ import com.acclorite.books_history.domain.model.NavigationBarItem
|
|||
* Bottom Navigation Bar Item, uses default [NavigationBarItem].
|
||||
*/
|
||||
@Composable
|
||||
fun BottomNavigationBarItem(
|
||||
modifier: Modifier,
|
||||
fun RowScope.BottomNavigationBarItem(
|
||||
modifier: Modifier = Modifier,
|
||||
item: NavigationBarItem,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(modifier.fillMaxHeight()) {
|
||||
NavigationBarItem(
|
||||
label = {
|
||||
Text(
|
||||
|
|
@ -46,6 +44,7 @@ fun BottomNavigationBarItem(
|
|||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
},
|
||||
modifier = modifier,
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
|
|
@ -53,5 +52,4 @@ fun BottomNavigationBarItem(
|
|||
disabledTextColor = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
package com.acclorite.books_history.presentation.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.compose.ui.window.DialogWindowProvider
|
||||
import com.acclorite.books_history.R
|
||||
import com.acclorite.books_history.ui.ElevationDefaults
|
||||
import com.acclorite.books_history.ui.elevation
|
||||
|
||||
@Composable
|
||||
fun CustomAlertDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
properties: DialogProperties = DialogProperties(),
|
||||
drawableIcon: Painter? = null,
|
||||
imageVectorIcon: ImageVector? = null,
|
||||
backgroundTransparency: Float = 0.2f,
|
||||
title: String,
|
||||
description: String,
|
||||
actionText: String,
|
||||
onDismiss: () -> Unit,
|
||||
onAction: () -> Unit
|
||||
) {
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
properties = properties,
|
||||
confirmButton = {
|
||||
TextButton(onClick = { onAction() }) {
|
||||
Text(
|
||||
text = actionText,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
shape = MaterialTheme.shapes.extraLarge,
|
||||
modifier = modifier
|
||||
.navigationBarsPadding()
|
||||
.statusBarsPadding()
|
||||
.clip(MaterialTheme.shapes.extraLarge)
|
||||
.background(MaterialTheme.elevation(ElevationDefaults.DialogElevation)),
|
||||
containerColor = Color.Transparent,
|
||||
dismissButton = {
|
||||
TextButton(onClick = { onDismiss() }) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.cancel),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
if (drawableIcon != null) {
|
||||
Icon(
|
||||
painter = drawableIcon,
|
||||
contentDescription = title,
|
||||
modifier = Modifier
|
||||
.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
if (imageVectorIcon != null) {
|
||||
Icon(
|
||||
imageVector = imageVectorIcon,
|
||||
contentDescription = title,
|
||||
modifier = Modifier
|
||||
.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
},
|
||||
text = {
|
||||
(LocalView.current.parent as DialogWindowProvider).window.setDimAmount(
|
||||
backgroundTransparency
|
||||
)
|
||||
Text(
|
||||
text = description,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ fun CustomDialogWithContent(
|
|||
properties: DialogProperties = DialogProperties(),
|
||||
drawableIcon: Painter? = null,
|
||||
imageVectorIcon: ImageVector? = null,
|
||||
backgroundTransparency: Float = 0.2f,
|
||||
backgroundTransparency: Float = 0.3f,
|
||||
title: String,
|
||||
description: String?,
|
||||
actionText: String?,
|
||||
|
|
@ -63,7 +63,7 @@ fun CustomDialogWithContent(
|
|||
.navigationBarsPadding()
|
||||
.statusBarsPadding()
|
||||
.clip(MaterialTheme.shapes.extraLarge)
|
||||
.background(MaterialTheme.elevation(ElevationDefaults.DialogElevation))
|
||||
.background(MaterialTheme.elevation(ElevationDefaults.Dialog))
|
||||
.padding(top = 24.dp, bottom = 12.dp)
|
||||
) {
|
||||
if (drawableIcon != null) {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ fun CustomDialogWithLazyColumn(
|
|||
properties: DialogProperties = DialogProperties(),
|
||||
drawableIcon: Painter? = null,
|
||||
imageVectorIcon: ImageVector? = null,
|
||||
backgroundTransparency: Float = 0.2f,
|
||||
backgroundTransparency: Float = 0.3f,
|
||||
title: String,
|
||||
description: String?,
|
||||
actionText: String?,
|
||||
|
|
@ -65,7 +65,7 @@ fun CustomDialogWithLazyColumn(
|
|||
.navigationBarsPadding()
|
||||
.statusBarsPadding()
|
||||
.clip(MaterialTheme.shapes.extraLarge)
|
||||
.background(MaterialTheme.elevation(ElevationDefaults.DialogElevation))
|
||||
.background(MaterialTheme.elevation(ElevationDefaults.Dialog))
|
||||
.padding(top = 24.dp, bottom = 12.dp)
|
||||
) {
|
||||
if (drawableIcon != null) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
package com.acclorite.books_history.presentation.components
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun CustomDropDownMenuItem(
|
||||
leadingIcon: ImageVector,
|
||||
text: String,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
leadingIcon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
|
||||
},
|
||||
text = {
|
||||
Text(
|
||||
text = text,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onClick()
|
||||
},
|
||||
contentPadding = PaddingValues(start = 12.dp, end = 0.dp)
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,282 @@
|
|||
package com.acclorite.books_history.presentation.screens.book_info
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Done
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.acclorite.books_history.R
|
||||
import com.acclorite.books_history.domain.model.Book
|
||||
import com.acclorite.books_history.presentation.Argument
|
||||
import com.acclorite.books_history.presentation.Navigator
|
||||
import com.acclorite.books_history.presentation.Screen
|
||||
import com.acclorite.books_history.presentation.screens.book_info.components.BookInfoBackground
|
||||
import com.acclorite.books_history.presentation.screens.book_info.components.BookInfoChangeCoverBottomSheet
|
||||
import com.acclorite.books_history.presentation.screens.book_info.components.BookInfoDeleteDialog
|
||||
import com.acclorite.books_history.presentation.screens.book_info.components.BookInfoDescriptionSection
|
||||
import com.acclorite.books_history.presentation.screens.book_info.components.BookInfoDetailsBottomSheet
|
||||
import com.acclorite.books_history.presentation.screens.book_info.components.BookInfoInfoSection
|
||||
import com.acclorite.books_history.presentation.screens.book_info.components.BookInfoMoreDropDown
|
||||
import com.acclorite.books_history.presentation.screens.book_info.components.BookInfoMoveDialog
|
||||
import com.acclorite.books_history.presentation.screens.book_info.components.BookInfoStatisticSection
|
||||
import com.acclorite.books_history.presentation.screens.book_info.data.BookInfoEvent
|
||||
import com.acclorite.books_history.presentation.screens.book_info.data.BookInfoViewModel
|
||||
import com.acclorite.books_history.presentation.screens.library.data.LibraryEvent
|
||||
import com.acclorite.books_history.presentation.screens.library.data.LibraryViewModel
|
||||
import com.acclorite.books_history.ui.DefaultTransition
|
||||
import com.acclorite.books_history.ui.Transitions
|
||||
import com.acclorite.books_history.ui.elevation
|
||||
import com.acclorite.books_history.util.Constants
|
||||
import dagger.hilt.android.lifecycle.withCreationCallback
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BookInfoScreen(
|
||||
libraryViewModel: LibraryViewModel = hiltViewModel(),
|
||||
navigator: Navigator
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val viewModel by (context as ComponentActivity).viewModels<BookInfoViewModel>(
|
||||
extrasProducer = {
|
||||
val book = navigator.retrieveArgument("book") as? Book
|
||||
if (book == null) {
|
||||
navigator.navigateBack()
|
||||
}
|
||||
|
||||
context.defaultViewModelCreationExtras
|
||||
.withCreationCallback<BookInfoViewModel.Factory> { factory ->
|
||||
factory.create(book ?: Constants.EMPTY_BOOK)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val state by viewModel.state.collectAsState()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.init(
|
||||
navigator,
|
||||
failure = {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(
|
||||
R.string.file_not_found,
|
||||
it.takeLast(40)
|
||||
),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (state.showChangeCoverBottomSheet) {
|
||||
BookInfoChangeCoverBottomSheet(libraryViewModel = libraryViewModel, viewModel = viewModel)
|
||||
}
|
||||
if (state.showDetailsBottomSheet) {
|
||||
BookInfoDetailsBottomSheet(viewModel = viewModel)
|
||||
}
|
||||
if (state.showDeleteDialog) {
|
||||
BookInfoDeleteDialog(
|
||||
libraryViewModel = libraryViewModel,
|
||||
viewModel = viewModel,
|
||||
navigator = navigator
|
||||
)
|
||||
}
|
||||
if (state.showMoveDialog) {
|
||||
BookInfoMoveDialog(
|
||||
libraryViewModel = libraryViewModel,
|
||||
viewModel = viewModel,
|
||||
navigator = navigator
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.windowInsetsPadding(WindowInsets.navigationBars),
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = {
|
||||
IconButton(onClick = {
|
||||
navigator.navigateBack()
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
|
||||
contentDescription = "Go back",
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
},
|
||||
title = {},
|
||||
actions = {
|
||||
Box {
|
||||
DefaultTransition(
|
||||
visible = !state.editTitle
|
||||
) {
|
||||
BookInfoMoreDropDown(viewModel = viewModel)
|
||||
}
|
||||
androidx.compose.animation.AnimatedVisibility(
|
||||
visible = state.editTitle,
|
||||
enter = Transitions.DefaultTransitionIn,
|
||||
exit = fadeOut(tween(200))
|
||||
) {
|
||||
IconButton(
|
||||
enabled = state.titleValue.isNotBlank() &&
|
||||
state.titleValue.trim() != state.book.title.trim(),
|
||||
onClick = {
|
||||
viewModel.onEvent(BookInfoEvent.OnUpdateTitle(
|
||||
refreshList = {
|
||||
libraryViewModel.onEvent(
|
||||
LibraryEvent.OnLoadList
|
||||
)
|
||||
}
|
||||
))
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.title_changed),
|
||||
Toast.LENGTH_LONG
|
||||
).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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color.Transparent,
|
||||
scrolledContainerColor = MaterialTheme.elevation()
|
||||
)
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
if (state.book.coverImage != null) {
|
||||
BookInfoBackground(image = state.book.coverImage!!.asImageBitmap())
|
||||
}
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = paddingValues.calculateTopPadding())
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.padding(horizontal = 24.dp)
|
||||
) {
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
// Info
|
||||
BookInfoInfoSection(viewModel = viewModel, book = state.book)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
// Statistic
|
||||
BookInfoStatisticSection(book = state.book)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
// Description
|
||||
BookInfoDescriptionSection(book = state.book)
|
||||
}
|
||||
|
||||
if (state.book.file != null) {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
navigator.navigate(
|
||||
Screen.READER, Argument(
|
||||
"book", state.book
|
||||
)
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(16.dp),
|
||||
shape = MaterialTheme.shapes.large,
|
||||
elevation = FloatingActionButtonDefaults.elevation(),
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
content = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Spacer(modifier = Modifier.width(13.dp))
|
||||
Icon(
|
||||
imageVector = Icons.Filled.PlayArrow,
|
||||
contentDescription = "Continue reading",
|
||||
Modifier.size(24.dp)
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = scrollState.value < 5
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(
|
||||
if (state.book.progress == 0f) R.string.start
|
||||
else R.string.continue_read
|
||||
),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
maxLines = 1,
|
||||
modifier = Modifier.padding(start = 8.dp)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.acclorite.books_history.presentation.screens.book_info.components
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun BookInfoBackground(image: ImageBitmap) {
|
||||
Image(
|
||||
bitmap = image,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(0.5f)
|
||||
.blur(1.5.dp),
|
||||
alpha = 0.3f,
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(0.51f)
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
listOf(
|
||||
Color.Transparent,
|
||||
MaterialTheme.colorScheme.surface
|
||||
),
|
||||
startY = 0f,
|
||||
endY = 750f
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package com.acclorite.books_history.presentation.screens.book_info.components
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.HideImage
|
||||
import androidx.compose.material.icons.filled.ImageSearch
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.acclorite.books_history.R
|
||||
import com.acclorite.books_history.presentation.screens.book_info.data.BookInfoEvent
|
||||
import com.acclorite.books_history.presentation.screens.book_info.data.BookInfoViewModel
|
||||
import com.acclorite.books_history.presentation.screens.library.data.LibraryEvent
|
||||
import com.acclorite.books_history.presentation.screens.library.data.LibraryViewModel
|
||||
import com.acclorite.books_history.ui.ElevationDefaults
|
||||
import com.acclorite.books_history.ui.elevation
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BookInfoChangeCoverBottomSheet(
|
||||
libraryViewModel: LibraryViewModel,
|
||||
viewModel: BookInfoViewModel
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val context = LocalContext.current
|
||||
val book = state.book
|
||||
|
||||
val photoPicker = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.PickVisualMedia(),
|
||||
onResult = { uri ->
|
||||
if (uri != null) {
|
||||
viewModel.onEvent(
|
||||
BookInfoEvent.OnChangeCover(
|
||||
uri,
|
||||
context,
|
||||
refreshList = {
|
||||
libraryViewModel.onEvent(LibraryEvent.OnLoadList)
|
||||
}
|
||||
)
|
||||
)
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.cover_image_changed),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = {
|
||||
viewModel.onEvent(BookInfoEvent.OnShowHideChangeCoverBottomSheet)
|
||||
},
|
||||
windowInsets = WindowInsets(0, 0, 0, 0),
|
||||
containerColor = MaterialTheme.elevation(ElevationDefaults.BottomSheet)
|
||||
) {
|
||||
|
||||
BookInfoChangeCoverBottomSheetItem(
|
||||
icon = Icons.Default.ImageSearch,
|
||||
text = stringResource(id = R.string.change_cover),
|
||||
description = stringResource(id = R.string.change_cover_desc)
|
||||
) {
|
||||
photoPicker.launch(
|
||||
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
|
||||
)
|
||||
}
|
||||
|
||||
if (book.coverImage != null) {
|
||||
BookInfoChangeCoverBottomSheetItem(
|
||||
icon = Icons.Default.HideImage,
|
||||
text = stringResource(id = R.string.delete_cover),
|
||||
description = stringResource(id = R.string.delete_cover_desc)
|
||||
) {
|
||||
viewModel.onEvent(BookInfoEvent.OnDeleteCover(
|
||||
refreshList = {
|
||||
libraryViewModel.onEvent(LibraryEvent.OnLoadList)
|
||||
}
|
||||
))
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.cover_image_deleted),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
package com.acclorite.books_history.presentation.screens.book_info.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun BookInfoChangeCoverBottomSheetItem(
|
||||
icon: ImageVector,
|
||||
text: String,
|
||||
description: String,
|
||||
verticalPadding: Dp = 12.dp,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
onClick()
|
||||
}
|
||||
.padding(vertical = verticalPadding, horizontal = 18.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
description,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
package com.acclorite.books_history.presentation.screens.book_info.components
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.DeleteOutline
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.acclorite.books_history.R
|
||||
import com.acclorite.books_history.presentation.Navigator
|
||||
import com.acclorite.books_history.presentation.components.CustomDialogWithContent
|
||||
import com.acclorite.books_history.presentation.screens.book_info.data.BookInfoEvent
|
||||
import com.acclorite.books_history.presentation.screens.book_info.data.BookInfoViewModel
|
||||
import com.acclorite.books_history.presentation.screens.library.data.LibraryEvent
|
||||
import com.acclorite.books_history.presentation.screens.library.data.LibraryViewModel
|
||||
|
||||
@Composable
|
||||
fun BookInfoDeleteDialog(
|
||||
libraryViewModel: LibraryViewModel,
|
||||
viewModel: BookInfoViewModel,
|
||||
navigator: Navigator
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
CustomDialogWithContent(
|
||||
title = stringResource(id = R.string.delete_book),
|
||||
imageVectorIcon = Icons.Outlined.DeleteOutline,
|
||||
description = stringResource(
|
||||
id = R.string.delete_book_description
|
||||
),
|
||||
actionText = stringResource(id = R.string.delete),
|
||||
onDismiss = { viewModel.onEvent(BookInfoEvent.OnShowHideDeleteDialog) },
|
||||
withDivider = false,
|
||||
isActionEnabled = true,
|
||||
onAction = {
|
||||
viewModel.onEvent(
|
||||
BookInfoEvent.OnDeleteBook(
|
||||
navigator = navigator,
|
||||
refreshList = {
|
||||
libraryViewModel.onEvent(
|
||||
LibraryEvent.OnLoadList
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.book_deleted),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.acclorite.books_history.presentation.screens.book_info.components
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.acclorite.books_history.R
|
||||
import com.acclorite.books_history.domain.model.Book
|
||||
|
||||
@Composable
|
||||
fun BookInfoDescriptionSection(book: Book) {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
book.description
|
||||
?: stringResource(id = R.string.error_no_description),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Start
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(96.dp))
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
package com.acclorite.books_history.presentation.screens.book_info.components
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.acclorite.books_history.R
|
||||
import com.acclorite.books_history.presentation.screens.book_info.data.BookInfoEvent
|
||||
import com.acclorite.books_history.presentation.screens.book_info.data.BookInfoViewModel
|
||||
import com.acclorite.books_history.ui.ElevationDefaults
|
||||
import com.acclorite.books_history.ui.elevation
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BookInfoDetailsBottomSheet(
|
||||
viewModel: BookInfoViewModel
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val context = LocalContext.current
|
||||
|
||||
val pattern = SimpleDateFormat("HH:mm dd MMM", Locale.getDefault())
|
||||
val lastOpened = pattern.format(Date(state.book.lastOpened ?: 0))
|
||||
|
||||
val sizeBytes = state.book.file?.length() ?: 0
|
||||
|
||||
val fileSizeKB = if (sizeBytes > 0) sizeBytes.toDouble() / 1024.0 else 0.0
|
||||
val fileSizeMB = if (sizeBytes > 0) fileSizeKB / 1024.0 else 0.0
|
||||
|
||||
val fileSize =
|
||||
if (fileSizeMB >= 1.0) "%.2f MB".format(fileSizeMB)
|
||||
else if (fileSizeMB > 0.0) "%.2f KB".format(fileSizeKB)
|
||||
else ""
|
||||
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = {
|
||||
viewModel.onEvent(BookInfoEvent.OnShowHideDetailsBottomSheet)
|
||||
},
|
||||
windowInsets = WindowInsets(0, 0, 0, 0),
|
||||
containerColor = MaterialTheme.elevation(ElevationDefaults.BottomSheet)
|
||||
) {
|
||||
BookInfoDetailsBottomSheetItem(
|
||||
title = stringResource(id = R.string.file_name),
|
||||
description = state.book.filePath.substringAfterLast("/").trim()
|
||||
) {
|
||||
viewModel.onEvent(BookInfoEvent.OnCopyToClipboard(
|
||||
context,
|
||||
state.book.filePath.substringAfterLast("/").trim(),
|
||||
success = {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.copied),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
))
|
||||
}
|
||||
BookInfoDetailsBottomSheetItem(
|
||||
title = stringResource(id = R.string.file_path),
|
||||
description = state.book.filePath.trim()
|
||||
) {
|
||||
viewModel.onEvent(BookInfoEvent.OnCopyToClipboard(
|
||||
context,
|
||||
state.book.filePath.trim(),
|
||||
success = {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.copied),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
))
|
||||
}
|
||||
BookInfoDetailsBottomSheetItem(
|
||||
title = stringResource(id = R.string.file_last_opened),
|
||||
description = if (state.book.lastOpened != null) lastOpened
|
||||
else stringResource(id = R.string.never)
|
||||
) {
|
||||
if (state.book.lastOpened != null) {
|
||||
viewModel.onEvent(BookInfoEvent.OnCopyToClipboard(
|
||||
context,
|
||||
lastOpened,
|
||||
success = {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.copied),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
BookInfoDetailsBottomSheetItem(
|
||||
title = stringResource(id = R.string.file_size),
|
||||
description = fileSize.ifBlank { stringResource(id = R.string.unknown) }
|
||||
) {
|
||||
if (fileSize.isNotBlank()) {
|
||||
viewModel.onEvent(BookInfoEvent.OnCopyToClipboard(
|
||||
context,
|
||||
fileSize,
|
||||
success = {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.copied),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package com.acclorite.books_history.presentation.screens.book_info.components
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun BookInfoDetailsBottomSheetItem(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
description: String,
|
||||
verticalPadding: Dp = 12.dp,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
|
||||
},
|
||||
onLongClick = {
|
||||
onClick()
|
||||
}
|
||||
)
|
||||
.padding(horizontal = 16.dp, vertical = verticalPadding),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.Start
|
||||
) {
|
||||
Text(
|
||||
title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Text(
|
||||
description,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
package com.acclorite.books_history.presentation.screens.book_info.components
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Image
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.acclorite.books_history.domain.model.Book
|
||||
import com.acclorite.books_history.presentation.screens.book_info.data.BookInfoEvent
|
||||
import com.acclorite.books_history.presentation.screens.book_info.data.BookInfoViewModel
|
||||
import com.acclorite.books_history.ui.elevation
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun BookInfoInfoSection(viewModel: BookInfoViewModel, book: Book) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically, modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.height(195.dp)
|
||||
.width(130.dp)
|
||||
.clip(MaterialTheme.shapes.large)
|
||||
.background(MaterialTheme.elevation())
|
||||
.combinedClickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = {},
|
||||
onLongClick = {
|
||||
viewModel.onEvent(BookInfoEvent.OnShowHideChangeCoverBottomSheet)
|
||||
}
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Image,
|
||||
contentDescription = "Cover Image not found",
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.fillMaxWidth(0.7f)
|
||||
.aspectRatio(1f),
|
||||
tint = MaterialTheme.elevation(12.dp)
|
||||
)
|
||||
|
||||
if (book.coverImage != null) {
|
||||
Image(
|
||||
bitmap = book.coverImage.asImageBitmap(),
|
||||
contentDescription = "Cover",
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(MaterialTheme.shapes.large),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Column(verticalArrangement = Arrangement.Center) {
|
||||
if (!state.editTitle) {
|
||||
Text(
|
||||
book.title,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = {},
|
||||
onLongClick = {
|
||||
viewModel.onEvent(BookInfoEvent.OnShowHideEditTitle)
|
||||
}
|
||||
),
|
||||
maxLines = 4,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
} else {
|
||||
BasicTextField(
|
||||
value = state.titleValue,
|
||||
singleLine = true,
|
||||
textStyle = TextStyle(
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontSize = MaterialTheme.typography.headlineSmall.fontSize,
|
||||
lineHeight = MaterialTheme.typography.headlineSmall.lineHeight,
|
||||
fontFamily = MaterialTheme.typography.headlineSmall.fontFamily
|
||||
),
|
||||
onValueChange = {
|
||||
if (it.length < 80) {
|
||||
viewModel.onEvent(BookInfoEvent.OnTitleValueChange(it))
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.onGloballyPositioned {
|
||||
viewModel.onEvent(BookInfoEvent.OnRequestFocus(focusRequester))
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(KeyboardCapitalization.Words),
|
||||
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
) { innerText ->
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
innerText()
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
SelectionContainer {
|
||||
Text(
|
||||
book.author.asString(),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package com.acclorite.books_history.presentation.screens.book_info.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.DriveFileMove
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.acclorite.books_history.R
|
||||
import com.acclorite.books_history.presentation.components.CustomDropDownMenuItem
|
||||
import com.acclorite.books_history.presentation.screens.book_info.data.BookInfoEvent
|
||||
import com.acclorite.books_history.presentation.screens.book_info.data.BookInfoViewModel
|
||||
|
||||
@Composable
|
||||
fun BookInfoMoreDropDown(viewModel: BookInfoViewModel) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
Box {
|
||||
IconButton(onClick = {
|
||||
viewModel.onEvent(BookInfoEvent.OnShowHideMoreDropDown)
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "Show drop down",
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = state.showMoreDropDown,
|
||||
onDismissRequest = {
|
||||
viewModel.onEvent(BookInfoEvent.OnShowHideMoreDropDown)
|
||||
},
|
||||
offset = DpOffset(10.dp, 0.dp)
|
||||
) {
|
||||
CustomDropDownMenuItem(
|
||||
leadingIcon = Icons.AutoMirrored.Outlined.DriveFileMove,
|
||||
text = stringResource(id = R.string.move_this_book)
|
||||
) {
|
||||
viewModel.onEvent(BookInfoEvent.OnShowHideMoveDialog)
|
||||
viewModel.onEvent(BookInfoEvent.OnShowHideMoreDropDown)
|
||||
}
|
||||
CustomDropDownMenuItem(
|
||||
leadingIcon = Icons.Outlined.Delete,
|
||||
text = stringResource(id = R.string.delete_this_book)
|
||||
) {
|
||||
viewModel.onEvent(BookInfoEvent.OnShowHideDeleteDialog)
|
||||
viewModel.onEvent(BookInfoEvent.OnShowHideMoreDropDown)
|
||||
}
|
||||
CustomDropDownMenuItem(
|
||||
leadingIcon = Icons.Outlined.Info,
|
||||
text = stringResource(id = R.string.details_book)
|
||||
) {
|
||||
viewModel.onEvent(BookInfoEvent.OnShowHideDetailsBottomSheet)
|
||||
viewModel.onEvent(BookInfoEvent.OnShowHideMoreDropDown)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package com.acclorite.books_history.presentation.screens.book_info.components
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.DriveFileMove
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.acclorite.books_history.R
|
||||
import com.acclorite.books_history.domain.model.Category
|
||||
import com.acclorite.books_history.presentation.Navigator
|
||||
import com.acclorite.books_history.presentation.components.CustomDialogWithLazyColumn
|
||||
import com.acclorite.books_history.presentation.components.SelectableDialogItem
|
||||
import com.acclorite.books_history.presentation.screens.book_info.data.BookInfoEvent
|
||||
import com.acclorite.books_history.presentation.screens.book_info.data.BookInfoViewModel
|
||||
import com.acclorite.books_history.presentation.screens.library.data.LibraryEvent
|
||||
import com.acclorite.books_history.presentation.screens.library.data.LibraryViewModel
|
||||
|
||||
@Composable
|
||||
fun BookInfoMoveDialog(
|
||||
libraryViewModel: LibraryViewModel,
|
||||
viewModel: BookInfoViewModel,
|
||||
navigator: Navigator
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val context = LocalContext.current
|
||||
|
||||
val categories = remember {
|
||||
Category.entries.filter { state.book.category != it }
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.onEvent(BookInfoEvent.OnSelectCategory(categories[0]))
|
||||
}
|
||||
|
||||
CustomDialogWithLazyColumn(
|
||||
title = stringResource(id = R.string.move_book),
|
||||
imageVectorIcon = Icons.AutoMirrored.Outlined.DriveFileMove,
|
||||
description = stringResource(
|
||||
id = R.string.move_book_description
|
||||
),
|
||||
actionText = stringResource(id = R.string.move),
|
||||
isActionEnabled = true,
|
||||
onDismiss = { viewModel.onEvent(BookInfoEvent.OnShowHideMoveDialog) },
|
||||
onAction = {
|
||||
viewModel.onEvent(
|
||||
BookInfoEvent.OnMoveBook(
|
||||
refreshList = {
|
||||
libraryViewModel.onEvent(
|
||||
LibraryEvent.OnLoadList
|
||||
)
|
||||
},
|
||||
updatePage = {
|
||||
libraryViewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(it))
|
||||
},
|
||||
navigator = navigator
|
||||
)
|
||||
)
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.book_moved),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
},
|
||||
withDivider = false,
|
||||
items = {
|
||||
items(categories) {
|
||||
val category = when (it) {
|
||||
Category.READING -> stringResource(id = R.string.reading_tab)
|
||||
Category.ALREADY_READ -> stringResource(id = R.string.already_read_tab)
|
||||
Category.PLANNING -> stringResource(id = R.string.planning_tab)
|
||||
Category.DROPPED -> stringResource(id = R.string.dropped_tab)
|
||||
}
|
||||
|
||||
SelectableDialogItem(selected = it == state.selectedCategory, title = category) {
|
||||
viewModel.onEvent(BookInfoEvent.OnSelectCategory(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package com.acclorite.books_history.presentation.screens.book_info.components
|
||||
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.acclorite.books_history.R
|
||||
import com.acclorite.books_history.domain.model.Book
|
||||
import com.acclorite.books_history.util.removeDigits
|
||||
import com.acclorite.books_history.util.removeTrailingZero
|
||||
|
||||
@Composable
|
||||
fun BookInfoStatisticSection(book: Book) {
|
||||
Text(
|
||||
text = "${
|
||||
(book.progress * 100)
|
||||
.toDouble()
|
||||
.removeDigits(1)
|
||||
.removeTrailingZero()
|
||||
}%",
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
|
||||
LinearProgressIndicator(
|
||||
book.progress.coerceAtLeast(0.01f),
|
||||
strokeCap = StrokeCap.Round,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
trackColor = MaterialTheme.colorScheme.onPrimaryContainer.copy(0.05f)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Text(
|
||||
text = stringResource(
|
||||
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
|
||||
),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.acclorite.books_history.presentation.screens.book_info.data
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import com.acclorite.books_history.domain.model.Category
|
||||
import com.acclorite.books_history.presentation.Navigator
|
||||
|
||||
sealed class BookInfoEvent {
|
||||
data object OnShowHideChangeCoverBottomSheet : BookInfoEvent()
|
||||
data object OnShowHideDetailsBottomSheet : BookInfoEvent()
|
||||
data class OnDeleteCover(val refreshList: () -> Unit) : BookInfoEvent()
|
||||
data class OnChangeCover(val uri: Uri, val context: Context, val refreshList: () -> Unit) :
|
||||
BookInfoEvent()
|
||||
|
||||
data object OnShowHideEditTitle : BookInfoEvent()
|
||||
data class OnRequestFocus(val focusRequester: FocusRequester) : BookInfoEvent()
|
||||
data class OnTitleValueChange(val value: String) : BookInfoEvent()
|
||||
data class OnUpdateTitle(val refreshList: () -> Unit) : BookInfoEvent()
|
||||
data object OnShowHideMoreDropDown : BookInfoEvent()
|
||||
data object OnShowHideDeleteDialog : BookInfoEvent()
|
||||
data object OnShowHideMoveDialog : BookInfoEvent()
|
||||
data class OnDeleteBook(val refreshList: () -> Unit, val navigator: Navigator) : BookInfoEvent()
|
||||
data class OnMoveBook(
|
||||
val refreshList: () -> Unit,
|
||||
val updatePage: (Int) -> Unit,
|
||||
val navigator: Navigator
|
||||
) : BookInfoEvent()
|
||||
|
||||
data class OnSelectCategory(val category: Category) : BookInfoEvent()
|
||||
data class OnCopyToClipboard(
|
||||
val context: Context,
|
||||
val text: String,
|
||||
val success: () -> Unit
|
||||
) : BookInfoEvent()
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.acclorite.books_history.presentation.screens.book_info.data
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.acclorite.books_history.domain.model.Book
|
||||
import com.acclorite.books_history.domain.model.Category
|
||||
|
||||
@Immutable
|
||||
data class BookInfoState(
|
||||
val book: Book,
|
||||
|
||||
val editTitle: Boolean = false,
|
||||
val hasFocused: Boolean = false,
|
||||
val titleValue: String = "",
|
||||
|
||||
val showChangeCoverBottomSheet: Boolean = false,
|
||||
val showDetailsBottomSheet: Boolean = false,
|
||||
|
||||
val showMoreDropDown: Boolean = false,
|
||||
val showDeleteDialog: Boolean = false,
|
||||
|
||||
val showMoveDialog: Boolean = false,
|
||||
val selectedCategory: Category = Category.READING,
|
||||
)
|
||||
|
|
@ -0,0 +1,270 @@
|
|||
package com.acclorite.books_history.presentation.screens.book_info.data
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context.CLIPBOARD_SERVICE
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.acclorite.books_history.domain.model.Book
|
||||
import com.acclorite.books_history.domain.model.Category
|
||||
import com.acclorite.books_history.domain.use_case.DeleteBooks
|
||||
import com.acclorite.books_history.domain.use_case.UpdateBooks
|
||||
import com.acclorite.books_history.presentation.Navigator
|
||||
import com.acclorite.books_history.presentation.Screen
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
|
||||
@HiltViewModel(assistedFactory = BookInfoViewModel.Factory::class)
|
||||
class BookInfoViewModel @AssistedInject constructor(
|
||||
@Assisted book: Book,
|
||||
private val updateBooks: UpdateBooks,
|
||||
private val deleteBooks: DeleteBooks
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow(BookInfoState(book))
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
fun onEvent(event: BookInfoEvent) {
|
||||
when (event) {
|
||||
is BookInfoEvent.OnShowHideChangeCoverBottomSheet -> {
|
||||
_state.update {
|
||||
it.copy(
|
||||
showChangeCoverBottomSheet = !it.showChangeCoverBottomSheet
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is BookInfoEvent.OnChangeCover -> {
|
||||
viewModelScope.launch {
|
||||
val image = event.context.contentResolver?.openInputStream(event.uri)?.use {
|
||||
BitmapFactory.decodeStream(it)
|
||||
} ?: return@launch
|
||||
|
||||
updateBooks.execute(
|
||||
listOf(
|
||||
_state.value.book.copy(coverImage = image)
|
||||
)
|
||||
)
|
||||
event.refreshList()
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
book = it.book.copy(
|
||||
coverImage = image
|
||||
),
|
||||
showChangeCoverBottomSheet = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is BookInfoEvent.OnDeleteCover -> {
|
||||
viewModelScope.launch {
|
||||
if (_state.value.book.coverImage == null) {
|
||||
return@launch
|
||||
}
|
||||
|
||||
updateBooks.execute(
|
||||
listOf(
|
||||
_state.value.book.copy(coverImage = null)
|
||||
)
|
||||
)
|
||||
event.refreshList()
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
book = it.book.copy(
|
||||
coverImage = null
|
||||
),
|
||||
showChangeCoverBottomSheet = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is BookInfoEvent.OnShowHideEditTitle -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val shouldHide = _state.value.editTitle
|
||||
|
||||
if (!shouldHide) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
titleValue = it.book.title,
|
||||
hasFocused = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
editTitle = !shouldHide
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is BookInfoEvent.OnRequestFocus -> {
|
||||
if (!_state.value.hasFocused) {
|
||||
event.focusRequester.requestFocus()
|
||||
_state.update {
|
||||
it.copy(
|
||||
hasFocused = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is BookInfoEvent.OnTitleValueChange -> {
|
||||
_state.update {
|
||||
it.copy(
|
||||
titleValue = event.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is BookInfoEvent.OnUpdateTitle -> {
|
||||
viewModelScope.launch {
|
||||
updateBooks.execute(
|
||||
listOf(
|
||||
_state.value.book.copy(title = _state.value.titleValue.trim())
|
||||
)
|
||||
)
|
||||
event.refreshList()
|
||||
|
||||
_state.update {
|
||||
it.copy(
|
||||
book = it.book.copy(
|
||||
title = it.titleValue
|
||||
)
|
||||
)
|
||||
}
|
||||
onEvent(BookInfoEvent.OnShowHideEditTitle)
|
||||
}
|
||||
}
|
||||
|
||||
is BookInfoEvent.OnShowHideMoreDropDown -> {
|
||||
_state.update {
|
||||
it.copy(
|
||||
showMoreDropDown = !it.showMoreDropDown
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is BookInfoEvent.OnShowHideDeleteDialog -> {
|
||||
_state.update {
|
||||
it.copy(
|
||||
showDeleteDialog = !it.showDeleteDialog
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is BookInfoEvent.OnDeleteBook -> {
|
||||
viewModelScope.launch {
|
||||
_state.update {
|
||||
it.copy(
|
||||
showDeleteDialog = false
|
||||
)
|
||||
}
|
||||
|
||||
deleteBooks.execute(listOf(_state.value.book))
|
||||
event.refreshList()
|
||||
|
||||
event.navigator.navigateBack()
|
||||
}
|
||||
}
|
||||
|
||||
is BookInfoEvent.OnShowHideMoveDialog -> {
|
||||
_state.update {
|
||||
it.copy(
|
||||
showMoveDialog = !it.showMoveDialog
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is BookInfoEvent.OnSelectCategory -> {
|
||||
_state.update {
|
||||
it.copy(
|
||||
selectedCategory = event.category
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is BookInfoEvent.OnMoveBook -> {
|
||||
viewModelScope.launch {
|
||||
_state.update {
|
||||
it.copy(
|
||||
showMoveDialog = false
|
||||
)
|
||||
}
|
||||
|
||||
val book = _state.value.book.copy(
|
||||
category = _state.value.selectedCategory
|
||||
)
|
||||
updateBooks.execute(listOf(book))
|
||||
|
||||
event.refreshList()
|
||||
|
||||
event.updatePage(
|
||||
Category.entries.dropLastWhile {
|
||||
it != _state.value.selectedCategory
|
||||
}.size - 1
|
||||
)
|
||||
event.navigator.navigate(Screen.LIBRARY)
|
||||
}
|
||||
}
|
||||
|
||||
is BookInfoEvent.OnShowHideDetailsBottomSheet -> {
|
||||
_state.update {
|
||||
it.copy(
|
||||
showDetailsBottomSheet = !it.showDetailsBottomSheet
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is BookInfoEvent.OnCopyToClipboard -> {
|
||||
val clipboardManager =
|
||||
event.context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText("", event.text))
|
||||
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
event.success()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun init(navigator: Navigator, failure: (String) -> Unit) {
|
||||
viewModelScope.launch {
|
||||
val book = navigator.retrieveArgument("book") as? Book
|
||||
|
||||
if (book == null) {
|
||||
navigator.navigateBack()
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (book.file == null) {
|
||||
failure(book.filePath)
|
||||
}
|
||||
|
||||
_state.update {
|
||||
BookInfoState(book = book)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(book: Book): BookInfoViewModel
|
||||
}
|
||||
}
|
||||
|
|
@ -3,8 +3,6 @@ package com.acclorite.books_history.presentation.screens.browse
|
|||
import android.Manifest
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
|
|
@ -17,6 +15,7 @@ import androidx.compose.foundation.layout.size
|
|||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
|
|
@ -35,19 +34,18 @@ import androidx.compose.material3.IconButton
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
|
@ -73,12 +71,15 @@ import com.acclorite.books_history.ui.Transitions
|
|||
import com.acclorite.books_history.ui.elevation
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
|
||||
@OptIn(
|
||||
ExperimentalMaterial3Api::class,
|
||||
ExperimentalPermissionsApi::class,
|
||||
ExperimentalFoundationApi::class,
|
||||
ExperimentalMaterialApi::class
|
||||
ExperimentalMaterialApi::class, FlowPreview::class
|
||||
)
|
||||
@Composable
|
||||
fun BrowseScreen(
|
||||
|
|
@ -88,12 +89,7 @@ fun BrowseScreen(
|
|||
val permissionState = rememberPermissionState(
|
||||
permission = Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
)
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.onEvent(BrowseEvent.OnPermissionCheck(permissionState))
|
||||
}
|
||||
|
||||
val state by viewModel.state.collectAsState()
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
val refreshState = rememberPullRefreshState(
|
||||
refreshing = state.isRefreshing,
|
||||
onRefresh = {
|
||||
|
|
@ -101,15 +97,29 @@ fun BrowseScreen(
|
|||
}
|
||||
)
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val listState = rememberLazyGridState(state.scrollIndex, state.scrollOffset)
|
||||
|
||||
val topAppBarColor = if (state.hasSelectedItems) MaterialTheme.elevation()
|
||||
else MaterialTheme.colorScheme.surface
|
||||
|
||||
val animatedTopAppBarColor by animateColorAsState(
|
||||
targetValue = topAppBarColor,
|
||||
animationSpec = tween(300, easing = LinearEasing),
|
||||
label = "TopAppBar color animation"
|
||||
)
|
||||
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) {
|
||||
viewModel.onEvent(BrowseEvent.OnPermissionCheck(permissionState))
|
||||
}
|
||||
|
||||
if (state.requestPermissionDialog) {
|
||||
BrowseStoragePermissionDialog(viewModel, permissionState)
|
||||
|
|
@ -121,14 +131,15 @@ fun BrowseScreen(
|
|||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.pullRefresh(refreshState)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
.pullRefresh(refreshState),
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
topBar = {
|
||||
AnimatedTopAppBar(
|
||||
containerColor = animatedTopAppBarColor,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
scrolledContainerColor = MaterialTheme.elevation(),
|
||||
scrollBehavior = scrollBehavior,
|
||||
|
||||
scrollBehavior = null,
|
||||
isTopBarScrolled = state.scrollIndex > 0 || state.scrollOffset > 0 || state.hasSelectedItems,
|
||||
|
||||
content1Visibility = !state.hasSelectedItems && !state.showSearch,
|
||||
content1NavigationIcon = {},
|
||||
|
|
@ -251,16 +262,18 @@ fun BrowseScreen(
|
|||
.padding(top = padding.calculateTopPadding())
|
||||
) {
|
||||
LazyVerticalGrid(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
columns = GridCells.Adaptive(170.dp),
|
||||
contentPadding = PaddingValues(10.dp)
|
||||
) {
|
||||
items(state.selectableFiles) { selectableFile ->
|
||||
items(state.selectableFiles, key = { it.first.path }) { selectableFile ->
|
||||
BrowseFileItem(
|
||||
file = selectableFile,
|
||||
modifier = Modifier
|
||||
.animateItemPlacement(
|
||||
animationSpec = tween(500)
|
||||
animationSpec = tween(300)
|
||||
),
|
||||
onClick = {
|
||||
viewModel.onEvent(BrowseEvent.OnSelectFile(selectableFile))
|
||||
|
|
|
|||
|
|
@ -19,14 +19,21 @@ import androidx.compose.ui.graphics.StrokeCap
|
|||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.acclorite.books_history.R
|
||||
import com.acclorite.books_history.presentation.Navigator
|
||||
import com.acclorite.books_history.presentation.components.CustomDialogWithLazyColumn
|
||||
import com.acclorite.books_history.presentation.screens.browse.data.BrowseEvent
|
||||
import com.acclorite.books_history.presentation.screens.browse.data.BrowseViewModel
|
||||
import com.acclorite.books_history.presentation.screens.library.data.LibraryEvent
|
||||
import com.acclorite.books_history.presentation.screens.library.data.LibraryViewModel
|
||||
|
||||
@Composable
|
||||
fun BrowseAddingDialog(viewModel: BrowseViewModel, navigator: Navigator) {
|
||||
fun BrowseAddingDialog(
|
||||
libraryViewModel: LibraryViewModel = hiltViewModel(),
|
||||
viewModel: BrowseViewModel,
|
||||
navigator: Navigator
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val context = LocalContext.current
|
||||
|
||||
|
|
@ -38,7 +45,14 @@ fun BrowseAddingDialog(viewModel: BrowseViewModel, navigator: Navigator) {
|
|||
isActionEnabled = !state.isBooksLoading,
|
||||
onDismiss = { viewModel.onEvent(BrowseEvent.OnAddingDialogDismiss) },
|
||||
onAction = {
|
||||
viewModel.onEvent(BrowseEvent.OnAddBooks(navigator))
|
||||
viewModel.onEvent(
|
||||
BrowseEvent.OnAddBooks(
|
||||
navigator,
|
||||
resetScroll = {
|
||||
libraryViewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(0))
|
||||
}
|
||||
)
|
||||
)
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.books_added),
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.acclorite.books_history.R
|
||||
import com.acclorite.books_history.presentation.components.CustomAlertDialog
|
||||
import com.acclorite.books_history.presentation.components.CustomDialogWithContent
|
||||
import com.acclorite.books_history.presentation.screens.browse.data.BrowseEvent
|
||||
import com.acclorite.books_history.presentation.screens.browse.data.BrowseViewModel
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
|
|
@ -19,12 +19,14 @@ import com.google.accompanist.permissions.PermissionState
|
|||
fun BrowseStoragePermissionDialog(viewModel: BrowseViewModel, permissionState: PermissionState) {
|
||||
val activity = LocalContext.current as ComponentActivity
|
||||
|
||||
CustomAlertDialog(
|
||||
CustomDialogWithContent(
|
||||
title = stringResource(id = R.string.storage_permission),
|
||||
description = stringResource(id = R.string.storage_permission_description),
|
||||
actionText = stringResource(id = R.string.grant),
|
||||
imageVectorIcon = Icons.Default.SdStorage,
|
||||
onDismiss = { viewModel.onEvent(BrowseEvent.OnStoragePermissionDismiss(permissionState)) },
|
||||
isActionEnabled = true,
|
||||
withDivider = false,
|
||||
onAction = {
|
||||
viewModel.onEvent(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ sealed class BrowseEvent() {
|
|||
data object OnAddingDialogRequest : BrowseEvent()
|
||||
data object OnAddingDialogDismiss : BrowseEvent()
|
||||
data object OnGetBooksFromFiles : BrowseEvent()
|
||||
data class OnAddBooks(val navigator: Navigator) : BrowseEvent()
|
||||
data class OnAddBooks(val navigator: Navigator, val resetScroll: () -> Unit) : BrowseEvent()
|
||||
data object OnLoadList : BrowseEvent()
|
||||
data class OnUpdateScrollIndex(val index: Int) : BrowseEvent()
|
||||
data class OnUpdateScrollOffset(val offset: Int) : BrowseEvent()
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -15,6 +15,9 @@ data class BrowseState(
|
|||
val selectedItemsCount: Int = 0,
|
||||
val hasSelectedItems: Boolean = false,
|
||||
|
||||
val scrollIndex: Int = 0,
|
||||
val scrollOffset: Int = 0,
|
||||
|
||||
val showSearch: Boolean = false,
|
||||
val searchQuery: String = "",
|
||||
val hasFocused: Boolean = false,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import com.acclorite.books_history.presentation.Argument
|
|||
import com.acclorite.books_history.presentation.Screen
|
||||
import com.acclorite.books_history.util.Resource
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.PermissionState
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
|
@ -52,11 +51,6 @@ class BrowseViewModel @Inject constructor(
|
|||
event.permissionState.launchPermissionRequest()
|
||||
|
||||
viewModelScope.launch {
|
||||
_state.update {
|
||||
it.copy(
|
||||
showErrorMessage = true
|
||||
)
|
||||
}
|
||||
for (i in 0 until 100) {
|
||||
if (!event.permissionState.status.isGranted) {
|
||||
delay(100)
|
||||
|
|
@ -147,7 +141,20 @@ class BrowseViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
is BrowseEvent.OnPermissionCheck -> {
|
||||
checkPermission(event.permissionState)
|
||||
val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
||||
val isPermissionGranted =
|
||||
if (!legacyPermission) Environment.isExternalStorageManager()
|
||||
else event.permissionState.status.isGranted
|
||||
|
||||
if (isPermissionGranted) {
|
||||
return
|
||||
}
|
||||
_state.update {
|
||||
it.copy(
|
||||
requestPermissionDialog = true,
|
||||
showErrorMessage = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is BrowseEvent.OnSelectFile -> {
|
||||
|
|
@ -299,6 +306,7 @@ class BrowseViewModel @Inject constructor(
|
|||
viewModelScope.launch {
|
||||
val booksToInsert = _state.value.selectedBooks
|
||||
.filterIsInstance<NullableBook.NotNull>()
|
||||
.filter { it.book!!.second }
|
||||
.map { it.book!!.first }
|
||||
|
||||
if (booksToInsert.isEmpty()) {
|
||||
|
|
@ -308,8 +316,7 @@ class BrowseViewModel @Inject constructor(
|
|||
insertBooks.execute(booksToInsert)
|
||||
val books = fastGetBooks.execute("")
|
||||
|
||||
|
||||
|
||||
event.resetScroll()
|
||||
_state.update {
|
||||
it.copy(
|
||||
showAddingDialog = false
|
||||
|
|
@ -331,25 +338,25 @@ class BrowseViewModel @Inject constructor(
|
|||
getFilesFromDownloads("")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkPermission(permissionState: PermissionState) {
|
||||
val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
||||
val isPermissionGranted = if (!legacyPermission) Environment.isExternalStorageManager()
|
||||
else permissionState.status.isGranted
|
||||
|
||||
if (isPermissionGranted) {
|
||||
return
|
||||
}
|
||||
is BrowseEvent.OnUpdateScrollIndex -> {
|
||||
_state.update {
|
||||
it.copy(
|
||||
requestPermissionDialog = true,
|
||||
showErrorMessage = false
|
||||
scrollIndex = event.index
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is BrowseEvent.OnUpdateScrollOffset -> {
|
||||
_state.update {
|
||||
it.copy(
|
||||
scrollOffset = event.offset
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getFilesFromDownloads(query: String = _state.value.searchQuery) {
|
||||
getFilesFromDownloads.execute(query).collect { result ->
|
||||
when (result) {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ import android.widget.Toast
|
|||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
|
|
@ -75,24 +73,27 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
|||
import com.acclorite.books_history.R
|
||||
import com.acclorite.books_history.domain.model.Book
|
||||
import com.acclorite.books_history.domain.model.Category
|
||||
import com.acclorite.books_history.presentation.Argument
|
||||
import com.acclorite.books_history.presentation.Navigator
|
||||
import com.acclorite.books_history.presentation.Screen
|
||||
import com.acclorite.books_history.presentation.components.AnimatedTopAppBar
|
||||
import com.acclorite.books_history.presentation.components.IsEmpty
|
||||
import com.acclorite.books_history.presentation.components.MoreDropDown
|
||||
import com.acclorite.books_history.presentation.screens.library.components.LibraryBookItem
|
||||
import com.acclorite.books_history.presentation.screens.library.components.LibraryDeleteDialog
|
||||
import com.acclorite.books_history.presentation.screens.library.components.LibraryMoveDialog
|
||||
import com.acclorite.books_history.presentation.screens.library.components.LibraryTabRow
|
||||
import com.acclorite.books_history.presentation.screens.library.data.LibraryEvent
|
||||
import com.acclorite.books_history.presentation.screens.library.data.LibraryViewModel
|
||||
import com.acclorite.books_history.ui.Transitions
|
||||
import com.acclorite.books_history.ui.elevation
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(
|
||||
ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class,
|
||||
ExperimentalMaterialApi::class
|
||||
ExperimentalMaterialApi::class, FlowPreview::class
|
||||
)
|
||||
@Composable
|
||||
fun LibraryScreen(
|
||||
|
|
@ -124,18 +125,12 @@ fun LibraryScreen(
|
|||
viewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(pagerState.currentPage))
|
||||
}
|
||||
|
||||
val topAppBarColor = if (state.hasSelectedItems) MaterialTheme.elevation()
|
||||
else MaterialTheme.colorScheme.surface
|
||||
|
||||
val animatedTopAppBarColor by animateColorAsState(
|
||||
targetValue = topAppBarColor,
|
||||
animationSpec = tween(300, easing = LinearEasing),
|
||||
label = "TopAppBar color animation"
|
||||
)
|
||||
|
||||
if (state.showMoveDialog) {
|
||||
LibraryMoveDialog(viewModel = viewModel, pagerState = pagerState)
|
||||
}
|
||||
if (state.showDeleteDialog) {
|
||||
LibraryDeleteDialog(viewModel = viewModel)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
Modifier
|
||||
|
|
@ -145,9 +140,11 @@ fun LibraryScreen(
|
|||
topBar = {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
AnimatedTopAppBar(
|
||||
containerColor = animatedTopAppBarColor,
|
||||
scrolledContainerColor = null,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
scrolledContainerColor = MaterialTheme.elevation(),
|
||||
|
||||
scrollBehavior = null,
|
||||
isTopBarScrolled = state.hasSelectedItems,
|
||||
|
||||
content1Visibility = !state.hasSelectedItems && !state.showSearch,
|
||||
content1NavigationIcon = {},
|
||||
|
|
@ -217,7 +214,7 @@ fun LibraryScreen(
|
|||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { /* TODO Delete book */ }) {
|
||||
IconButton(onClick = { viewModel.onEvent(LibraryEvent.OnShowHideDeleteDialog) }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Delete,
|
||||
contentDescription = "Delete books from database",
|
||||
|
|
@ -317,18 +314,28 @@ fun LibraryScreen(
|
|||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Adaptive(120.dp),
|
||||
Modifier.fillMaxSize(),
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentPadding = PaddingValues(8.dp)
|
||||
) {
|
||||
if (!state.isLoading) {
|
||||
items(categorizedBooks) {
|
||||
items(
|
||||
categorizedBooks,
|
||||
key = { it.first.id ?: 0 }
|
||||
) {
|
||||
LibraryBookItem(
|
||||
book = it,
|
||||
modifier = Modifier.animateItemPlacement(
|
||||
animationSpec = tween(300)
|
||||
),
|
||||
onCoverImageClick = {
|
||||
if (state.hasSelectedItems) {
|
||||
viewModel.onEvent(LibraryEvent.OnSelectBook(it))
|
||||
} else {
|
||||
navigator.navigate(Screen.BOOKS_INFO)
|
||||
navigator.navigate(
|
||||
Screen.BOOK_INFO,
|
||||
Argument("book", it.first)
|
||||
)
|
||||
}
|
||||
},
|
||||
onLongClick = {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import com.acclorite.books_history.util.removeTrailingZero
|
|||
@Composable
|
||||
fun LibraryBookItem(
|
||||
book: Pair<Book, Boolean>,
|
||||
modifier: Modifier = Modifier,
|
||||
onCoverImageClick: () -> Unit,
|
||||
onLongClick: () -> Unit,
|
||||
onButtonClick: () -> Unit
|
||||
|
|
@ -65,7 +66,7 @@ fun LibraryBookItem(
|
|||
)
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
modifier
|
||||
.padding(3.dp)
|
||||
.background(
|
||||
animatedBackgroundColor,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
package com.acclorite.books_history.presentation.screens.library.components
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.DeleteOutline
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.acclorite.books_history.R
|
||||
import com.acclorite.books_history.presentation.components.CustomDialogWithContent
|
||||
import com.acclorite.books_history.presentation.screens.library.data.LibraryEvent
|
||||
import com.acclorite.books_history.presentation.screens.library.data.LibraryViewModel
|
||||
|
||||
@Composable
|
||||
fun LibraryDeleteDialog(viewModel: LibraryViewModel) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val context = LocalContext.current
|
||||
|
||||
CustomDialogWithContent(
|
||||
title = stringResource(id = R.string.delete_books),
|
||||
imageVectorIcon = Icons.Outlined.DeleteOutline,
|
||||
description = stringResource(
|
||||
id = R.string.delete_books_description,
|
||||
state.books.filter { it.second }.size
|
||||
),
|
||||
actionText = stringResource(id = R.string.delete),
|
||||
onDismiss = { viewModel.onEvent(LibraryEvent.OnShowHideDeleteDialog) },
|
||||
withDivider = false,
|
||||
isActionEnabled = true,
|
||||
onAction = {
|
||||
viewModel.onEvent(LibraryEvent.OnDeleteBooks)
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.books_deleted),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -4,6 +4,8 @@ import android.widget.Toast
|
|||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.DriveFileMove
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -24,7 +26,7 @@ fun LibraryMoveDialog(viewModel: LibraryViewModel, pagerState: PagerState) {
|
|||
|
||||
CustomDialogWithLazyColumn(
|
||||
title = stringResource(id = R.string.move_books),
|
||||
imageVectorIcon = null,
|
||||
imageVectorIcon = Icons.AutoMirrored.Outlined.DriveFileMove,
|
||||
description = stringResource(
|
||||
id = R.string.move_books_description,
|
||||
state.books.filter { it.second }.size
|
||||
|
|
@ -42,10 +44,6 @@ fun LibraryMoveDialog(viewModel: LibraryViewModel, pagerState: PagerState) {
|
|||
},
|
||||
withDivider = false,
|
||||
items = {
|
||||
// item {
|
||||
// Spacer(modifier = Modifier.height(12.dp))
|
||||
// }
|
||||
|
||||
items(state.categories) {
|
||||
val category = when (it) {
|
||||
Category.READING -> stringResource(id = R.string.reading_tab)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.acclorite.books_history.domain.model.Category
|
|||
sealed class LibraryEvent {
|
||||
data class OnPreloadBooks(val books: List<Book>) : LibraryEvent()
|
||||
data object OnRefreshList : LibraryEvent()
|
||||
data object OnLoadList : LibraryEvent()
|
||||
data class OnScrollToPage(val index: Int, val pagerState: PagerState) : LibraryEvent()
|
||||
data class OnUpdateCurrentPage(val page: Int) : LibraryEvent()
|
||||
data object OnSearchShowHide : LibraryEvent()
|
||||
|
|
@ -23,6 +24,6 @@ sealed class LibraryEvent {
|
|||
data object OnShowHideMoveDialog : LibraryEvent()
|
||||
data class OnSelectCategory(val category: Category) : LibraryEvent()
|
||||
data class OnMoveBooks(val pagerState: PagerState) : LibraryEvent()
|
||||
|
||||
|
||||
data object OnShowHideDeleteDialog : LibraryEvent()
|
||||
data object OnDeleteBooks : LibraryEvent()
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.acclorite.books_history.domain.model.Category
|
||||
import com.acclorite.books_history.domain.use_case.DeleteBooks
|
||||
import com.acclorite.books_history.domain.use_case.GetBooks
|
||||
import com.acclorite.books_history.domain.use_case.UpdateBooks
|
||||
import com.acclorite.books_history.util.Resource
|
||||
|
|
@ -21,7 +22,8 @@ import javax.inject.Inject
|
|||
@HiltViewModel
|
||||
class LibraryViewModel @Inject constructor(
|
||||
private val getBooks: GetBooks,
|
||||
private val updateBooks: UpdateBooks
|
||||
private val updateBooks: UpdateBooks,
|
||||
private val deleteBooks: DeleteBooks
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow(LibraryState())
|
||||
|
|
@ -86,6 +88,17 @@ class LibraryViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
is LibraryEvent.OnLoadList -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
_state.update {
|
||||
it.copy(
|
||||
isLoading = true
|
||||
)
|
||||
}
|
||||
getBooksFromDatabase()
|
||||
}
|
||||
}
|
||||
|
||||
is LibraryEvent.OnSearchShowHide -> {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val shouldHide = _state.value.showSearch
|
||||
|
|
@ -217,6 +230,33 @@ class LibraryViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
is LibraryEvent.OnShowHideDeleteDialog -> {
|
||||
_state.update {
|
||||
it.copy(
|
||||
showDeleteDialog = !it.showDeleteDialog
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is LibraryEvent.OnDeleteBooks -> {
|
||||
viewModelScope.launch {
|
||||
_state.update {
|
||||
it.copy(
|
||||
showDeleteDialog = false
|
||||
)
|
||||
}
|
||||
|
||||
val books = _state.value.books.filter {
|
||||
it.second
|
||||
}.map { it.first }
|
||||
deleteBooks.execute(books)
|
||||
|
||||
getBooksFromDatabase()
|
||||
|
||||
onEvent(LibraryEvent.OnClearSelectedBooks)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ fun SettingsCategoryItem(
|
|||
.clickable {
|
||||
onClick()
|
||||
}
|
||||
.padding(vertical = verticalPadding, horizontal = 16.dp),
|
||||
.padding(vertical = verticalPadding, horizontal = 18.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import com.acclorite.books_history.presentation.main.MainEvent
|
|||
import com.acclorite.books_history.presentation.main.MainViewModel
|
||||
import com.acclorite.books_history.ui.Theme
|
||||
import com.acclorite.books_history.ui.isDark
|
||||
import com.acclorite.books_history.util.Constants
|
||||
|
||||
@Composable
|
||||
fun AppearanceSettingsThemeSwitcher(
|
||||
|
|
@ -30,8 +31,8 @@ fun AppearanceSettingsThemeSwitcher(
|
|||
val theme = mainViewModel.theme.collectAsState().value!!
|
||||
val darkTheme = mainViewModel.darkTheme.collectAsState().value!!
|
||||
|
||||
val themes = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Theme.entries
|
||||
else Theme.entries.dropWhile { it == Theme.DYNAMIC }
|
||||
val themes = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Constants.THEMES
|
||||
else Constants.THEMES.dropWhile { it.first == Theme.DYNAMIC }
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
|
|
@ -56,9 +57,9 @@ fun AppearanceSettingsThemeSwitcher(
|
|||
AppearanceSettingsThemeSwitcherItem(
|
||||
theme = themeEntry,
|
||||
darkTheme = darkTheme.isDark(),
|
||||
selected = theme == themeEntry
|
||||
selected = theme == themeEntry.first
|
||||
) {
|
||||
mainViewModel.onEvent(MainEvent.OnChangeTheme(themeEntry.toString()))
|
||||
mainViewModel.onEvent(MainEvent.OnChangeTheme(themeEntry.first.toString()))
|
||||
}
|
||||
|
||||
if (index != Theme.entries.lastIndex) {
|
||||
|
|
|
|||
|
|
@ -27,21 +27,20 @@ import androidx.compose.ui.Alignment
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.acclorite.books_history.R
|
||||
import com.acclorite.books_history.ui.Theme
|
||||
import com.acclorite.books_history.ui.colorScheme
|
||||
import com.acclorite.books_history.util.UIText
|
||||
|
||||
@Composable
|
||||
fun AppearanceSettingsThemeSwitcherItem(
|
||||
theme: Theme,
|
||||
theme: Pair<Theme, UIText>,
|
||||
darkTheme: Boolean,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val colorScheme = colorScheme(theme, darkTheme)
|
||||
val colorScheme = colorScheme(theme.first, darkTheme)
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
|
@ -133,13 +132,7 @@ fun AppearanceSettingsThemeSwitcherItem(
|
|||
}
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Text(
|
||||
text = when (theme) {
|
||||
Theme.BLUE -> stringResource(id = R.string.blue_theme)
|
||||
Theme.PURPLE -> stringResource(id = R.string.purple_theme)
|
||||
Theme.PINK -> stringResource(id = R.string.pink_theme)
|
||||
Theme.GREEN -> stringResource(id = R.string.green_theme)
|
||||
Theme.DYNAMIC -> stringResource(id = R.string.dynamic_theme)
|
||||
},
|
||||
text = theme.second.asString(),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
|
|
|||
|
|
@ -10,13 +10,17 @@ import com.acclorite.books_history.ui.theme.blueTheme
|
|||
import com.acclorite.books_history.ui.theme.greenTheme
|
||||
import com.acclorite.books_history.ui.theme.pinkTheme
|
||||
import com.acclorite.books_history.ui.theme.purpleTheme
|
||||
import com.acclorite.books_history.ui.theme.redTheme
|
||||
import com.acclorite.books_history.ui.theme.yellowTheme
|
||||
|
||||
enum class Theme {
|
||||
DYNAMIC,
|
||||
BLUE,
|
||||
PURPLE,
|
||||
GREEN,
|
||||
PINK
|
||||
PINK,
|
||||
YELLOW,
|
||||
RED
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -28,6 +32,8 @@ fun String.toTheme(): Theme {
|
|||
"PURPLE" -> Theme.PURPLE
|
||||
"GREEN" -> Theme.GREEN
|
||||
"PINK" -> Theme.PINK
|
||||
"YELLOW" -> Theme.YELLOW
|
||||
"RED" -> Theme.RED
|
||||
else -> Theme.BLUE
|
||||
}
|
||||
}
|
||||
|
|
@ -66,9 +72,19 @@ fun colorScheme(theme: Theme, darkTheme: Boolean): ColorScheme {
|
|||
return greenTheme(isDark = darkTheme)
|
||||
}
|
||||
|
||||
else -> {
|
||||
Theme.PINK -> {
|
||||
/* Pink Theme */
|
||||
return pinkTheme(isDark = darkTheme)
|
||||
}
|
||||
|
||||
Theme.YELLOW -> {
|
||||
/* Yellow Theme */
|
||||
return yellowTheme(isDark = darkTheme)
|
||||
}
|
||||
|
||||
Theme.RED -> {
|
||||
/* Red Theme */
|
||||
return redTheme(isDark = darkTheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,5 +3,6 @@ package com.acclorite.books_history.ui
|
|||
import androidx.compose.ui.unit.dp
|
||||
|
||||
object ElevationDefaults {
|
||||
val DialogElevation = 6.dp
|
||||
val Dialog = 6.dp
|
||||
val BottomSheet = 1.dp
|
||||
}
|
||||
|
|
@ -48,7 +48,6 @@ fun BooksHistoryResurrectionTheme(
|
|||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (view.context as Activity).window
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
window.statusBarColor = Color.Transparent.toArgb()
|
||||
window.navigationBarColor = Color.Transparent.toArgb()
|
||||
|
|
|
|||
|
|
@ -6,16 +6,14 @@ import androidx.compose.animation.fadeIn
|
|||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
object Transitions {
|
||||
val DefaultTransitionIn = fadeIn(tween(300))
|
||||
val DefaultTransitionOut = fadeOut(tween(300))
|
||||
|
||||
val SlidingTransitionIn = fadeIn(tween(300), initialAlpha = 0.6f) +
|
||||
val SlidingTransitionIn = fadeIn(tween(300)) +
|
||||
slideInHorizontally(tween(300)) { it / 6 }
|
||||
|
||||
val SlidingTransitionOut = fadeOut(tween(300)) +
|
||||
|
|
@ -39,15 +37,6 @@ fun DefaultTransition(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TopAppBarTransition(visible: Boolean, content: @Composable (() -> Unit)) {
|
||||
DefaultTransition(visible = visible) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
package com.acclorite.books_history.ui.theme
|
||||
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val md_theme_light_primary = Color(0xFF9C4143)
|
||||
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_primaryContainer = Color(0xFFFFDAD8)
|
||||
private val md_theme_light_onPrimaryContainer = Color(0xFF410007)
|
||||
private val md_theme_light_secondary = Color(0xFF775655)
|
||||
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_secondaryContainer = Color(0xFFFFDAD8)
|
||||
private val md_theme_light_onSecondaryContainer = Color(0xFF2C1515)
|
||||
private val md_theme_light_tertiary = Color(0xFF745A2F)
|
||||
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_tertiaryContainer = Color(0xFFFFDEAC)
|
||||
private val md_theme_light_onTertiaryContainer = Color(0xFF281900)
|
||||
private val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
private val md_theme_light_background = Color(0xFFFFFBFF)
|
||||
private val md_theme_light_onBackground = Color(0xFF201A1A)
|
||||
private val md_theme_light_surface = Color(0xFFFFFBFF)
|
||||
private val md_theme_light_onSurface = Color(0xFF201A1A)
|
||||
private val md_theme_light_surfaceVariant = Color(0xFFF4DDDC)
|
||||
private val md_theme_light_onSurfaceVariant = Color(0xFF524343)
|
||||
private val md_theme_light_outline = Color(0xFF857372)
|
||||
private val md_theme_light_inverseOnSurface = Color(0xFFFBEEED)
|
||||
private val md_theme_light_inverseSurface = Color(0xFF362F2E)
|
||||
private val md_theme_light_inversePrimary = Color(0xFFFFB3B1)
|
||||
private val md_theme_light_surfaceTint = Color(0xFF9C4143)
|
||||
private val md_theme_light_outlineVariant = Color(0xFFD7C1C0)
|
||||
private val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
private val md_theme_dark_primary = Color(0xFFFFB3B1)
|
||||
private val md_theme_dark_onPrimary = Color(0xFF5F1319)
|
||||
private val md_theme_dark_primaryContainer = Color(0xFF7E2A2D)
|
||||
private val md_theme_dark_onPrimaryContainer = Color(0xFFFFDAD8)
|
||||
private val md_theme_dark_secondary = Color(0xFFE6BDBB)
|
||||
private val md_theme_dark_onSecondary = Color(0xFF442929)
|
||||
private val md_theme_dark_secondaryContainer = Color(0xFF5D3F3E)
|
||||
private val md_theme_dark_onSecondaryContainer = Color(0xFFFFDAD8)
|
||||
private val md_theme_dark_tertiary = Color(0xFFE4C18D)
|
||||
private val md_theme_dark_onTertiary = Color(0xFF412D05)
|
||||
private val md_theme_dark_tertiaryContainer = Color(0xFF5A4319)
|
||||
private val md_theme_dark_onTertiaryContainer = Color(0xFFFFDEAC)
|
||||
private val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
private val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
private val md_theme_dark_onError = Color(0xFF690005)
|
||||
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_dark_background = Color(0xFF201A1A)
|
||||
private val md_theme_dark_onBackground = Color(0xFFEDE0DF)
|
||||
private val md_theme_dark_surface = Color(0xFF201A1A)
|
||||
private val md_theme_dark_onSurface = Color(0xFFEDE0DF)
|
||||
private val md_theme_dark_surfaceVariant = Color(0xFF524343)
|
||||
private val md_theme_dark_onSurfaceVariant = Color(0xFFD7C1C0)
|
||||
private val md_theme_dark_outline = Color(0xFFA08C8B)
|
||||
private val md_theme_dark_inverseOnSurface = Color(0xFF201A1A)
|
||||
private val md_theme_dark_inverseSurface = Color(0xFFEDE0DF)
|
||||
private val md_theme_dark_inversePrimary = Color(0xFF9C4143)
|
||||
private val md_theme_dark_surfaceTint = Color(0xFFFFB3B1)
|
||||
private val md_theme_dark_outlineVariant = Color(0xFF524343)
|
||||
private val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
@Composable
|
||||
fun redTheme(isDark: Boolean): ColorScheme {
|
||||
return if (isDark) {
|
||||
darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim
|
||||
)
|
||||
} else {
|
||||
lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
package com.acclorite.books_history.ui.theme
|
||||
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
private val md_theme_light_primary = Color(0xFF8F4E00)
|
||||
private val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_primaryContainer = Color(0xFFFFDCC2)
|
||||
private val md_theme_light_onPrimaryContainer = Color(0xFF2E1500)
|
||||
private val md_theme_light_secondary = Color(0xFF745943)
|
||||
private val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_secondaryContainer = Color(0xFFFFDCC2)
|
||||
private val md_theme_light_onSecondaryContainer = Color(0xFF2A1707)
|
||||
private val md_theme_light_tertiary = Color(0xFF5B6237)
|
||||
private val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_tertiaryContainer = Color(0xFFE0E7B1)
|
||||
private val md_theme_light_onTertiaryContainer = Color(0xFF191E00)
|
||||
private val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
private val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
private val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
private val md_theme_light_background = Color(0xFFFFFBFF)
|
||||
private val md_theme_light_onBackground = Color(0xFF201B17)
|
||||
private val md_theme_light_surface = Color(0xFFFFFBFF)
|
||||
private val md_theme_light_onSurface = Color(0xFF201B17)
|
||||
private val md_theme_light_surfaceVariant = Color(0xFFF3DFD1)
|
||||
private val md_theme_light_onSurfaceVariant = Color(0xFF51443B)
|
||||
private val md_theme_light_outline = Color(0xFF847469)
|
||||
private val md_theme_light_inverseOnSurface = Color(0xFFFAEEE8)
|
||||
private val md_theme_light_inverseSurface = Color(0xFF352F2B)
|
||||
private val md_theme_light_inversePrimary = Color(0xFFFFB77A)
|
||||
private val md_theme_light_surfaceTint = Color(0xFF8F4E00)
|
||||
private val md_theme_light_outlineVariant = Color(0xFFD6C3B6)
|
||||
private val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
private val md_theme_dark_primary = Color(0xFFFFB77A)
|
||||
private val md_theme_dark_onPrimary = Color(0xFF4C2700)
|
||||
private val md_theme_dark_primaryContainer = Color(0xFF6D3A00)
|
||||
private val md_theme_dark_onPrimaryContainer = Color(0xFFFFDCC2)
|
||||
private val md_theme_dark_secondary = Color(0xFFE3C0A5)
|
||||
private val md_theme_dark_onSecondary = Color(0xFF412C19)
|
||||
private val md_theme_dark_secondaryContainer = Color(0xFF5A422E)
|
||||
private val md_theme_dark_onSecondaryContainer = Color(0xFFFFDCC2)
|
||||
private val md_theme_dark_tertiary = Color(0xFFC4CB97)
|
||||
private val md_theme_dark_onTertiary = Color(0xFF2D330E)
|
||||
private val md_theme_dark_tertiaryContainer = Color(0xFF434A22)
|
||||
private val md_theme_dark_onTertiaryContainer = Color(0xFFE0E7B1)
|
||||
private val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
private val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
private val md_theme_dark_onError = Color(0xFF690005)
|
||||
private val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
private val md_theme_dark_background = Color(0xFF201B17)
|
||||
private val md_theme_dark_onBackground = Color(0xFFECE0DA)
|
||||
private val md_theme_dark_surface = Color(0xFF201B17)
|
||||
private val md_theme_dark_onSurface = Color(0xFFECE0DA)
|
||||
private val md_theme_dark_surfaceVariant = Color(0xFF51443B)
|
||||
private val md_theme_dark_onSurfaceVariant = Color(0xFFD6C3B6)
|
||||
private val md_theme_dark_outline = Color(0xFF9E8E82)
|
||||
private val md_theme_dark_inverseOnSurface = Color(0xFF201B17)
|
||||
private val md_theme_dark_inverseSurface = Color(0xFFECE0DA)
|
||||
private val md_theme_dark_inversePrimary = Color(0xFF8F4E00)
|
||||
private val md_theme_dark_surfaceTint = Color(0xFFFFB77A)
|
||||
private val md_theme_dark_outlineVariant = Color(0xFF51443B)
|
||||
private val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
@Composable
|
||||
fun yellowTheme(isDark: Boolean): ColorScheme {
|
||||
return if (isDark) {
|
||||
darkColorScheme(
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim
|
||||
)
|
||||
} else {
|
||||
lightColorScheme(
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,10 +5,14 @@ import androidx.compose.ui.text.font.FontFamily
|
|||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import com.acclorite.books_history.R
|
||||
import com.acclorite.books_history.domain.model.Book
|
||||
import com.acclorite.books_history.domain.model.Category
|
||||
import com.acclorite.books_history.domain.model.FontWithName
|
||||
import com.acclorite.books_history.ui.Theme
|
||||
|
||||
object Constants {
|
||||
|
||||
// SavedStateHandle constants(actually just all settings params)
|
||||
const val LANGUAGE = "language"
|
||||
const val THEME = "theme"
|
||||
const val DARK_THEME = "dark_theme"
|
||||
|
|
@ -22,13 +26,27 @@ object Constants {
|
|||
const val PARAGRAPH_HEIGHT = "paragraph_height"
|
||||
const val PARAGRAPH_INDENTATION = "paragraph_indentation"
|
||||
|
||||
// Supported file extensions
|
||||
val EXTENSIONS = listOf(".txt", ".pdf", ".epub")
|
||||
|
||||
// Supported languages
|
||||
val LANGUAGES = listOf(
|
||||
Pair("en", "English"),
|
||||
Pair("uk", "Українська")
|
||||
Pair("uk", "Українська"),
|
||||
)
|
||||
|
||||
// Supported themes
|
||||
val THEMES = listOf(
|
||||
Pair(Theme.DYNAMIC, UIText.StringResource(R.string.dynamic_theme)),
|
||||
Pair(Theme.BLUE, UIText.StringResource(R.string.blue_theme)),
|
||||
Pair(Theme.PURPLE, UIText.StringResource(R.string.purple_theme)),
|
||||
Pair(Theme.GREEN, UIText.StringResource(R.string.green_theme)),
|
||||
Pair(Theme.PINK, UIText.StringResource(R.string.pink_theme)),
|
||||
Pair(Theme.YELLOW, UIText.StringResource(R.string.yellow_theme)),
|
||||
Pair(Theme.RED, UIText.StringResource(R.string.red_theme)),
|
||||
)
|
||||
|
||||
// Fonts for Reader
|
||||
val FONTS = listOf(
|
||||
FontWithName(
|
||||
"Raleway",
|
||||
|
|
@ -141,4 +159,18 @@ object Constants {
|
|||
)
|
||||
)
|
||||
)
|
||||
|
||||
val EMPTY_BOOK = Book(
|
||||
null,
|
||||
"",
|
||||
UIText.StringValue(""),
|
||||
null,
|
||||
emptyList(),
|
||||
0f,
|
||||
null,
|
||||
"",
|
||||
null,
|
||||
Category.READING,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/app_icon_colored"
|
||||
<item
|
||||
android:drawable="@drawable/app_icon_colored"
|
||||
android:height="75.dp"
|
||||
android:width="75.dp"
|
||||
android:gravity="center"
|
||||
/>
|
||||
android:gravity="center" />
|
||||
</layer-list>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/app_icon_background"/>
|
||||
<foreground android:drawable="@mipmap/app_icon_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/app_icon_monochrome"/>
|
||||
<background android:drawable="@color/app_icon_background" />
|
||||
<foreground android:drawable="@mipmap/app_icon_foreground" />
|
||||
<monochrome android:drawable="@mipmap/app_icon_monochrome" />
|
||||
</adaptive-icon>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/app_icon_background"/>
|
||||
<foreground android:drawable="@mipmap/app_icon_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/app_icon_monochrome"/>
|
||||
<background android:drawable="@color/app_icon_background" />
|
||||
<foreground android:drawable="@mipmap/app_icon_foreground" />
|
||||
<monochrome android:drawable="@mipmap/app_icon_monochrome" />
|
||||
</adaptive-icon>
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<style name="Theme.Start.Splash" parent="Theme.SplashScreen">
|
||||
<item name="android:forceDarkAllowed" tools:ignore="NewApi">false</item>
|
||||
<item name="postSplashScreenTheme">@style/Theme.BooksHistoryResurrection</item>
|
||||
|
|
|
|||
|
|
@ -12,8 +12,11 @@
|
|||
|
||||
<!-- Dialog Labels -->
|
||||
<string name="storage_permission">Надати Дозвіл?</string>
|
||||
<string name="add_books">Додати ці книги?</string>
|
||||
<string name="add_books">Додати книги?</string>
|
||||
<string name="move_books">Перемістити книги?</string>
|
||||
<string name="delete_books">Видалити книги?</string>
|
||||
<string name="delete_book">Видалити книгу?</string>
|
||||
<string name="move_book">Перемістити книгу?</string>
|
||||
|
||||
<!-- Dialog Descriptions -->
|
||||
<string name="storage_permission_description">
|
||||
|
|
@ -28,11 +31,26 @@
|
|||
Виберіть категорію, в яку ви хочете перемістити всі обрані книги(%1$s).
|
||||
Всі обрані книги будуть переміщені в обрану категорію.
|
||||
</string>
|
||||
<string name="delete_books_description">
|
||||
Це видалить всі обрані книги(%1$s) з бази даних.
|
||||
Це не видалить обрані книги з вашого пристрою.
|
||||
</string>
|
||||
<string name="delete_book_description">
|
||||
Це видалить цю книгу з бази даних.
|
||||
Це не видалить цю книгу з вашого пристрою.
|
||||
</string>
|
||||
<string name="move_book_description">
|
||||
Виберіть категорію, в яку ви хочете перемістити цю книгу.
|
||||
Ця книга буде переміщена в обрану категорію.
|
||||
</string>
|
||||
|
||||
<!-- Errors -->
|
||||
<string name="error_permission">
|
||||
Нам потрібен дозвіл на сховище, щоб просканувати вашу теку Завантажень та отримати книги
|
||||
</string>
|
||||
<string name="error_no_description">
|
||||
Немає опису.
|
||||
</string>
|
||||
|
||||
<!-- Is Empty messages -->
|
||||
<string name="browse_empty">
|
||||
|
|
@ -44,7 +62,8 @@
|
|||
|
||||
<!-- Query strings -->
|
||||
<string name="selected_items_count_query">%1$s обрано</string>
|
||||
<string name="search_query">Знайти %1$s...</string>
|
||||
<string name="search_query">Знайти %1$s…</string>
|
||||
<string name="you_read_query">Ви прочитали %1$s цієї книги.</string>
|
||||
|
||||
<!-- Action Texts -->
|
||||
<string name="cancel">Відмінити</string>
|
||||
|
|
@ -54,6 +73,10 @@
|
|||
<string name="add">Додати</string>
|
||||
<string name="add_book">Додати книгу</string>
|
||||
<string name="move">Перемістити</string>
|
||||
<string name="delete">Видалити</string>
|
||||
<string name="start">Почати</string>
|
||||
<string name="continue_read">Продовжити</string>
|
||||
<string name="never">Ніколи</string>
|
||||
|
||||
<!-- Items -->
|
||||
<string name="files">файли</string>
|
||||
|
|
@ -61,6 +84,12 @@
|
|||
|
||||
<!-- Book -->
|
||||
<string name="unknown_author">Невідомий</string>
|
||||
<string name="read_more">Вам потрібно читати більше!</string>
|
||||
<string name="read_keep">Так тримати!</string>
|
||||
<string name="change_cover">Змінити обкладинку</string>
|
||||
<string name="change_cover_desc">Виберіть нову обкладинку з ваших фото</string>
|
||||
<string name="delete_cover">Видалити обкладинку</string>
|
||||
<string name="delete_cover_desc">Видаліть нинішню обкладинку книги</string>
|
||||
|
||||
<!-- Tabs -->
|
||||
<string name="reading_tab">Читаю</string>
|
||||
|
|
@ -84,16 +113,38 @@
|
|||
<string name="dark_theme_on">Ввімкнено</string>
|
||||
<string name="dark_theme_follow_system">Тема системи</string>
|
||||
|
||||
<!-- Theme properties -->
|
||||
<!-- Themes -->
|
||||
<string name="dynamic_theme">Хаос</string>
|
||||
<string name="blue_theme">Зоряний шлях</string>
|
||||
<string name="blue_theme">Нептун</string>
|
||||
<string name="purple_theme">Супернова</string>
|
||||
<string name="green_theme">Земля</string>
|
||||
<string name="pink_theme">Пульсар</string>
|
||||
<string name="yellow_theme">Венера</string>
|
||||
<string name="red_theme">Марс</string>
|
||||
|
||||
<!-- Toast messages -->
|
||||
<string name="press_again_toast">Натисніть ще раз, щоб вийти</string>
|
||||
<string name="books_added">Всі книги були успішно додані.</string>
|
||||
<string name="books_moved">Всі книги були успішно переміщені.</string>
|
||||
<string name="books_deleted">Всі книги були успішно видалені.</string>
|
||||
<string name="cover_image_changed">Обкладинка була успішно змінена.</string>
|
||||
<string name="cover_image_deleted">Обкладинка була успішно видалена.</string>
|
||||
<string name="title_changed">Назва була успішно змінена.</string>
|
||||
<string name="file_not_found">Файл \"%1$s\" не знайдено.</string>
|
||||
<string name="book_deleted">Ця книга була успішно видалена.</string>
|
||||
<string name="book_moved">Ця книга була успішно переміщена.</string>
|
||||
<string name="copied">Скопійовано</string>
|
||||
|
||||
<!-- DropDown properties -->
|
||||
<string name="move_this_book">Перемістити</string>
|
||||
<string name="delete_this_book">Видалити</string>
|
||||
<string name="details_book">Подробиці</string>
|
||||
|
||||
<!-- BookInfo details -->
|
||||
<string name="file_name">Ім\'я файлу</string>
|
||||
<string name="file_path">Шлях до файлу</string>
|
||||
<string name="file_last_opened">Останнє відкриття файлу</string>
|
||||
<string name="file_size">Розмір файлу</string>
|
||||
<string name="unknown">Невідомо</string>
|
||||
|
||||
</resources>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<resources>
|
||||
<!-- Basic Strings -->
|
||||
<string name="app_version" translatable="false">0.5.0</string>
|
||||
<string name="app_version" translatable="false">0.6.0</string>
|
||||
<string name="app_name">Book\'s History</string>
|
||||
|
||||
<!-- Screens -->
|
||||
|
|
@ -13,8 +13,11 @@
|
|||
|
||||
<!-- Dialog Labels -->
|
||||
<string name="storage_permission">Grant Permission?</string>
|
||||
<string name="add_books">Add these books?</string>
|
||||
<string name="add_books">Add books?</string>
|
||||
<string name="move_books">Move books?</string>
|
||||
<string name="delete_books">Delete books?</string>
|
||||
<string name="delete_book">Delete book?</string>
|
||||
<string name="move_book">Move book?</string>
|
||||
|
||||
<!-- Dialog Descriptions -->
|
||||
<string name="storage_permission_description">
|
||||
|
|
@ -29,11 +32,26 @@
|
|||
Choose the category to which you want to move all the selected books(%1$s).
|
||||
All selected books will be moved to the selected category.
|
||||
</string>
|
||||
<string name="delete_books_description">
|
||||
It will delete all selected books(%1$s) from the database.
|
||||
It will not delete selected books from your device.
|
||||
</string>
|
||||
<string name="delete_book_description">
|
||||
It will delete this book from the database.
|
||||
It will not delete this book from your device.
|
||||
</string>
|
||||
<string name="move_book_description">
|
||||
Choose the category to which you want to move this book.
|
||||
This book will be moved to the selected category.
|
||||
</string>
|
||||
|
||||
<!-- Errors -->
|
||||
<string name="error_permission">
|
||||
We need storage permission to scan your Downloads directory and get books
|
||||
</string>
|
||||
<string name="error_no_description">
|
||||
No description.
|
||||
</string>
|
||||
|
||||
<!-- Is Empty messages -->
|
||||
<string name="browse_empty">
|
||||
|
|
@ -45,7 +63,8 @@
|
|||
|
||||
<!-- Query strings -->
|
||||
<string name="selected_items_count_query">%1$s selected</string>
|
||||
<string name="search_query">Search %1$s...</string>
|
||||
<string name="search_query">Search %1$s…</string>
|
||||
<string name="you_read_query">You read %1$s of this book.</string>
|
||||
|
||||
<!-- Action Texts -->
|
||||
<string name="cancel">Cancel</string>
|
||||
|
|
@ -55,6 +74,10 @@
|
|||
<string name="add">Add</string>
|
||||
<string name="add_book">Add a book</string>
|
||||
<string name="move">Move</string>
|
||||
<string name="delete">Delete</string>
|
||||
<string name="start">Start</string>
|
||||
<string name="continue_read">Continue</string>
|
||||
<string name="never">Never</string>
|
||||
|
||||
<!-- Items -->
|
||||
<string name="files">files</string>
|
||||
|
|
@ -62,6 +85,12 @@
|
|||
|
||||
<!-- Book -->
|
||||
<string name="unknown_author">Unknown</string>
|
||||
<string name="read_more">You need to read more!</string>
|
||||
<string name="read_keep">Keep it up!</string>
|
||||
<string name="change_cover">Change cover image</string>
|
||||
<string name="change_cover_desc">Choose new cover from your photos</string>
|
||||
<string name="delete_cover">Delete cover image</string>
|
||||
<string name="delete_cover_desc">Delete current cover image of the book</string>
|
||||
|
||||
<!-- Tabs -->
|
||||
<string name="reading_tab">Reading</string>
|
||||
|
|
@ -85,16 +114,38 @@
|
|||
<string name="dark_theme_on">Enabled</string>
|
||||
<string name="dark_theme_follow_system">Follow system</string>
|
||||
|
||||
<!-- Theme properties -->
|
||||
<!-- Themes -->
|
||||
<string name="dynamic_theme">Chaos</string>
|
||||
<string name="blue_theme">Milky Way</string>
|
||||
<string name="blue_theme">Neptune</string>
|
||||
<string name="purple_theme">Supernova</string>
|
||||
<string name="green_theme">Earth</string>
|
||||
<string name="pink_theme">Pulsar</string>
|
||||
<string name="yellow_theme">Venus</string>
|
||||
<string name="red_theme">Mars</string>
|
||||
|
||||
<!-- Toast messages -->
|
||||
<string name="press_again_toast">Press back again to exit</string>
|
||||
<string name="books_added">All books were successfully added.</string>
|
||||
<string name="books_moved">All books were successfully moved.</string>
|
||||
<string name="books_deleted">All books were successfully deleted.</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="title_changed">Title was successfully changed.</string>
|
||||
<string name="file_not_found">File \"%1$s\" was not found.</string>
|
||||
<string name="book_deleted">This book was successfully deleted.</string>
|
||||
<string name="book_moved">This book was successfully moved.</string>
|
||||
<string name="copied">Copied</string>
|
||||
|
||||
<!-- DropDown properties -->
|
||||
<string name="move_this_book">Move</string>
|
||||
<string name="delete_this_book">Delete</string>
|
||||
<string name="details_book">Details</string>
|
||||
|
||||
<!-- BookInfo details -->
|
||||
<string name="file_name">File name</string>
|
||||
<string name="file_path">File path</string>
|
||||
<string name="file_last_opened">File last opened</string>
|
||||
<string name="file_size">File size</string>
|
||||
<string name="unknown">Unknown</string>
|
||||
|
||||
</resources>
|
||||
|
|
@ -3,6 +3,9 @@
|
|||
|
||||
<style name="Theme.BooksHistoryResurrection" parent="android:Theme.Material.Light.NoActionBar">
|
||||
<item name="android:forceDarkAllowed" tools:ignore="NewApi">false</item>
|
||||
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Start.Splash" parent="Theme.SplashScreen">
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
package com.acclorite.books_history
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
package com.acclorite.books_history.data.repository
|
||||
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import com.acclorite.books_history.domain.model.Book
|
||||
import com.acclorite.books_history.domain.repository.BookRepository
|
||||
import com.acclorite.books_history.util.Resource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
||||
class FakeBookRepository: BookRepository {
|
||||
|
||||
private val booksList = mutableListOf<Book>()
|
||||
|
||||
override suspend fun getBooks(query: String): Flow<Resource<List<Book>>> {
|
||||
return flow { emit(Resource.Success(booksList.filter { it.title.lowercase().contains(query) })) }
|
||||
}
|
||||
|
||||
override suspend fun insertBooks(books: List<Book>) {
|
||||
booksList.addAll(books)
|
||||
}
|
||||
|
||||
override suspend fun updateBooks(books: List<Book>) {
|
||||
booksList.forEachIndexed { index, book ->
|
||||
books.forEach {
|
||||
if(it.id == book.id)
|
||||
booksList[index] = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteBooks(books: List<Book>) {
|
||||
booksList.removeAll(books)
|
||||
}
|
||||
|
||||
override suspend fun <T> retrieveDataFromDataStore(
|
||||
key: Preferences.Key<T>,
|
||||
defaultValue: T
|
||||
): Flow<T> {
|
||||
return flow { emit(defaultValue) }
|
||||
}
|
||||
|
||||
override suspend fun <T> putDataToDataStore(key: Preferences.Key<T>, value: T) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
package com.acclorite.books_history.domain.use_case
|
||||
|
||||
import com.acclorite.books_history.data.repository.FakeBookRepository
|
||||
import com.acclorite.books_history.domain.model.Book
|
||||
import com.acclorite.books_history.domain.model.Category
|
||||
import com.google.common.truth.Truth.*
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class InsertBooksTest {
|
||||
|
||||
private lateinit var insertBooks: InsertBooks
|
||||
private lateinit var fakeRepository: FakeBookRepository
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
fakeRepository = FakeBookRepository()
|
||||
insertBooks = InsertBooks(fakeRepository)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Insert all books, inserted book`() = runBlocking {
|
||||
val booksToInsert = mutableListOf<Book>()
|
||||
('a'..'z').forEachIndexed { index, c ->
|
||||
booksToInsert.add(
|
||||
Book(
|
||||
index,
|
||||
c.toString(),
|
||||
c.toString(),
|
||||
c.toString(),
|
||||
emptyList(),
|
||||
(index / 100f),
|
||||
null,
|
||||
null,
|
||||
Category.READING,
|
||||
coverImage = null
|
||||
)
|
||||
)
|
||||
}
|
||||
booksToInsert.shuffle()
|
||||
|
||||
val oldRepoSize = fakeRepository.getBooks("").first().data?.size ?: return@runBlocking
|
||||
insertBooks.execute(booksToInsert)
|
||||
val repoSize = fakeRepository.getBooks("").first().data?.size ?: return@runBlocking
|
||||
|
||||
assertThat(oldRepoSize).isLessThan(repoSize)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue