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>
|
<value>
|
||||||
<entry key="app">
|
<entry key="app">
|
||||||
<State>
|
<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>
|
<Target>
|
||||||
<type value="QUICK_BOOT_TARGET" />
|
<type value="QUICK_BOOT_TARGET" />
|
||||||
<deviceKey>
|
<deviceKey>
|
||||||
|
|
@ -14,27 +36,6 @@
|
||||||
</Key>
|
</Key>
|
||||||
</deviceKey>
|
</deviceKey>
|
||||||
</Target>
|
</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>
|
</targetsSelectedWithDialog>
|
||||||
</State>
|
</State>
|
||||||
</entry>
|
</entry>
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ android {
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "0.5.0"
|
versionName = "0.6.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
|
|
@ -27,14 +27,35 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
getByName("release") {
|
||||||
isMinifyEnabled = false
|
// 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(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
)
|
)
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// release {
|
||||||
|
// isMinifyEnabled = false
|
||||||
|
// proguardFiles(
|
||||||
|
// getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
|
// "proguard-rules.pro"
|
||||||
|
// )
|
||||||
|
// signingConfig = signingConfigs.getByName("debug")
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
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.
|
# Proguard-rules
|
||||||
# You can control the set of applied configuration files using the
|
-dontwarn org.xmlpull.v1.**
|
||||||
# proguardFiles setting in build.gradle.
|
-dontwarn org.kxml2.io.**
|
||||||
#
|
-dontwarn android.content.res.**
|
||||||
# For more details, see
|
-dontwarn org.slf4j.impl.StaticLoggerBinder
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
-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
|
-keep class org.xmlpull.** { *; }
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
-keepclassmembers class org.xmlpull.** { *; }
|
||||||
# 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
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
package com.acclorite.books_history
|
package com.acclorite.books_history
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import org.junit.Assert.*
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instrumented test, which will execute on an Android device.
|
* Instrumented test, which will execute on an Android device.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||||
tools:ignore="ScopedStorage" />
|
tools:ignore="ScopedStorage" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".util.BookApplication"
|
android:name=".util.BookApplication"
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,14 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
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.domain.model.Book
|
||||||
import com.acclorite.books_history.presentation.NavigationHost
|
import com.acclorite.books_history.presentation.NavigationHost
|
||||||
import com.acclorite.books_history.presentation.Screen
|
import com.acclorite.books_history.presentation.Screen
|
||||||
import com.acclorite.books_history.presentation.components.BottomNavigationBar
|
import com.acclorite.books_history.presentation.components.BottomNavigationBar
|
||||||
import com.acclorite.books_history.presentation.main.MainEvent
|
import com.acclorite.books_history.presentation.main.MainEvent
|
||||||
import com.acclorite.books_history.presentation.main.MainViewModel
|
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.browse.BrowseScreen
|
||||||
import com.acclorite.books_history.presentation.screens.library.LibraryScreen
|
import com.acclorite.books_history.presentation.screens.library.LibraryScreen
|
||||||
import com.acclorite.books_history.presentation.screens.settings.SettingsScreen
|
import com.acclorite.books_history.presentation.screens.settings.SettingsScreen
|
||||||
|
|
@ -41,6 +43,7 @@ class MainActivity : ComponentActivity() {
|
||||||
private val mainViewModel: MainViewModel by viewModels()
|
private val mainViewModel: MainViewModel by viewModels()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
mainViewModel.init(this)
|
mainViewModel.init(this)
|
||||||
|
|
@ -49,7 +52,9 @@ class MainActivity : ComponentActivity() {
|
||||||
!mainViewModel.isReady.value
|
!mainViewModel.isReady.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val updating = mainViewModel.updating.collectAsState().value
|
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
|
// Settings
|
||||||
composable(
|
composable(
|
||||||
screen = Screen.SETTINGS,
|
screen = Screen.SETTINGS,
|
||||||
|
|
@ -107,6 +124,7 @@ class MainActivity : ComponentActivity() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nested categories
|
||||||
composable(
|
composable(
|
||||||
screen = Screen.GENERAL_SETTINGS,
|
screen = Screen.GENERAL_SETTINGS,
|
||||||
enterAnim = Transitions.SlidingTransitionIn,
|
enterAnim = Transitions.SlidingTransitionIn,
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@ import javax.inject.Singleton
|
||||||
object AppModule {
|
object AppModule {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideBookDao(app: Application): BookDao {
|
fun provideBookDao(app: Application): BookDao {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,6 @@ import androidx.datastore.preferences.core.Preferences
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface DataStore {
|
interface DataStore {
|
||||||
suspend fun <T> getData (key: Preferences.Key<T>, defaultValue: T): Flow<T>
|
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> putData(key: Preferences.Key<T>, value: T)
|
||||||
}
|
}
|
||||||
|
|
@ -14,18 +14,18 @@ import javax.inject.Inject
|
||||||
|
|
||||||
private val Context.dataStore by preferencesDataStore("data_store")
|
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
|
private val dataStore = context.dataStore
|
||||||
|
|
||||||
override suspend fun <T> getData(key: Preferences.Key<T>, defaultValue: T): Flow<T> =
|
override suspend fun <T> getData(key: Preferences.Key<T>, defaultValue: T): Flow<T> =
|
||||||
dataStore.data.catch { exception ->
|
dataStore.data.catch { exception ->
|
||||||
if (exception is IOException){
|
if (exception is IOException) {
|
||||||
emit(emptyPreferences())
|
emit(emptyPreferences())
|
||||||
} else{
|
} else {
|
||||||
throw exception
|
throw exception
|
||||||
}
|
}
|
||||||
}.map { preferences ->
|
}.map { preferences ->
|
||||||
val result = preferences[key]?: defaultValue
|
val result = preferences[key] ?: defaultValue
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
package com.acclorite.books_history.data.mapper
|
package com.acclorite.books_history.data.mapper
|
||||||
|
|
||||||
import com.acclorite.books_history.data.local.dto.BookEntity
|
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
|
import com.acclorite.books_history.domain.model.Book
|
||||||
|
|
||||||
interface BookMapper {
|
interface BookMapper {
|
||||||
suspend fun toBookEntity(book: Book, database: BookDao): BookEntity
|
suspend fun toBookEntity(book: Book): BookEntity
|
||||||
|
|
||||||
suspend fun toBook(bookEntity: BookEntity): Book
|
suspend fun toBook(bookEntity: BookEntity): Book
|
||||||
}
|
}
|
||||||
|
|
@ -4,7 +4,6 @@ import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import com.acclorite.books_history.R
|
import com.acclorite.books_history.R
|
||||||
import com.acclorite.books_history.data.local.dto.BookEntity
|
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.EpubFileParser
|
||||||
import com.acclorite.books_history.data.parser.PdfFileParser
|
import com.acclorite.books_history.data.parser.PdfFileParser
|
||||||
import com.acclorite.books_history.data.parser.TxtFileParser
|
import com.acclorite.books_history.data.parser.TxtFileParser
|
||||||
|
|
@ -20,28 +19,14 @@ class BookMapperImpl @Inject constructor(
|
||||||
private val pdfFileParser: PdfFileParser,
|
private val pdfFileParser: PdfFileParser,
|
||||||
private val epubFileParser: EpubFileParser
|
private val epubFileParser: EpubFileParser
|
||||||
) : BookMapper {
|
) : BookMapper {
|
||||||
override suspend fun toBookEntity(book: Book, database: BookDao): BookEntity {
|
override suspend fun toBookEntity(book: Book): BookEntity {
|
||||||
val stream = ByteArrayOutputStream()
|
val stream = ByteArrayOutputStream()
|
||||||
book.coverImage?.compress(Bitmap.CompressFormat.JPEG, 50, stream)
|
book.coverImage?.compress(Bitmap.CompressFormat.JPEG, 30, stream)
|
||||||
|
|
||||||
if (book.file == null) {
|
|
||||||
val bookWithoutFile = database.findBookById(book.id!!)
|
|
||||||
|
|
||||||
return BookEntity(
|
return BookEntity(
|
||||||
id = book.id,
|
id = book.id,
|
||||||
title = book.title,
|
title = book.title,
|
||||||
filePath = bookWithoutFile.filePath,
|
filePath = book.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,
|
|
||||||
progress = book.progress,
|
progress = book.progress,
|
||||||
lastOpened = book.lastOpened,
|
lastOpened = book.lastOpened,
|
||||||
image = stream.toByteArray(),
|
image = stream.toByteArray(),
|
||||||
|
|
@ -73,6 +58,7 @@ class BookMapperImpl @Inject constructor(
|
||||||
text = emptyList(),
|
text = emptyList(),
|
||||||
progress = bookEntity.progress,
|
progress = bookEntity.progress,
|
||||||
file = file,
|
file = file,
|
||||||
|
filePath = bookEntity.filePath,
|
||||||
lastOpened = bookEntity.lastOpened,
|
lastOpened = bookEntity.lastOpened,
|
||||||
category = bookEntity.category,
|
category = bookEntity.category,
|
||||||
coverImage = if (bookEntity.image.isNotEmpty()) image else null
|
coverImage = if (bookEntity.image.isNotEmpty()) image else null
|
||||||
|
|
@ -86,6 +72,7 @@ class BookMapperImpl @Inject constructor(
|
||||||
description = null,
|
description = null,
|
||||||
progress = bookEntity.progress,
|
progress = bookEntity.progress,
|
||||||
file = null,
|
file = null,
|
||||||
|
filePath = bookEntity.filePath,
|
||||||
lastOpened = bookEntity.lastOpened,
|
lastOpened = bookEntity.lastOpened,
|
||||||
category = bookEntity.category,
|
category = bookEntity.category,
|
||||||
coverImage = if (bookEntity.image.isNotEmpty()) image else null
|
coverImage = if (bookEntity.image.isNotEmpty()) image else null
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ class EpubFileParser @Inject constructor() : FileParser {
|
||||||
text = emptyList(),
|
text = emptyList(),
|
||||||
progress = 0f,
|
progress = 0f,
|
||||||
file = file,
|
file = file,
|
||||||
|
filePath = file.path,
|
||||||
lastOpened = null,
|
lastOpened = null,
|
||||||
category = Category.entries[0],
|
category = Category.entries[0],
|
||||||
coverImage = coverImage
|
coverImage = coverImage
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ class PdfFileParser @Inject constructor(private val application: Application) :
|
||||||
text = text,
|
text = text,
|
||||||
progress = 0f,
|
progress = 0f,
|
||||||
file = file,
|
file = file,
|
||||||
|
filePath = file.path,
|
||||||
lastOpened = null,
|
lastOpened = null,
|
||||||
category = Category.entries[0],
|
category = Category.entries[0],
|
||||||
coverImage = null
|
coverImage = null
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ class TxtFileParser @Inject constructor() : FileParser {
|
||||||
text = text,
|
text = text,
|
||||||
progress = 0f,
|
progress = 0f,
|
||||||
file = file,
|
file = file,
|
||||||
|
filePath = file.path,
|
||||||
lastOpened = null,
|
lastOpened = null,
|
||||||
category = Category.entries[0],
|
category = Category.entries[0],
|
||||||
coverImage = null
|
coverImage = null
|
||||||
|
|
|
||||||
|
|
@ -66,15 +66,15 @@ class BookRepositoryImpl @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun insertBooks(books: List<Book>) {
|
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>) {
|
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>) {
|
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(
|
override suspend fun <T> retrieveDataFromDataStore(
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ data class Book(
|
||||||
val text: List<StringWithId> = emptyList(),
|
val text: List<StringWithId> = emptyList(),
|
||||||
val progress: Float,
|
val progress: Float,
|
||||||
val file: File?,
|
val file: File?,
|
||||||
|
val filePath: String,
|
||||||
val lastOpened: Long?,
|
val lastOpened: Long?,
|
||||||
val category: Category,
|
val category: Category,
|
||||||
val coverImage: Bitmap?
|
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) {
|
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)
|
return repository.retrieveDataFromDataStore(key, defaultValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ import javax.inject.Inject
|
||||||
|
|
||||||
class SetDatastore @Inject constructor(private val repository: BookRepository) {
|
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)
|
repository.putDataToDataStore(key, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -42,7 +42,7 @@ enum class Screen {
|
||||||
HISTORY,
|
HISTORY,
|
||||||
BROWSE,
|
BROWSE,
|
||||||
|
|
||||||
BOOKS_INFO,
|
BOOK_INFO,
|
||||||
READER,
|
READER,
|
||||||
|
|
||||||
SETTINGS,
|
SETTINGS,
|
||||||
|
|
@ -134,10 +134,6 @@ class Navigator @AssistedInject constructor(
|
||||||
return currentScreen
|
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.
|
* 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
|
val activity = LocalContext.current as ComponentActivity
|
||||||
|
|
||||||
activity.apply {
|
val navigator by activity.viewModels<Navigator>(
|
||||||
val navigator by viewModels<Navigator>(
|
|
||||||
extrasProducer = {
|
extrasProducer = {
|
||||||
defaultViewModelCreationExtras.withCreationCallback<Navigator.Factory> { factory ->
|
activity
|
||||||
|
.defaultViewModelCreationExtras
|
||||||
|
.withCreationCallback<Navigator.Factory> { factory ->
|
||||||
factory.create(startScreen)
|
factory.create(startScreen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -211,5 +208,4 @@ fun NavigationHost(
|
||||||
)
|
)
|
||||||
|
|
||||||
content(navigator)
|
content(navigator)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
package com.acclorite.books_history.presentation.components
|
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.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
|
@ -13,19 +16,21 @@ import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.acclorite.books_history.ui.DefaultTransition
|
import com.acclorite.books_history.ui.DefaultTransition
|
||||||
import com.acclorite.books_history.ui.elevation
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AnimatedTopAppBar(
|
fun AnimatedTopAppBar(
|
||||||
containerColor: Color,
|
containerColor: Color = MaterialTheme.colorScheme.surface,
|
||||||
scrolledContainerColor: Color?,
|
scrolledContainerColor: Color?,
|
||||||
|
|
||||||
scrollBehavior: TopAppBarScrollBehavior?,
|
scrollBehavior: TopAppBarScrollBehavior?,
|
||||||
|
isTopBarScrolled: Boolean?,
|
||||||
|
|
||||||
content1Visibility: Boolean,
|
content1Visibility: Boolean,
|
||||||
content1NavigationIcon: @Composable () -> Unit,
|
content1NavigationIcon: @Composable () -> Unit,
|
||||||
|
|
@ -42,6 +47,16 @@ fun AnimatedTopAppBar(
|
||||||
content3Title: @Composable () -> Unit,
|
content3Title: @Composable () -> Unit,
|
||||||
content3Actions: @Composable RowScope.() -> 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) {
|
if (scrollBehavior != null && scrolledContainerColor != null) {
|
||||||
val isScrolled = scrollBehavior.state.overlappedFraction > 0.01f
|
val isScrolled = scrollBehavior.state.overlappedFraction > 0.01f
|
||||||
val statusBarPadding = with(LocalDensity.current) {
|
val statusBarPadding = with(LocalDensity.current) {
|
||||||
|
|
@ -67,8 +82,8 @@ fun AnimatedTopAppBar(
|
||||||
actions = content1Actions,
|
actions = content1Actions,
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = containerColor,
|
containerColor = animatedColor,
|
||||||
scrolledContainerColor = MaterialTheme.elevation()
|
scrolledContainerColor = scrolledContainerColor ?: Color.Transparent
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -80,8 +95,8 @@ fun AnimatedTopAppBar(
|
||||||
actions = content2Actions,
|
actions = content2Actions,
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = containerColor,
|
containerColor = animatedColor,
|
||||||
scrolledContainerColor = MaterialTheme.elevation()
|
scrolledContainerColor = scrolledContainerColor ?: Color.Transparent
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -93,8 +108,8 @@ fun AnimatedTopAppBar(
|
||||||
actions = content3Actions,
|
actions = content3Actions,
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = containerColor,
|
containerColor = animatedColor,
|
||||||
scrolledContainerColor = MaterialTheme.elevation()
|
scrolledContainerColor = scrolledContainerColor ?: Color.Transparent
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import androidx.compose.material3.NavigationBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import com.acclorite.books_history.R
|
import com.acclorite.books_history.R
|
||||||
|
|
@ -27,7 +26,6 @@ fun BottomNavigationBar(
|
||||||
containerColor = MaterialTheme.elevation()
|
containerColor = MaterialTheme.elevation()
|
||||||
) {
|
) {
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
Modifier.weight(1f),
|
|
||||||
item = NavigationBarItem(
|
item = NavigationBarItem(
|
||||||
stringResource(id = R.string.library_screen),
|
stringResource(id = R.string.library_screen),
|
||||||
selectedIcon = painterResource(id = R.drawable.library_screen_filled),
|
selectedIcon = painterResource(id = R.drawable.library_screen_filled),
|
||||||
|
|
@ -38,7 +36,6 @@ fun BottomNavigationBar(
|
||||||
navigator.navigate(Screen.LIBRARY)
|
navigator.navigate(Screen.LIBRARY)
|
||||||
}
|
}
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
Modifier.weight(1f),
|
|
||||||
item = NavigationBarItem(
|
item = NavigationBarItem(
|
||||||
stringResource(id = R.string.history_screen),
|
stringResource(id = R.string.history_screen),
|
||||||
selectedIcon = painterResource(id = R.drawable.history_screen_filled),
|
selectedIcon = painterResource(id = R.drawable.history_screen_filled),
|
||||||
|
|
@ -49,7 +46,6 @@ fun BottomNavigationBar(
|
||||||
navigator.navigate(Screen.HISTORY)
|
navigator.navigate(Screen.HISTORY)
|
||||||
}
|
}
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
Modifier.weight(1f),
|
|
||||||
item = NavigationBarItem(
|
item = NavigationBarItem(
|
||||||
stringResource(id = R.string.browse_screen),
|
stringResource(id = R.string.browse_screen),
|
||||||
selectedIcon = painterResource(id = R.drawable.browse_screen_filled),
|
selectedIcon = painterResource(id = R.drawable.browse_screen_filled),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package com.acclorite.books_history.presentation.components
|
package com.acclorite.books_history.presentation.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|
@ -18,13 +17,12 @@ import com.acclorite.books_history.domain.model.NavigationBarItem
|
||||||
* Bottom Navigation Bar Item, uses default [NavigationBarItem].
|
* Bottom Navigation Bar Item, uses default [NavigationBarItem].
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun BottomNavigationBarItem(
|
fun RowScope.BottomNavigationBarItem(
|
||||||
modifier: Modifier,
|
modifier: Modifier = Modifier,
|
||||||
item: NavigationBarItem,
|
item: NavigationBarItem,
|
||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
Row(modifier.fillMaxHeight()) {
|
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
label = {
|
label = {
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -46,6 +44,7 @@ fun BottomNavigationBarItem(
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
modifier = modifier,
|
||||||
colors = NavigationBarItemDefaults.colors(
|
colors = NavigationBarItemDefaults.colors(
|
||||||
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
|
@ -53,5 +52,4 @@ fun BottomNavigationBarItem(
|
||||||
disabledTextColor = MaterialTheme.colorScheme.onSurface
|
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(),
|
properties: DialogProperties = DialogProperties(),
|
||||||
drawableIcon: Painter? = null,
|
drawableIcon: Painter? = null,
|
||||||
imageVectorIcon: ImageVector? = null,
|
imageVectorIcon: ImageVector? = null,
|
||||||
backgroundTransparency: Float = 0.2f,
|
backgroundTransparency: Float = 0.3f,
|
||||||
title: String,
|
title: String,
|
||||||
description: String?,
|
description: String?,
|
||||||
actionText: String?,
|
actionText: String?,
|
||||||
|
|
@ -63,7 +63,7 @@ fun CustomDialogWithContent(
|
||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
.statusBarsPadding()
|
.statusBarsPadding()
|
||||||
.clip(MaterialTheme.shapes.extraLarge)
|
.clip(MaterialTheme.shapes.extraLarge)
|
||||||
.background(MaterialTheme.elevation(ElevationDefaults.DialogElevation))
|
.background(MaterialTheme.elevation(ElevationDefaults.Dialog))
|
||||||
.padding(top = 24.dp, bottom = 12.dp)
|
.padding(top = 24.dp, bottom = 12.dp)
|
||||||
) {
|
) {
|
||||||
if (drawableIcon != null) {
|
if (drawableIcon != null) {
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ fun CustomDialogWithLazyColumn(
|
||||||
properties: DialogProperties = DialogProperties(),
|
properties: DialogProperties = DialogProperties(),
|
||||||
drawableIcon: Painter? = null,
|
drawableIcon: Painter? = null,
|
||||||
imageVectorIcon: ImageVector? = null,
|
imageVectorIcon: ImageVector? = null,
|
||||||
backgroundTransparency: Float = 0.2f,
|
backgroundTransparency: Float = 0.3f,
|
||||||
title: String,
|
title: String,
|
||||||
description: String?,
|
description: String?,
|
||||||
actionText: String?,
|
actionText: String?,
|
||||||
|
|
@ -65,7 +65,7 @@ fun CustomDialogWithLazyColumn(
|
||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
.statusBarsPadding()
|
.statusBarsPadding()
|
||||||
.clip(MaterialTheme.shapes.extraLarge)
|
.clip(MaterialTheme.shapes.extraLarge)
|
||||||
.background(MaterialTheme.elevation(ElevationDefaults.DialogElevation))
|
.background(MaterialTheme.elevation(ElevationDefaults.Dialog))
|
||||||
.padding(top = 24.dp, bottom = 12.dp)
|
.padding(top = 24.dp, bottom = 12.dp)
|
||||||
) {
|
) {
|
||||||
if (drawableIcon != null) {
|
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 android.Manifest
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
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.core.tween
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
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.GridCells
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
|
|
@ -35,19 +34,18 @@ import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
@ -73,12 +71,15 @@ import com.acclorite.books_history.ui.Transitions
|
||||||
import com.acclorite.books_history.ui.elevation
|
import com.acclorite.books_history.ui.elevation
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalMaterial3Api::class,
|
ExperimentalMaterial3Api::class,
|
||||||
ExperimentalPermissionsApi::class,
|
ExperimentalPermissionsApi::class,
|
||||||
ExperimentalFoundationApi::class,
|
ExperimentalFoundationApi::class,
|
||||||
ExperimentalMaterialApi::class
|
ExperimentalMaterialApi::class, FlowPreview::class
|
||||||
)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun BrowseScreen(
|
fun BrowseScreen(
|
||||||
|
|
@ -88,12 +89,7 @@ fun BrowseScreen(
|
||||||
val permissionState = rememberPermissionState(
|
val permissionState = rememberPermissionState(
|
||||||
permission = Manifest.permission.READ_EXTERNAL_STORAGE
|
permission = Manifest.permission.READ_EXTERNAL_STORAGE
|
||||||
)
|
)
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
viewModel.onEvent(BrowseEvent.OnPermissionCheck(permissionState))
|
|
||||||
}
|
|
||||||
|
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
|
||||||
val refreshState = rememberPullRefreshState(
|
val refreshState = rememberPullRefreshState(
|
||||||
refreshing = state.isRefreshing,
|
refreshing = state.isRefreshing,
|
||||||
onRefresh = {
|
onRefresh = {
|
||||||
|
|
@ -101,15 +97,29 @@ fun BrowseScreen(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
val listState = rememberLazyGridState(state.scrollIndex, state.scrollOffset)
|
||||||
|
|
||||||
val topAppBarColor = if (state.hasSelectedItems) MaterialTheme.elevation()
|
LaunchedEffect(listState) {
|
||||||
else MaterialTheme.colorScheme.surface
|
snapshotFlow {
|
||||||
|
listState.firstVisibleItemIndex
|
||||||
val animatedTopAppBarColor by animateColorAsState(
|
}
|
||||||
targetValue = topAppBarColor,
|
.debounce(10L)
|
||||||
animationSpec = tween(300, easing = LinearEasing),
|
.collectLatest {
|
||||||
label = "TopAppBar color animation"
|
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) {
|
if (state.requestPermissionDialog) {
|
||||||
BrowseStoragePermissionDialog(viewModel, permissionState)
|
BrowseStoragePermissionDialog(viewModel, permissionState)
|
||||||
|
|
@ -121,14 +131,15 @@ fun BrowseScreen(
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.pullRefresh(refreshState)
|
.pullRefresh(refreshState),
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
topBar = {
|
topBar = {
|
||||||
AnimatedTopAppBar(
|
AnimatedTopAppBar(
|
||||||
containerColor = animatedTopAppBarColor,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
scrolledContainerColor = MaterialTheme.elevation(),
|
scrolledContainerColor = MaterialTheme.elevation(),
|
||||||
scrollBehavior = scrollBehavior,
|
|
||||||
|
scrollBehavior = null,
|
||||||
|
isTopBarScrolled = state.scrollIndex > 0 || state.scrollOffset > 0 || state.hasSelectedItems,
|
||||||
|
|
||||||
content1Visibility = !state.hasSelectedItems && !state.showSearch,
|
content1Visibility = !state.hasSelectedItems && !state.showSearch,
|
||||||
content1NavigationIcon = {},
|
content1NavigationIcon = {},
|
||||||
|
|
@ -251,16 +262,18 @@ fun BrowseScreen(
|
||||||
.padding(top = padding.calculateTopPadding())
|
.padding(top = padding.calculateTopPadding())
|
||||||
) {
|
) {
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
modifier = Modifier.fillMaxSize(),
|
state = listState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
columns = GridCells.Adaptive(170.dp),
|
columns = GridCells.Adaptive(170.dp),
|
||||||
contentPadding = PaddingValues(10.dp)
|
contentPadding = PaddingValues(10.dp)
|
||||||
) {
|
) {
|
||||||
items(state.selectableFiles) { selectableFile ->
|
items(state.selectableFiles, key = { it.first.path }) { selectableFile ->
|
||||||
BrowseFileItem(
|
BrowseFileItem(
|
||||||
file = selectableFile,
|
file = selectableFile,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.animateItemPlacement(
|
.animateItemPlacement(
|
||||||
animationSpec = tween(500)
|
animationSpec = tween(300)
|
||||||
),
|
),
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.onEvent(BrowseEvent.OnSelectFile(selectableFile))
|
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.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import com.acclorite.books_history.R
|
import com.acclorite.books_history.R
|
||||||
import com.acclorite.books_history.presentation.Navigator
|
import com.acclorite.books_history.presentation.Navigator
|
||||||
import com.acclorite.books_history.presentation.components.CustomDialogWithLazyColumn
|
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.BrowseEvent
|
||||||
import com.acclorite.books_history.presentation.screens.browse.data.BrowseViewModel
|
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
|
@Composable
|
||||||
fun BrowseAddingDialog(viewModel: BrowseViewModel, navigator: Navigator) {
|
fun BrowseAddingDialog(
|
||||||
|
libraryViewModel: LibraryViewModel = hiltViewModel(),
|
||||||
|
viewModel: BrowseViewModel,
|
||||||
|
navigator: Navigator
|
||||||
|
) {
|
||||||
val state by viewModel.state.collectAsState()
|
val state by viewModel.state.collectAsState()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
|
@ -38,7 +45,14 @@ fun BrowseAddingDialog(viewModel: BrowseViewModel, navigator: Navigator) {
|
||||||
isActionEnabled = !state.isBooksLoading,
|
isActionEnabled = !state.isBooksLoading,
|
||||||
onDismiss = { viewModel.onEvent(BrowseEvent.OnAddingDialogDismiss) },
|
onDismiss = { viewModel.onEvent(BrowseEvent.OnAddingDialogDismiss) },
|
||||||
onAction = {
|
onAction = {
|
||||||
viewModel.onEvent(BrowseEvent.OnAddBooks(navigator))
|
viewModel.onEvent(
|
||||||
|
BrowseEvent.OnAddBooks(
|
||||||
|
navigator,
|
||||||
|
resetScroll = {
|
||||||
|
libraryViewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(0))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
Toast.makeText(
|
Toast.makeText(
|
||||||
context,
|
context,
|
||||||
context.getString(R.string.books_added),
|
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.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import com.acclorite.books_history.R
|
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.BrowseEvent
|
||||||
import com.acclorite.books_history.presentation.screens.browse.data.BrowseViewModel
|
import com.acclorite.books_history.presentation.screens.browse.data.BrowseViewModel
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
|
|
@ -19,12 +19,14 @@ import com.google.accompanist.permissions.PermissionState
|
||||||
fun BrowseStoragePermissionDialog(viewModel: BrowseViewModel, permissionState: PermissionState) {
|
fun BrowseStoragePermissionDialog(viewModel: BrowseViewModel, permissionState: PermissionState) {
|
||||||
val activity = LocalContext.current as ComponentActivity
|
val activity = LocalContext.current as ComponentActivity
|
||||||
|
|
||||||
CustomAlertDialog(
|
CustomDialogWithContent(
|
||||||
title = stringResource(id = R.string.storage_permission),
|
title = stringResource(id = R.string.storage_permission),
|
||||||
description = stringResource(id = R.string.storage_permission_description),
|
description = stringResource(id = R.string.storage_permission_description),
|
||||||
actionText = stringResource(id = R.string.grant),
|
actionText = stringResource(id = R.string.grant),
|
||||||
imageVectorIcon = Icons.Default.SdStorage,
|
imageVectorIcon = Icons.Default.SdStorage,
|
||||||
onDismiss = { viewModel.onEvent(BrowseEvent.OnStoragePermissionDismiss(permissionState)) },
|
onDismiss = { viewModel.onEvent(BrowseEvent.OnStoragePermissionDismiss(permissionState)) },
|
||||||
|
isActionEnabled = true,
|
||||||
|
withDivider = false,
|
||||||
onAction = {
|
onAction = {
|
||||||
viewModel.onEvent(
|
viewModel.onEvent(
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,10 @@ sealed class BrowseEvent() {
|
||||||
data object OnAddingDialogRequest : BrowseEvent()
|
data object OnAddingDialogRequest : BrowseEvent()
|
||||||
data object OnAddingDialogDismiss : BrowseEvent()
|
data object OnAddingDialogDismiss : BrowseEvent()
|
||||||
data object OnGetBooksFromFiles : BrowseEvent()
|
data object OnGetBooksFromFiles : BrowseEvent()
|
||||||
data class OnAddBooks(val navigator: Navigator) : BrowseEvent()
|
data class OnAddBooks(val navigator: Navigator, val resetScroll: () -> Unit) : BrowseEvent()
|
||||||
data object OnLoadList : BrowseEvent()
|
data object OnLoadList : BrowseEvent()
|
||||||
|
data class OnUpdateScrollIndex(val index: Int) : BrowseEvent()
|
||||||
|
data class OnUpdateScrollOffset(val offset: Int) : BrowseEvent()
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -15,6 +15,9 @@ data class BrowseState(
|
||||||
val selectedItemsCount: Int = 0,
|
val selectedItemsCount: Int = 0,
|
||||||
val hasSelectedItems: Boolean = false,
|
val hasSelectedItems: Boolean = false,
|
||||||
|
|
||||||
|
val scrollIndex: Int = 0,
|
||||||
|
val scrollOffset: Int = 0,
|
||||||
|
|
||||||
val showSearch: Boolean = false,
|
val showSearch: Boolean = false,
|
||||||
val searchQuery: String = "",
|
val searchQuery: String = "",
|
||||||
val hasFocused: Boolean = false,
|
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.presentation.Screen
|
||||||
import com.acclorite.books_history.util.Resource
|
import com.acclorite.books_history.util.Resource
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.PermissionState
|
|
||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
@ -52,11 +51,6 @@ class BrowseViewModel @Inject constructor(
|
||||||
event.permissionState.launchPermissionRequest()
|
event.permissionState.launchPermissionRequest()
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_state.update {
|
|
||||||
it.copy(
|
|
||||||
showErrorMessage = true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
for (i in 0 until 100) {
|
for (i in 0 until 100) {
|
||||||
if (!event.permissionState.status.isGranted) {
|
if (!event.permissionState.status.isGranted) {
|
||||||
delay(100)
|
delay(100)
|
||||||
|
|
@ -147,7 +141,20 @@ class BrowseViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
is BrowseEvent.OnPermissionCheck -> {
|
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 -> {
|
is BrowseEvent.OnSelectFile -> {
|
||||||
|
|
@ -299,6 +306,7 @@ class BrowseViewModel @Inject constructor(
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val booksToInsert = _state.value.selectedBooks
|
val booksToInsert = _state.value.selectedBooks
|
||||||
.filterIsInstance<NullableBook.NotNull>()
|
.filterIsInstance<NullableBook.NotNull>()
|
||||||
|
.filter { it.book!!.second }
|
||||||
.map { it.book!!.first }
|
.map { it.book!!.first }
|
||||||
|
|
||||||
if (booksToInsert.isEmpty()) {
|
if (booksToInsert.isEmpty()) {
|
||||||
|
|
@ -308,8 +316,7 @@ class BrowseViewModel @Inject constructor(
|
||||||
insertBooks.execute(booksToInsert)
|
insertBooks.execute(booksToInsert)
|
||||||
val books = fastGetBooks.execute("")
|
val books = fastGetBooks.execute("")
|
||||||
|
|
||||||
|
event.resetScroll()
|
||||||
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
showAddingDialog = false
|
showAddingDialog = false
|
||||||
|
|
@ -331,25 +338,25 @@ class BrowseViewModel @Inject constructor(
|
||||||
getFilesFromDownloads("")
|
getFilesFromDownloads("")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkPermission(permissionState: PermissionState) {
|
is BrowseEvent.OnUpdateScrollIndex -> {
|
||||||
val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
|
||||||
val isPermissionGranted = if (!legacyPermission) Environment.isExternalStorageManager()
|
|
||||||
else permissionState.status.isGranted
|
|
||||||
|
|
||||||
if (isPermissionGranted) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_state.update {
|
_state.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
requestPermissionDialog = true,
|
scrollIndex = event.index
|
||||||
showErrorMessage = false
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is BrowseEvent.OnUpdateScrollOffset -> {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
scrollOffset = event.offset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun getFilesFromDownloads(query: String = _state.value.searchQuery) {
|
private suspend fun getFilesFromDownloads(query: String = _state.value.searchQuery) {
|
||||||
getFilesFromDownloads.execute(query).collect { result ->
|
getFilesFromDownloads.execute(query).collect { result ->
|
||||||
when (result) {
|
when (result) {
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,6 @@ import android.widget.Toast
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
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.core.tween
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
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.R
|
||||||
import com.acclorite.books_history.domain.model.Book
|
import com.acclorite.books_history.domain.model.Book
|
||||||
import com.acclorite.books_history.domain.model.Category
|
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.Navigator
|
||||||
import com.acclorite.books_history.presentation.Screen
|
import com.acclorite.books_history.presentation.Screen
|
||||||
import com.acclorite.books_history.presentation.components.AnimatedTopAppBar
|
import com.acclorite.books_history.presentation.components.AnimatedTopAppBar
|
||||||
import com.acclorite.books_history.presentation.components.IsEmpty
|
import com.acclorite.books_history.presentation.components.IsEmpty
|
||||||
import com.acclorite.books_history.presentation.components.MoreDropDown
|
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.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.LibraryMoveDialog
|
||||||
import com.acclorite.books_history.presentation.screens.library.components.LibraryTabRow
|
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.LibraryEvent
|
||||||
import com.acclorite.books_history.presentation.screens.library.data.LibraryViewModel
|
import com.acclorite.books_history.presentation.screens.library.data.LibraryViewModel
|
||||||
import com.acclorite.books_history.ui.Transitions
|
import com.acclorite.books_history.ui.Transitions
|
||||||
import com.acclorite.books_history.ui.elevation
|
import com.acclorite.books_history.ui.elevation
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(
|
||||||
ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class,
|
ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class,
|
||||||
ExperimentalMaterialApi::class
|
ExperimentalMaterialApi::class, FlowPreview::class
|
||||||
)
|
)
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryScreen(
|
fun LibraryScreen(
|
||||||
|
|
@ -124,18 +125,12 @@ fun LibraryScreen(
|
||||||
viewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(pagerState.currentPage))
|
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) {
|
if (state.showMoveDialog) {
|
||||||
LibraryMoveDialog(viewModel = viewModel, pagerState = pagerState)
|
LibraryMoveDialog(viewModel = viewModel, pagerState = pagerState)
|
||||||
}
|
}
|
||||||
|
if (state.showDeleteDialog) {
|
||||||
|
LibraryDeleteDialog(viewModel = viewModel)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
Modifier
|
Modifier
|
||||||
|
|
@ -145,9 +140,11 @@ fun LibraryScreen(
|
||||||
topBar = {
|
topBar = {
|
||||||
Column(Modifier.fillMaxWidth()) {
|
Column(Modifier.fillMaxWidth()) {
|
||||||
AnimatedTopAppBar(
|
AnimatedTopAppBar(
|
||||||
containerColor = animatedTopAppBarColor,
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
scrolledContainerColor = null,
|
scrolledContainerColor = MaterialTheme.elevation(),
|
||||||
|
|
||||||
scrollBehavior = null,
|
scrollBehavior = null,
|
||||||
|
isTopBarScrolled = state.hasSelectedItems,
|
||||||
|
|
||||||
content1Visibility = !state.hasSelectedItems && !state.showSearch,
|
content1Visibility = !state.hasSelectedItems && !state.showSearch,
|
||||||
content1NavigationIcon = {},
|
content1NavigationIcon = {},
|
||||||
|
|
@ -217,7 +214,7 @@ fun LibraryScreen(
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(onClick = { /* TODO Delete book */ }) {
|
IconButton(onClick = { viewModel.onEvent(LibraryEvent.OnShowHideDeleteDialog) }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Delete,
|
imageVector = Icons.Outlined.Delete,
|
||||||
contentDescription = "Delete books from database",
|
contentDescription = "Delete books from database",
|
||||||
|
|
@ -317,18 +314,28 @@ fun LibraryScreen(
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Adaptive(120.dp),
|
columns = GridCells.Adaptive(120.dp),
|
||||||
Modifier.fillMaxSize(),
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(8.dp)
|
contentPadding = PaddingValues(8.dp)
|
||||||
) {
|
) {
|
||||||
if (!state.isLoading) {
|
if (!state.isLoading) {
|
||||||
items(categorizedBooks) {
|
items(
|
||||||
|
categorizedBooks,
|
||||||
|
key = { it.first.id ?: 0 }
|
||||||
|
) {
|
||||||
LibraryBookItem(
|
LibraryBookItem(
|
||||||
book = it,
|
book = it,
|
||||||
|
modifier = Modifier.animateItemPlacement(
|
||||||
|
animationSpec = tween(300)
|
||||||
|
),
|
||||||
onCoverImageClick = {
|
onCoverImageClick = {
|
||||||
if (state.hasSelectedItems) {
|
if (state.hasSelectedItems) {
|
||||||
viewModel.onEvent(LibraryEvent.OnSelectBook(it))
|
viewModel.onEvent(LibraryEvent.OnSelectBook(it))
|
||||||
} else {
|
} else {
|
||||||
navigator.navigate(Screen.BOOKS_INFO)
|
navigator.navigate(
|
||||||
|
Screen.BOOK_INFO,
|
||||||
|
Argument("book", it.first)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ import com.acclorite.books_history.util.removeTrailingZero
|
||||||
@Composable
|
@Composable
|
||||||
fun LibraryBookItem(
|
fun LibraryBookItem(
|
||||||
book: Pair<Book, Boolean>,
|
book: Pair<Book, Boolean>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
onCoverImageClick: () -> Unit,
|
onCoverImageClick: () -> Unit,
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
onButtonClick: () -> Unit
|
onButtonClick: () -> Unit
|
||||||
|
|
@ -65,7 +66,7 @@ fun LibraryBookItem(
|
||||||
)
|
)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
modifier
|
||||||
.padding(3.dp)
|
.padding(3.dp)
|
||||||
.background(
|
.background(
|
||||||
animatedBackgroundColor,
|
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.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.pager.PagerState
|
import androidx.compose.foundation.pager.PagerState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.DriveFileMove
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
|
@ -24,7 +26,7 @@ fun LibraryMoveDialog(viewModel: LibraryViewModel, pagerState: PagerState) {
|
||||||
|
|
||||||
CustomDialogWithLazyColumn(
|
CustomDialogWithLazyColumn(
|
||||||
title = stringResource(id = R.string.move_books),
|
title = stringResource(id = R.string.move_books),
|
||||||
imageVectorIcon = null,
|
imageVectorIcon = Icons.AutoMirrored.Outlined.DriveFileMove,
|
||||||
description = stringResource(
|
description = stringResource(
|
||||||
id = R.string.move_books_description,
|
id = R.string.move_books_description,
|
||||||
state.books.filter { it.second }.size
|
state.books.filter { it.second }.size
|
||||||
|
|
@ -42,10 +44,6 @@ fun LibraryMoveDialog(viewModel: LibraryViewModel, pagerState: PagerState) {
|
||||||
},
|
},
|
||||||
withDivider = false,
|
withDivider = false,
|
||||||
items = {
|
items = {
|
||||||
// item {
|
|
||||||
// Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
// }
|
|
||||||
|
|
||||||
items(state.categories) {
|
items(state.categories) {
|
||||||
val category = when (it) {
|
val category = when (it) {
|
||||||
Category.READING -> stringResource(id = R.string.reading_tab)
|
Category.READING -> stringResource(id = R.string.reading_tab)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import com.acclorite.books_history.domain.model.Category
|
||||||
sealed class LibraryEvent {
|
sealed class LibraryEvent {
|
||||||
data class OnPreloadBooks(val books: List<Book>) : LibraryEvent()
|
data class OnPreloadBooks(val books: List<Book>) : LibraryEvent()
|
||||||
data object OnRefreshList : LibraryEvent()
|
data object OnRefreshList : LibraryEvent()
|
||||||
|
data object OnLoadList : LibraryEvent()
|
||||||
data class OnScrollToPage(val index: Int, val pagerState: PagerState) : LibraryEvent()
|
data class OnScrollToPage(val index: Int, val pagerState: PagerState) : LibraryEvent()
|
||||||
data class OnUpdateCurrentPage(val page: Int) : LibraryEvent()
|
data class OnUpdateCurrentPage(val page: Int) : LibraryEvent()
|
||||||
data object OnSearchShowHide : LibraryEvent()
|
data object OnSearchShowHide : LibraryEvent()
|
||||||
|
|
@ -23,6 +24,6 @@ sealed class LibraryEvent {
|
||||||
data object OnShowHideMoveDialog : LibraryEvent()
|
data object OnShowHideMoveDialog : LibraryEvent()
|
||||||
data class OnSelectCategory(val category: Category) : LibraryEvent()
|
data class OnSelectCategory(val category: Category) : LibraryEvent()
|
||||||
data class OnMoveBooks(val pagerState: PagerState) : 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.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.acclorite.books_history.domain.model.Category
|
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.GetBooks
|
||||||
import com.acclorite.books_history.domain.use_case.UpdateBooks
|
import com.acclorite.books_history.domain.use_case.UpdateBooks
|
||||||
import com.acclorite.books_history.util.Resource
|
import com.acclorite.books_history.util.Resource
|
||||||
|
|
@ -21,7 +22,8 @@ import javax.inject.Inject
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class LibraryViewModel @Inject constructor(
|
class LibraryViewModel @Inject constructor(
|
||||||
private val getBooks: GetBooks,
|
private val getBooks: GetBooks,
|
||||||
private val updateBooks: UpdateBooks
|
private val updateBooks: UpdateBooks,
|
||||||
|
private val deleteBooks: DeleteBooks
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _state = MutableStateFlow(LibraryState())
|
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 -> {
|
is LibraryEvent.OnSearchShowHide -> {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val shouldHide = _state.value.showSearch
|
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 {
|
.clickable {
|
||||||
onClick()
|
onClick()
|
||||||
}
|
}
|
||||||
.padding(vertical = verticalPadding, horizontal = 16.dp),
|
.padding(vertical = verticalPadding, horizontal = 18.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Start
|
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.presentation.main.MainViewModel
|
||||||
import com.acclorite.books_history.ui.Theme
|
import com.acclorite.books_history.ui.Theme
|
||||||
import com.acclorite.books_history.ui.isDark
|
import com.acclorite.books_history.ui.isDark
|
||||||
|
import com.acclorite.books_history.util.Constants
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppearanceSettingsThemeSwitcher(
|
fun AppearanceSettingsThemeSwitcher(
|
||||||
|
|
@ -30,8 +31,8 @@ fun AppearanceSettingsThemeSwitcher(
|
||||||
val theme = mainViewModel.theme.collectAsState().value!!
|
val theme = mainViewModel.theme.collectAsState().value!!
|
||||||
val darkTheme = mainViewModel.darkTheme.collectAsState().value!!
|
val darkTheme = mainViewModel.darkTheme.collectAsState().value!!
|
||||||
|
|
||||||
val themes = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Theme.entries
|
val themes = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Constants.THEMES
|
||||||
else Theme.entries.dropWhile { it == Theme.DYNAMIC }
|
else Constants.THEMES.dropWhile { it.first == Theme.DYNAMIC }
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
|
|
@ -56,9 +57,9 @@ fun AppearanceSettingsThemeSwitcher(
|
||||||
AppearanceSettingsThemeSwitcherItem(
|
AppearanceSettingsThemeSwitcherItem(
|
||||||
theme = themeEntry,
|
theme = themeEntry,
|
||||||
darkTheme = darkTheme.isDark(),
|
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) {
|
if (index != Theme.entries.lastIndex) {
|
||||||
|
|
|
||||||
|
|
@ -27,21 +27,20 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.acclorite.books_history.R
|
|
||||||
import com.acclorite.books_history.ui.Theme
|
import com.acclorite.books_history.ui.Theme
|
||||||
import com.acclorite.books_history.ui.colorScheme
|
import com.acclorite.books_history.ui.colorScheme
|
||||||
|
import com.acclorite.books_history.util.UIText
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppearanceSettingsThemeSwitcherItem(
|
fun AppearanceSettingsThemeSwitcherItem(
|
||||||
theme: Theme,
|
theme: Pair<Theme, UIText>,
|
||||||
darkTheme: Boolean,
|
darkTheme: Boolean,
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val colorScheme = colorScheme(theme, darkTheme)
|
val colorScheme = colorScheme(theme.first, darkTheme)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
|
@ -133,13 +132,7 @@ fun AppearanceSettingsThemeSwitcherItem(
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
Text(
|
Text(
|
||||||
text = when (theme) {
|
text = theme.second.asString(),
|
||||||
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)
|
|
||||||
},
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
overflow = TextOverflow.Ellipsis,
|
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.greenTheme
|
||||||
import com.acclorite.books_history.ui.theme.pinkTheme
|
import com.acclorite.books_history.ui.theme.pinkTheme
|
||||||
import com.acclorite.books_history.ui.theme.purpleTheme
|
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 {
|
enum class Theme {
|
||||||
DYNAMIC,
|
DYNAMIC,
|
||||||
BLUE,
|
BLUE,
|
||||||
PURPLE,
|
PURPLE,
|
||||||
GREEN,
|
GREEN,
|
||||||
PINK
|
PINK,
|
||||||
|
YELLOW,
|
||||||
|
RED
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -28,6 +32,8 @@ fun String.toTheme(): Theme {
|
||||||
"PURPLE" -> Theme.PURPLE
|
"PURPLE" -> Theme.PURPLE
|
||||||
"GREEN" -> Theme.GREEN
|
"GREEN" -> Theme.GREEN
|
||||||
"PINK" -> Theme.PINK
|
"PINK" -> Theme.PINK
|
||||||
|
"YELLOW" -> Theme.YELLOW
|
||||||
|
"RED" -> Theme.RED
|
||||||
else -> Theme.BLUE
|
else -> Theme.BLUE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -66,9 +72,19 @@ fun colorScheme(theme: Theme, darkTheme: Boolean): ColorScheme {
|
||||||
return greenTheme(isDark = darkTheme)
|
return greenTheme(isDark = darkTheme)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
Theme.PINK -> {
|
||||||
/* Pink Theme */
|
/* Pink Theme */
|
||||||
return pinkTheme(isDark = darkTheme)
|
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
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
object ElevationDefaults {
|
object ElevationDefaults {
|
||||||
val DialogElevation = 6.dp
|
val Dialog = 6.dp
|
||||||
|
val BottomSheet = 1.dp
|
||||||
}
|
}
|
||||||
|
|
@ -48,7 +48,6 @@ fun BooksHistoryResurrectionTheme(
|
||||||
if (!view.isInEditMode) {
|
if (!view.isInEditMode) {
|
||||||
SideEffect {
|
SideEffect {
|
||||||
val window = (view.context as Activity).window
|
val window = (view.context as Activity).window
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
|
||||||
|
|
||||||
window.statusBarColor = Color.Transparent.toArgb()
|
window.statusBarColor = Color.Transparent.toArgb()
|
||||||
window.navigationBarColor = 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.fadeOut
|
||||||
import androidx.compose.animation.slideInHorizontally
|
import androidx.compose.animation.slideInHorizontally
|
||||||
import androidx.compose.animation.slideOutHorizontally
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
object Transitions {
|
object Transitions {
|
||||||
val DefaultTransitionIn = fadeIn(tween(300))
|
val DefaultTransitionIn = fadeIn(tween(300))
|
||||||
val DefaultTransitionOut = fadeOut(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 }
|
slideInHorizontally(tween(300)) { it / 6 }
|
||||||
|
|
||||||
val SlidingTransitionOut = fadeOut(tween(300)) +
|
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.FontStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import com.acclorite.books_history.R
|
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.domain.model.FontWithName
|
||||||
|
import com.acclorite.books_history.ui.Theme
|
||||||
|
|
||||||
object Constants {
|
object Constants {
|
||||||
|
|
||||||
|
// SavedStateHandle constants(actually just all settings params)
|
||||||
const val LANGUAGE = "language"
|
const val LANGUAGE = "language"
|
||||||
const val THEME = "theme"
|
const val THEME = "theme"
|
||||||
const val DARK_THEME = "dark_theme"
|
const val DARK_THEME = "dark_theme"
|
||||||
|
|
@ -22,13 +26,27 @@ object Constants {
|
||||||
const val PARAGRAPH_HEIGHT = "paragraph_height"
|
const val PARAGRAPH_HEIGHT = "paragraph_height"
|
||||||
const val PARAGRAPH_INDENTATION = "paragraph_indentation"
|
const val PARAGRAPH_INDENTATION = "paragraph_indentation"
|
||||||
|
|
||||||
|
// Supported file extensions
|
||||||
val EXTENSIONS = listOf(".txt", ".pdf", ".epub")
|
val EXTENSIONS = listOf(".txt", ".pdf", ".epub")
|
||||||
|
|
||||||
|
// Supported languages
|
||||||
val LANGUAGES = listOf(
|
val LANGUAGES = listOf(
|
||||||
Pair("en", "English"),
|
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(
|
val FONTS = listOf(
|
||||||
FontWithName(
|
FontWithName(
|
||||||
"Raleway",
|
"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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<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:height="75.dp"
|
||||||
android:width="75.dp"
|
android:width="75.dp"
|
||||||
android:gravity="center"
|
android:gravity="center" />
|
||||||
/>
|
|
||||||
</layer-list>
|
</layer-list>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/app_icon_background"/>
|
<background android:drawable="@color/app_icon_background" />
|
||||||
<foreground android:drawable="@mipmap/app_icon_foreground"/>
|
<foreground android:drawable="@mipmap/app_icon_foreground" />
|
||||||
<monochrome android:drawable="@mipmap/app_icon_monochrome"/>
|
<monochrome android:drawable="@mipmap/app_icon_monochrome" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@color/app_icon_background"/>
|
<background android:drawable="@color/app_icon_background" />
|
||||||
<foreground android:drawable="@mipmap/app_icon_foreground"/>
|
<foreground android:drawable="@mipmap/app_icon_foreground" />
|
||||||
<monochrome android:drawable="@mipmap/app_icon_monochrome"/>
|
<monochrome android:drawable="@mipmap/app_icon_monochrome" />
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<style name="Theme.Start.Splash" parent="Theme.SplashScreen">
|
<style name="Theme.Start.Splash" parent="Theme.SplashScreen">
|
||||||
<item name="android:forceDarkAllowed" tools:ignore="NewApi">false</item>
|
<item name="android:forceDarkAllowed" tools:ignore="NewApi">false</item>
|
||||||
<item name="postSplashScreenTheme">@style/Theme.BooksHistoryResurrection</item>
|
<item name="postSplashScreenTheme">@style/Theme.BooksHistoryResurrection</item>
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,11 @@
|
||||||
|
|
||||||
<!-- Dialog Labels -->
|
<!-- Dialog Labels -->
|
||||||
<string name="storage_permission">Надати Дозвіл?</string>
|
<string name="storage_permission">Надати Дозвіл?</string>
|
||||||
<string name="add_books">Додати ці книги?</string>
|
<string name="add_books">Додати книги?</string>
|
||||||
<string name="move_books">Перемістити книги?</string>
|
<string name="move_books">Перемістити книги?</string>
|
||||||
|
<string name="delete_books">Видалити книги?</string>
|
||||||
|
<string name="delete_book">Видалити книгу?</string>
|
||||||
|
<string name="move_book">Перемістити книгу?</string>
|
||||||
|
|
||||||
<!-- Dialog Descriptions -->
|
<!-- Dialog Descriptions -->
|
||||||
<string name="storage_permission_description">
|
<string name="storage_permission_description">
|
||||||
|
|
@ -28,11 +31,26 @@
|
||||||
Виберіть категорію, в яку ви хочете перемістити всі обрані книги(%1$s).
|
Виберіть категорію, в яку ви хочете перемістити всі обрані книги(%1$s).
|
||||||
Всі обрані книги будуть переміщені в обрану категорію.
|
Всі обрані книги будуть переміщені в обрану категорію.
|
||||||
</string>
|
</string>
|
||||||
|
<string name="delete_books_description">
|
||||||
|
Це видалить всі обрані книги(%1$s) з бази даних.
|
||||||
|
Це не видалить обрані книги з вашого пристрою.
|
||||||
|
</string>
|
||||||
|
<string name="delete_book_description">
|
||||||
|
Це видалить цю книгу з бази даних.
|
||||||
|
Це не видалить цю книгу з вашого пристрою.
|
||||||
|
</string>
|
||||||
|
<string name="move_book_description">
|
||||||
|
Виберіть категорію, в яку ви хочете перемістити цю книгу.
|
||||||
|
Ця книга буде переміщена в обрану категорію.
|
||||||
|
</string>
|
||||||
|
|
||||||
<!-- Errors -->
|
<!-- Errors -->
|
||||||
<string name="error_permission">
|
<string name="error_permission">
|
||||||
Нам потрібен дозвіл на сховище, щоб просканувати вашу теку Завантажень та отримати книги
|
Нам потрібен дозвіл на сховище, щоб просканувати вашу теку Завантажень та отримати книги
|
||||||
</string>
|
</string>
|
||||||
|
<string name="error_no_description">
|
||||||
|
Немає опису.
|
||||||
|
</string>
|
||||||
|
|
||||||
<!-- Is Empty messages -->
|
<!-- Is Empty messages -->
|
||||||
<string name="browse_empty">
|
<string name="browse_empty">
|
||||||
|
|
@ -44,7 +62,8 @@
|
||||||
|
|
||||||
<!-- Query strings -->
|
<!-- Query strings -->
|
||||||
<string name="selected_items_count_query">%1$s обрано</string>
|
<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 -->
|
<!-- Action Texts -->
|
||||||
<string name="cancel">Відмінити</string>
|
<string name="cancel">Відмінити</string>
|
||||||
|
|
@ -54,6 +73,10 @@
|
||||||
<string name="add">Додати</string>
|
<string name="add">Додати</string>
|
||||||
<string name="add_book">Додати книгу</string>
|
<string name="add_book">Додати книгу</string>
|
||||||
<string name="move">Перемістити</string>
|
<string name="move">Перемістити</string>
|
||||||
|
<string name="delete">Видалити</string>
|
||||||
|
<string name="start">Почати</string>
|
||||||
|
<string name="continue_read">Продовжити</string>
|
||||||
|
<string name="never">Ніколи</string>
|
||||||
|
|
||||||
<!-- Items -->
|
<!-- Items -->
|
||||||
<string name="files">файли</string>
|
<string name="files">файли</string>
|
||||||
|
|
@ -61,6 +84,12 @@
|
||||||
|
|
||||||
<!-- Book -->
|
<!-- Book -->
|
||||||
<string name="unknown_author">Невідомий</string>
|
<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 -->
|
<!-- Tabs -->
|
||||||
<string name="reading_tab">Читаю</string>
|
<string name="reading_tab">Читаю</string>
|
||||||
|
|
@ -84,16 +113,38 @@
|
||||||
<string name="dark_theme_on">Ввімкнено</string>
|
<string name="dark_theme_on">Ввімкнено</string>
|
||||||
<string name="dark_theme_follow_system">Тема системи</string>
|
<string name="dark_theme_follow_system">Тема системи</string>
|
||||||
|
|
||||||
<!-- Theme properties -->
|
<!-- Themes -->
|
||||||
<string name="dynamic_theme">Хаос</string>
|
<string name="dynamic_theme">Хаос</string>
|
||||||
<string name="blue_theme">Зоряний шлях</string>
|
<string name="blue_theme">Нептун</string>
|
||||||
<string name="purple_theme">Супернова</string>
|
<string name="purple_theme">Супернова</string>
|
||||||
<string name="green_theme">Земля</string>
|
<string name="green_theme">Земля</string>
|
||||||
<string name="pink_theme">Пульсар</string>
|
<string name="pink_theme">Пульсар</string>
|
||||||
|
<string name="yellow_theme">Венера</string>
|
||||||
|
<string name="red_theme">Марс</string>
|
||||||
|
|
||||||
<!-- Toast messages -->
|
<!-- Toast messages -->
|
||||||
<string name="press_again_toast">Натисніть ще раз, щоб вийти</string>
|
<string name="press_again_toast">Натисніть ще раз, щоб вийти</string>
|
||||||
<string name="books_added">Всі книги були успішно додані.</string>
|
<string name="books_added">Всі книги були успішно додані.</string>
|
||||||
<string name="books_moved">Всі книги були успішно переміщені.</string>
|
<string name="books_moved">Всі книги були успішно переміщені.</string>
|
||||||
|
<string name="books_deleted">Всі книги були успішно видалені.</string>
|
||||||
|
<string name="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>
|
</resources>
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<resources>
|
<resources>
|
||||||
<!-- Basic Strings -->
|
<!-- 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>
|
<string name="app_name">Book\'s History</string>
|
||||||
|
|
||||||
<!-- Screens -->
|
<!-- Screens -->
|
||||||
|
|
@ -13,8 +13,11 @@
|
||||||
|
|
||||||
<!-- Dialog Labels -->
|
<!-- Dialog Labels -->
|
||||||
<string name="storage_permission">Grant Permission?</string>
|
<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="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 -->
|
<!-- Dialog Descriptions -->
|
||||||
<string name="storage_permission_description">
|
<string name="storage_permission_description">
|
||||||
|
|
@ -29,11 +32,26 @@
|
||||||
Choose the category to which you want to move all the selected books(%1$s).
|
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.
|
All selected books will be moved to the selected category.
|
||||||
</string>
|
</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 -->
|
<!-- Errors -->
|
||||||
<string name="error_permission">
|
<string name="error_permission">
|
||||||
We need storage permission to scan your Downloads directory and get books
|
We need storage permission to scan your Downloads directory and get books
|
||||||
</string>
|
</string>
|
||||||
|
<string name="error_no_description">
|
||||||
|
No description.
|
||||||
|
</string>
|
||||||
|
|
||||||
<!-- Is Empty messages -->
|
<!-- Is Empty messages -->
|
||||||
<string name="browse_empty">
|
<string name="browse_empty">
|
||||||
|
|
@ -45,7 +63,8 @@
|
||||||
|
|
||||||
<!-- Query strings -->
|
<!-- Query strings -->
|
||||||
<string name="selected_items_count_query">%1$s selected</string>
|
<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 -->
|
<!-- Action Texts -->
|
||||||
<string name="cancel">Cancel</string>
|
<string name="cancel">Cancel</string>
|
||||||
|
|
@ -55,6 +74,10 @@
|
||||||
<string name="add">Add</string>
|
<string name="add">Add</string>
|
||||||
<string name="add_book">Add a book</string>
|
<string name="add_book">Add a book</string>
|
||||||
<string name="move">Move</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 -->
|
<!-- Items -->
|
||||||
<string name="files">files</string>
|
<string name="files">files</string>
|
||||||
|
|
@ -62,6 +85,12 @@
|
||||||
|
|
||||||
<!-- Book -->
|
<!-- Book -->
|
||||||
<string name="unknown_author">Unknown</string>
|
<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 -->
|
<!-- Tabs -->
|
||||||
<string name="reading_tab">Reading</string>
|
<string name="reading_tab">Reading</string>
|
||||||
|
|
@ -85,16 +114,38 @@
|
||||||
<string name="dark_theme_on">Enabled</string>
|
<string name="dark_theme_on">Enabled</string>
|
||||||
<string name="dark_theme_follow_system">Follow system</string>
|
<string name="dark_theme_follow_system">Follow system</string>
|
||||||
|
|
||||||
<!-- Theme properties -->
|
<!-- Themes -->
|
||||||
<string name="dynamic_theme">Chaos</string>
|
<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="purple_theme">Supernova</string>
|
||||||
<string name="green_theme">Earth</string>
|
<string name="green_theme">Earth</string>
|
||||||
<string name="pink_theme">Pulsar</string>
|
<string name="pink_theme">Pulsar</string>
|
||||||
|
<string name="yellow_theme">Venus</string>
|
||||||
|
<string name="red_theme">Mars</string>
|
||||||
|
|
||||||
<!-- Toast messages -->
|
<!-- Toast messages -->
|
||||||
<string name="press_again_toast">Press back again to exit</string>
|
<string name="press_again_toast">Press back again to exit</string>
|
||||||
<string name="books_added">All books were successfully added.</string>
|
<string name="books_added">All books were successfully added.</string>
|
||||||
<string name="books_moved">All books were successfully moved.</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>
|
</resources>
|
||||||
|
|
@ -3,6 +3,9 @@
|
||||||
|
|
||||||
<style name="Theme.BooksHistoryResurrection" parent="android:Theme.Material.Light.NoActionBar">
|
<style name="Theme.BooksHistoryResurrection" parent="android:Theme.Material.Light.NoActionBar">
|
||||||
<item name="android:forceDarkAllowed" tools:ignore="NewApi">false</item>
|
<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>
|
||||||
|
|
||||||
<style name="Theme.Start.Splash" parent="Theme.SplashScreen">
|
<style name="Theme.Start.Splash" parent="Theme.SplashScreen">
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
package com.acclorite.books_history
|
package com.acclorite.books_history
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example local unit test, which will execute on the development machine (host).
|
* 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