diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml index 9bd07c82..5679c4ab 100644 --- a/.idea/deploymentTargetDropDown.xml +++ b/.idea/deploymentTargetDropDown.xml @@ -4,7 +4,29 @@ - + + + + + + + + + + + + + + + + + + + + + + + @@ -14,27 +36,6 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 45f8a828..9c123d3d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,7 +18,7 @@ android { minSdk = 26 targetSdk = 34 versionCode = 1 - versionName = "0.5.0" + versionName = "0.6.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -27,14 +27,35 @@ android { } buildTypes { - release { - isMinifyEnabled = false + getByName("release") { + // Enables code shrinking, obfuscation, and optimization for only + // your project's release build type. Make sure to use a build + // variant with `isDebuggable=false`. + isMinifyEnabled = true + + // Enables resource shrinking, which is performed by the + // Android Gradle plugin. + isShrinkResources = true + + // Includes the default ProGuard rules files that are packaged with + // the Android Gradle plugin. To learn more, go to the section about + // R8 configuration files. proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) signingConfig = signingConfigs.getByName("debug") } + + +// release { +// isMinifyEnabled = false +// proguardFiles( +// getDefaultProguardFile("proguard-android-optimize.txt"), +// "proguard-rules.pro" +// ) +// signingConfig = signingConfigs.getByName("debug") +// } } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 481bb434..8030281d 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,21 +1,43 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html +# Proguard-rules +-dontwarn org.xmlpull.v1.** +-dontwarn org.kxml2.io.** +-dontwarn android.content.res.** +-dontwarn org.slf4j.impl.StaticLoggerBinder +-dontwarn com.gemalto.jp2.JP2Decoder +-dontwarn com.google.auto.service.AutoService +-dontwarn javax.annotation.processing.AbstractProcessor +-dontwarn javax.annotation.processing.Filer +-dontwarn javax.annotation.processing.Messager +-dontwarn javax.annotation.processing.ProcessingEnvironment +-dontwarn javax.lang.model.SourceVersion +-dontwarn javax.lang.model.element.AnnotationMirror +-dontwarn javax.lang.model.element.AnnotationValue +-dontwarn javax.lang.model.element.Element +-dontwarn javax.lang.model.element.ElementKind +-dontwarn javax.lang.model.element.ElementVisitor +-dontwarn javax.lang.model.element.ExecutableElement +-dontwarn javax.lang.model.element.Modifier +-dontwarn javax.lang.model.element.Name +-dontwarn javax.lang.model.element.PackageElement +-dontwarn javax.lang.model.element.TypeElement +-dontwarn javax.lang.model.element.TypeParameterElement +-dontwarn javax.lang.model.element.VariableElement +-dontwarn javax.lang.model.type.ArrayType +-dontwarn javax.lang.model.type.DeclaredType +-dontwarn javax.lang.model.type.ExecutableType +-dontwarn javax.lang.model.type.TypeKind +-dontwarn javax.lang.model.type.TypeMirror +-dontwarn javax.lang.model.type.TypeVariable +-dontwarn javax.lang.model.type.TypeVisitor +-dontwarn javax.lang.model.util.AbstractAnnotationValueVisitor8 +-dontwarn javax.lang.model.util.AbstractTypeVisitor8 +-dontwarn javax.lang.model.util.ElementFilter +-dontwarn javax.lang.model.util.Elements +-dontwarn javax.lang.model.util.SimpleElementVisitor8 +-dontwarn javax.lang.model.util.SimpleTypeVisitor7 +-dontwarn javax.lang.model.util.SimpleTypeVisitor8 +-dontwarn javax.lang.model.util.Types +-dontwarn javax.tools.Diagnostic$Kind -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +-keep class org.xmlpull.** { *; } +-keepclassmembers class org.xmlpull.** { *; } \ No newline at end of file diff --git a/app/src/androidTest/java/com/acclorite/books_history/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/acclorite/books_history/ExampleInstrumentedTest.kt index 6c197b50..e001c9c8 100644 --- a/app/src/androidTest/java/com/acclorite/books_history/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/acclorite/books_history/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package com.acclorite.books_history -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 21814654..d48af3ed 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,7 +6,6 @@ - getData (key: Preferences.Key, defaultValue: T): Flow - suspend fun putData (key: Preferences.Key, value: T) + suspend fun getData(key: Preferences.Key, defaultValue: T): Flow + suspend fun putData(key: Preferences.Key, value: T) } \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/data/local/data_store/DataStoreImpl.kt b/app/src/main/java/com/acclorite/books_history/data/local/data_store/DataStoreImpl.kt index 82e8794e..bcab84b2 100644 --- a/app/src/main/java/com/acclorite/books_history/data/local/data_store/DataStoreImpl.kt +++ b/app/src/main/java/com/acclorite/books_history/data/local/data_store/DataStoreImpl.kt @@ -14,18 +14,18 @@ import javax.inject.Inject private val Context.dataStore by preferencesDataStore("data_store") -class DataStoreImpl @Inject constructor(context: Application): DataStore { +class DataStoreImpl @Inject constructor(context: Application) : DataStore { private val dataStore = context.dataStore override suspend fun getData(key: Preferences.Key, defaultValue: T): Flow = dataStore.data.catch { exception -> - if (exception is IOException){ + if (exception is IOException) { emit(emptyPreferences()) - } else{ + } else { throw exception } }.map { preferences -> - val result = preferences[key]?: defaultValue + val result = preferences[key] ?: defaultValue result } diff --git a/app/src/main/java/com/acclorite/books_history/data/mapper/BookMapper.kt b/app/src/main/java/com/acclorite/books_history/data/mapper/BookMapper.kt index 655c5381..a904014c 100644 --- a/app/src/main/java/com/acclorite/books_history/data/mapper/BookMapper.kt +++ b/app/src/main/java/com/acclorite/books_history/data/mapper/BookMapper.kt @@ -1,11 +1,10 @@ package com.acclorite.books_history.data.mapper import com.acclorite.books_history.data.local.dto.BookEntity -import com.acclorite.books_history.data.local.room.BookDao import com.acclorite.books_history.domain.model.Book interface BookMapper { - suspend fun toBookEntity(book: Book, database: BookDao): BookEntity + suspend fun toBookEntity(book: Book): BookEntity suspend fun toBook(bookEntity: BookEntity): Book } \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/data/mapper/BookMapperImpl.kt b/app/src/main/java/com/acclorite/books_history/data/mapper/BookMapperImpl.kt index bb9bdc38..7cee664d 100644 --- a/app/src/main/java/com/acclorite/books_history/data/mapper/BookMapperImpl.kt +++ b/app/src/main/java/com/acclorite/books_history/data/mapper/BookMapperImpl.kt @@ -4,7 +4,6 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import com.acclorite.books_history.R import com.acclorite.books_history.data.local.dto.BookEntity -import com.acclorite.books_history.data.local.room.BookDao import com.acclorite.books_history.data.parser.EpubFileParser import com.acclorite.books_history.data.parser.PdfFileParser import com.acclorite.books_history.data.parser.TxtFileParser @@ -20,28 +19,14 @@ class BookMapperImpl @Inject constructor( private val pdfFileParser: PdfFileParser, private val epubFileParser: EpubFileParser ) : BookMapper { - override suspend fun toBookEntity(book: Book, database: BookDao): BookEntity { + override suspend fun toBookEntity(book: Book): BookEntity { val stream = ByteArrayOutputStream() - book.coverImage?.compress(Bitmap.CompressFormat.JPEG, 50, stream) - - if (book.file == null) { - val bookWithoutFile = database.findBookById(book.id!!) - - return BookEntity( - id = book.id, - title = book.title, - filePath = bookWithoutFile.filePath, - progress = book.progress, - lastOpened = book.lastOpened, - image = stream.toByteArray(), - category = book.category - ) - } + book.coverImage?.compress(Bitmap.CompressFormat.JPEG, 30, stream) return BookEntity( id = book.id, title = book.title, - filePath = book.file.path, + filePath = book.filePath, progress = book.progress, lastOpened = book.lastOpened, image = stream.toByteArray(), @@ -73,6 +58,7 @@ class BookMapperImpl @Inject constructor( text = emptyList(), progress = bookEntity.progress, file = file, + filePath = bookEntity.filePath, lastOpened = bookEntity.lastOpened, category = bookEntity.category, coverImage = if (bookEntity.image.isNotEmpty()) image else null @@ -86,6 +72,7 @@ class BookMapperImpl @Inject constructor( description = null, progress = bookEntity.progress, file = null, + filePath = bookEntity.filePath, lastOpened = bookEntity.lastOpened, category = bookEntity.category, coverImage = if (bookEntity.image.isNotEmpty()) image else null diff --git a/app/src/main/java/com/acclorite/books_history/data/parser/EpubFileParser.kt b/app/src/main/java/com/acclorite/books_history/data/parser/EpubFileParser.kt index 1862fe53..006dd7e9 100644 --- a/app/src/main/java/com/acclorite/books_history/data/parser/EpubFileParser.kt +++ b/app/src/main/java/com/acclorite/books_history/data/parser/EpubFileParser.kt @@ -44,6 +44,7 @@ class EpubFileParser @Inject constructor() : FileParser { text = emptyList(), progress = 0f, file = file, + filePath = file.path, lastOpened = null, category = Category.entries[0], coverImage = coverImage diff --git a/app/src/main/java/com/acclorite/books_history/data/parser/PdfFileParser.kt b/app/src/main/java/com/acclorite/books_history/data/parser/PdfFileParser.kt index 8979d6d4..2072a98c 100644 --- a/app/src/main/java/com/acclorite/books_history/data/parser/PdfFileParser.kt +++ b/app/src/main/java/com/acclorite/books_history/data/parser/PdfFileParser.kt @@ -41,6 +41,7 @@ class PdfFileParser @Inject constructor(private val application: Application) : text = text, progress = 0f, file = file, + filePath = file.path, lastOpened = null, category = Category.entries[0], coverImage = null diff --git a/app/src/main/java/com/acclorite/books_history/data/parser/TxtFileParser.kt b/app/src/main/java/com/acclorite/books_history/data/parser/TxtFileParser.kt index d225dc1d..76cd8224 100644 --- a/app/src/main/java/com/acclorite/books_history/data/parser/TxtFileParser.kt +++ b/app/src/main/java/com/acclorite/books_history/data/parser/TxtFileParser.kt @@ -28,6 +28,7 @@ class TxtFileParser @Inject constructor() : FileParser { text = text, progress = 0f, file = file, + filePath = file.path, lastOpened = null, category = Category.entries[0], coverImage = null diff --git a/app/src/main/java/com/acclorite/books_history/data/repository/BookRepositoryImpl.kt b/app/src/main/java/com/acclorite/books_history/data/repository/BookRepositoryImpl.kt index c05e3589..14a393a7 100644 --- a/app/src/main/java/com/acclorite/books_history/data/repository/BookRepositoryImpl.kt +++ b/app/src/main/java/com/acclorite/books_history/data/repository/BookRepositoryImpl.kt @@ -66,15 +66,15 @@ class BookRepositoryImpl @Inject constructor( } override suspend fun insertBooks(books: List) { - database.insertBooks(books.map { bookMapper.toBookEntity(it, database) }) + database.insertBooks(books.map { bookMapper.toBookEntity(it) }) } override suspend fun updateBooks(books: List) { - database.updateBooks(books.map { bookMapper.toBookEntity(it, database) }) + database.updateBooks(books.map { bookMapper.toBookEntity(it) }) } override suspend fun deleteBooks(books: List) { - database.deleteBooks(books.map { bookMapper.toBookEntity(it, database) }) + database.deleteBooks(books.map { bookMapper.toBookEntity(it) }) } override suspend fun retrieveDataFromDataStore( diff --git a/app/src/main/java/com/acclorite/books_history/domain/model/Book.kt b/app/src/main/java/com/acclorite/books_history/domain/model/Book.kt index 8558a954..4ee2c4ae 100644 --- a/app/src/main/java/com/acclorite/books_history/domain/model/Book.kt +++ b/app/src/main/java/com/acclorite/books_history/domain/model/Book.kt @@ -14,6 +14,7 @@ data class Book( val text: List = emptyList(), val progress: Float, val file: File?, + val filePath: String, val lastOpened: Long?, val category: Category, val coverImage: Bitmap? diff --git a/app/src/main/java/com/acclorite/books_history/domain/use_case/DeleteBooks.kt b/app/src/main/java/com/acclorite/books_history/domain/use_case/DeleteBooks.kt new file mode 100644 index 00000000..7ffe9c27 --- /dev/null +++ b/app/src/main/java/com/acclorite/books_history/domain/use_case/DeleteBooks.kt @@ -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) { + repository.deleteBooks(books) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/domain/use_case/GetDatastore.kt b/app/src/main/java/com/acclorite/books_history/domain/use_case/GetDatastore.kt index c3cec0c8..56f9654a 100644 --- a/app/src/main/java/com/acclorite/books_history/domain/use_case/GetDatastore.kt +++ b/app/src/main/java/com/acclorite/books_history/domain/use_case/GetDatastore.kt @@ -7,7 +7,7 @@ import javax.inject.Inject class GetDatastore @Inject constructor(private val repository: BookRepository) { - suspend fun execute (key: Preferences.Key, defaultValue: T): Flow { + suspend fun execute(key: Preferences.Key, defaultValue: T): Flow { return repository.retrieveDataFromDataStore(key, defaultValue) } } \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/domain/use_case/SetDatastore.kt b/app/src/main/java/com/acclorite/books_history/domain/use_case/SetDatastore.kt index a5f738b4..f536c350 100644 --- a/app/src/main/java/com/acclorite/books_history/domain/use_case/SetDatastore.kt +++ b/app/src/main/java/com/acclorite/books_history/domain/use_case/SetDatastore.kt @@ -6,7 +6,7 @@ import javax.inject.Inject class SetDatastore @Inject constructor(private val repository: BookRepository) { - suspend fun execute (key: Preferences.Key, value: T) { + suspend fun execute(key: Preferences.Key, value: T) { repository.putDataToDataStore(key, value) } } \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/presentation/Navigation.kt b/app/src/main/java/com/acclorite/books_history/presentation/Navigation.kt index ad1bb644..716dd7c2 100644 --- a/app/src/main/java/com/acclorite/books_history/presentation/Navigation.kt +++ b/app/src/main/java/com/acclorite/books_history/presentation/Navigation.kt @@ -42,7 +42,7 @@ enum class Screen { HISTORY, BROWSE, - BOOKS_INFO, + BOOK_INFO, READER, SETTINGS, @@ -134,10 +134,6 @@ class Navigator @AssistedInject constructor( return currentScreen } - fun init(startScreen: Screen) { - savedStateHandle[CURRENT_SCREEN] = startScreen - } - /** * Animated Screen. Using in [NavigationHost]. Be sure to not use the same [screen] parameter twice, it'll override the highest one in your code. * @@ -187,29 +183,29 @@ fun NavigationHost( ) { val activity = LocalContext.current as ComponentActivity - activity.apply { - val navigator by viewModels( - extrasProducer = { - defaultViewModelCreationExtras.withCreationCallback { factory -> + val navigator by activity.viewModels( + extrasProducer = { + activity + .defaultViewModelCreationExtras + .withCreationCallback { factory -> factory.create(startScreen) } - } - ) - - BackHandler { - if (navigator.canGoBack()) { - navigator.navigateBack() - } else { - activity.finishAffinity() - } } + ) - Box( - modifier = Modifier - .fillMaxSize() - .background(colorBetweenAnimations) - ) - - content(navigator) + BackHandler { + if (navigator.canGoBack()) { + navigator.navigateBack() + } else { + activity.finishAffinity() + } } + + Box( + modifier = Modifier + .fillMaxSize() + .background(colorBetweenAnimations) + ) + + content(navigator) } \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/presentation/components/AnimatedTopAppBar.kt b/app/src/main/java/com/acclorite/books_history/presentation/components/AnimatedTopAppBar.kt index e25c5a95..37e6eb14 100644 --- a/app/src/main/java/com/acclorite/books_history/presentation/components/AnimatedTopAppBar.kt +++ b/app/src/main/java/com/acclorite/books_history/presentation/components/AnimatedTopAppBar.kt @@ -1,5 +1,8 @@ package com.acclorite.books_history.presentation.components +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.spring import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.RowScope @@ -13,19 +16,21 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import com.acclorite.books_history.ui.DefaultTransition -import com.acclorite.books_history.ui.elevation @OptIn(ExperimentalMaterial3Api::class) @Composable fun AnimatedTopAppBar( - containerColor: Color, + containerColor: Color = MaterialTheme.colorScheme.surface, scrolledContainerColor: Color?, + scrollBehavior: TopAppBarScrollBehavior?, + isTopBarScrolled: Boolean?, content1Visibility: Boolean, content1NavigationIcon: @Composable () -> Unit, @@ -42,6 +47,16 @@ fun AnimatedTopAppBar( content3Title: @Composable () -> Unit, content3Actions: @Composable RowScope.() -> Unit ) { + val color = + if (isTopBarScrolled == true && scrolledContainerColor != null) scrolledContainerColor + else containerColor + + val animatedColor by animateColorAsState( + targetValue = color, + animationSpec = spring(stiffness = Spring.StiffnessMediumLow), + label = "TopAppBar color animation" + ) + if (scrollBehavior != null && scrolledContainerColor != null) { val isScrolled = scrollBehavior.state.overlappedFraction > 0.01f val statusBarPadding = with(LocalDensity.current) { @@ -67,8 +82,8 @@ fun AnimatedTopAppBar( actions = content1Actions, scrollBehavior = scrollBehavior, colors = TopAppBarDefaults.topAppBarColors( - containerColor = containerColor, - scrolledContainerColor = MaterialTheme.elevation() + containerColor = animatedColor, + scrolledContainerColor = scrolledContainerColor ?: Color.Transparent ) ) } @@ -80,8 +95,8 @@ fun AnimatedTopAppBar( actions = content2Actions, scrollBehavior = scrollBehavior, colors = TopAppBarDefaults.topAppBarColors( - containerColor = containerColor, - scrolledContainerColor = MaterialTheme.elevation() + containerColor = animatedColor, + scrolledContainerColor = scrolledContainerColor ?: Color.Transparent ) ) } @@ -93,8 +108,8 @@ fun AnimatedTopAppBar( actions = content3Actions, scrollBehavior = scrollBehavior, colors = TopAppBarDefaults.topAppBarColors( - containerColor = containerColor, - scrolledContainerColor = MaterialTheme.elevation() + containerColor = animatedColor, + scrolledContainerColor = scrolledContainerColor ?: Color.Transparent ) ) } diff --git a/app/src/main/java/com/acclorite/books_history/presentation/components/BottomNavigationBar.kt b/app/src/main/java/com/acclorite/books_history/presentation/components/BottomNavigationBar.kt index c28d4868..bae5ecf2 100644 --- a/app/src/main/java/com/acclorite/books_history/presentation/components/BottomNavigationBar.kt +++ b/app/src/main/java/com/acclorite/books_history/presentation/components/BottomNavigationBar.kt @@ -5,7 +5,6 @@ import androidx.compose.material3.NavigationBar import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import com.acclorite.books_history.R @@ -27,7 +26,6 @@ fun BottomNavigationBar( containerColor = MaterialTheme.elevation() ) { BottomNavigationBarItem( - Modifier.weight(1f), item = NavigationBarItem( stringResource(id = R.string.library_screen), selectedIcon = painterResource(id = R.drawable.library_screen_filled), @@ -38,7 +36,6 @@ fun BottomNavigationBar( navigator.navigate(Screen.LIBRARY) } BottomNavigationBarItem( - Modifier.weight(1f), item = NavigationBarItem( stringResource(id = R.string.history_screen), selectedIcon = painterResource(id = R.drawable.history_screen_filled), @@ -49,7 +46,6 @@ fun BottomNavigationBar( navigator.navigate(Screen.HISTORY) } BottomNavigationBarItem( - Modifier.weight(1f), item = NavigationBarItem( stringResource(id = R.string.browse_screen), selectedIcon = painterResource(id = R.drawable.browse_screen_filled), diff --git a/app/src/main/java/com/acclorite/books_history/presentation/components/BottomNavigationItem.kt b/app/src/main/java/com/acclorite/books_history/presentation/components/BottomNavigationItem.kt index c5244157..b4b80c79 100644 --- a/app/src/main/java/com/acclorite/books_history/presentation/components/BottomNavigationItem.kt +++ b/app/src/main/java/com/acclorite/books_history/presentation/components/BottomNavigationItem.kt @@ -1,7 +1,6 @@ package com.acclorite.books_history.presentation.components -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -18,40 +17,39 @@ import com.acclorite.books_history.domain.model.NavigationBarItem * Bottom Navigation Bar Item, uses default [NavigationBarItem]. */ @Composable -fun BottomNavigationBarItem( - modifier: Modifier, +fun RowScope.BottomNavigationBarItem( + modifier: Modifier = Modifier, item: NavigationBarItem, isSelected: Boolean, onClick: () -> Unit ) { - Row(modifier.fillMaxHeight()) { - NavigationBarItem( - label = { - Text( - text = item.title, - style = MaterialTheme.typography.labelMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - }, - selected = isSelected, - enabled = !isSelected, - onClick = { onClick() }, - icon = { - Icon( - painter = - if (isSelected) item.selectedIcon - else item.unselectedIcon, - contentDescription = item.title, - modifier = Modifier.size(24.dp) - ) - }, - colors = NavigationBarItemDefaults.colors( - unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, - unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant, - disabledIconColor = MaterialTheme.colorScheme.onSurface, - disabledTextColor = MaterialTheme.colorScheme.onSurface + NavigationBarItem( + label = { + Text( + text = item.title, + style = MaterialTheme.typography.labelMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) + }, + selected = isSelected, + enabled = !isSelected, + onClick = { onClick() }, + icon = { + Icon( + painter = + if (isSelected) item.selectedIcon + else item.unselectedIcon, + contentDescription = item.title, + modifier = Modifier.size(24.dp) + ) + }, + modifier = modifier, + colors = NavigationBarItemDefaults.colors( + unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant, + unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant, + disabledIconColor = MaterialTheme.colorScheme.onSurface, + disabledTextColor = MaterialTheme.colorScheme.onSurface ) - } + ) } \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/presentation/components/CustomAlertDialog.kt b/app/src/main/java/com/acclorite/books_history/presentation/components/CustomAlertDialog.kt deleted file mode 100644 index c567d429..00000000 --- a/app/src/main/java/com/acclorite/books_history/presentation/components/CustomAlertDialog.kt +++ /dev/null @@ -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 - ) - } - ) -} - diff --git a/app/src/main/java/com/acclorite/books_history/presentation/components/CustomDialogWithContent.kt b/app/src/main/java/com/acclorite/books_history/presentation/components/CustomDialogWithContent.kt index 0347fc58..647de3df 100644 --- a/app/src/main/java/com/acclorite/books_history/presentation/components/CustomDialogWithContent.kt +++ b/app/src/main/java/com/acclorite/books_history/presentation/components/CustomDialogWithContent.kt @@ -41,7 +41,7 @@ fun CustomDialogWithContent( properties: DialogProperties = DialogProperties(), drawableIcon: Painter? = null, imageVectorIcon: ImageVector? = null, - backgroundTransparency: Float = 0.2f, + backgroundTransparency: Float = 0.3f, title: String, description: String?, actionText: String?, @@ -63,7 +63,7 @@ fun CustomDialogWithContent( .navigationBarsPadding() .statusBarsPadding() .clip(MaterialTheme.shapes.extraLarge) - .background(MaterialTheme.elevation(ElevationDefaults.DialogElevation)) + .background(MaterialTheme.elevation(ElevationDefaults.Dialog)) .padding(top = 24.dp, bottom = 12.dp) ) { if (drawableIcon != null) { diff --git a/app/src/main/java/com/acclorite/books_history/presentation/components/CustomDialogWithLazyColumn.kt b/app/src/main/java/com/acclorite/books_history/presentation/components/CustomDialogWithLazyColumn.kt index 9ebed3b2..1c1ce798 100644 --- a/app/src/main/java/com/acclorite/books_history/presentation/components/CustomDialogWithLazyColumn.kt +++ b/app/src/main/java/com/acclorite/books_history/presentation/components/CustomDialogWithLazyColumn.kt @@ -43,7 +43,7 @@ fun CustomDialogWithLazyColumn( properties: DialogProperties = DialogProperties(), drawableIcon: Painter? = null, imageVectorIcon: ImageVector? = null, - backgroundTransparency: Float = 0.2f, + backgroundTransparency: Float = 0.3f, title: String, description: String?, actionText: String?, @@ -65,7 +65,7 @@ fun CustomDialogWithLazyColumn( .navigationBarsPadding() .statusBarsPadding() .clip(MaterialTheme.shapes.extraLarge) - .background(MaterialTheme.elevation(ElevationDefaults.DialogElevation)) + .background(MaterialTheme.elevation(ElevationDefaults.Dialog)) .padding(top = 24.dp, bottom = 12.dp) ) { if (drawableIcon != null) { diff --git a/app/src/main/java/com/acclorite/books_history/presentation/components/CustomDropDownMenuItem.kt b/app/src/main/java/com/acclorite/books_history/presentation/components/CustomDropDownMenuItem.kt new file mode 100644 index 00000000..91820fec --- /dev/null +++ b/app/src/main/java/com/acclorite/books_history/presentation/components/CustomDropDownMenuItem.kt @@ -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) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/BookInfoScreen.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/BookInfoScreen.kt new file mode 100644 index 00000000..076e7864 --- /dev/null +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/BookInfoScreen.kt @@ -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( + extrasProducer = { + val book = navigator.retrieveArgument("book") as? Book + if (book == null) { + navigator.navigateBack() + } + + context.defaultViewModelCreationExtras + .withCreationCallback { 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)) + } + } + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoBackground.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoBackground.kt new file mode 100644 index 00000000..6840ebd8 --- /dev/null +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoBackground.kt @@ -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 + ) + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoChangeCoverBottomSheet.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoChangeCoverBottomSheet.kt new file mode 100644 index 00000000..a830f5d0 --- /dev/null +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoChangeCoverBottomSheet.kt @@ -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)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoChangeCoverBottomSheetItem.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoChangeCoverBottomSheetItem.kt new file mode 100644 index 00000000..05955650 --- /dev/null +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoChangeCoverBottomSheetItem.kt @@ -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, + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoDeleteDialog.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoDeleteDialog.kt new file mode 100644 index 00000000..184c4027 --- /dev/null +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoDeleteDialog.kt @@ -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() + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoDescriptionSection.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoDescriptionSection.kt new file mode 100644 index 00000000..2b5f52c5 --- /dev/null +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoDescriptionSection.kt @@ -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)) +} \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoDetailsBottomSheet.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoDetailsBottomSheet.kt new file mode 100644 index 00000000..4daafb56 --- /dev/null +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoDetailsBottomSheet.kt @@ -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)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoDetailsBottomSheetItem.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoDetailsBottomSheetItem.kt new file mode 100644 index 00000000..422702e3 --- /dev/null +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoDetailsBottomSheetItem.kt @@ -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, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoInfoSection.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoInfoSection.kt new file mode 100644 index 00000000..9934620f --- /dev/null +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoInfoSection.kt @@ -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 + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoMoreDropDown.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoMoreDropDown.kt new file mode 100644 index 00000000..23e0be2c --- /dev/null +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoMoreDropDown.kt @@ -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) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoMoveDialog.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoMoveDialog.kt new file mode 100644 index 00000000..2192b83e --- /dev/null +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoMoveDialog.kt @@ -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)) + } + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoStatisticSection.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoStatisticSection.kt new file mode 100644 index 00000000..cbe77c4b --- /dev/null +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/components/BookInfoStatisticSection.kt @@ -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 + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/data/BookInfoEvent.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/data/BookInfoEvent.kt new file mode 100644 index 00000000..30d17975 --- /dev/null +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/data/BookInfoEvent.kt @@ -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() +} \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/data/BookInfoState.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/data/BookInfoState.kt new file mode 100644 index 00000000..e0469897 --- /dev/null +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/data/BookInfoState.kt @@ -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, +) diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/data/BookInfoViewModel.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/data/BookInfoViewModel.kt new file mode 100644 index 00000000..0cfdf7d7 --- /dev/null +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/book_info/data/BookInfoViewModel.kt @@ -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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/browse/BrowseScreen.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/browse/BrowseScreen.kt index 525a27fc..d57f276f 100644 --- a/app/src/main/java/com/acclorite/books_history/presentation/screens/browse/BrowseScreen.kt +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/browse/BrowseScreen.kt @@ -3,8 +3,6 @@ package com.acclorite.books_history.presentation.screens.browse import android.Manifest import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween import androidx.compose.animation.fadeOut import androidx.compose.foundation.ExperimentalFoundationApi @@ -17,6 +15,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.CircularProgressIndicator @@ -35,19 +34,18 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -73,12 +71,15 @@ import com.acclorite.books_history.ui.Transitions import com.acclorite.books_history.ui.elevation import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberPermissionState +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.debounce @OptIn( ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class, ExperimentalFoundationApi::class, - ExperimentalMaterialApi::class + ExperimentalMaterialApi::class, FlowPreview::class ) @Composable fun BrowseScreen( @@ -88,12 +89,7 @@ fun BrowseScreen( val permissionState = rememberPermissionState( permission = Manifest.permission.READ_EXTERNAL_STORAGE ) - LaunchedEffect(Unit) { - viewModel.onEvent(BrowseEvent.OnPermissionCheck(permissionState)) - } - val state by viewModel.state.collectAsState() - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() val refreshState = rememberPullRefreshState( refreshing = state.isRefreshing, onRefresh = { @@ -101,15 +97,29 @@ fun BrowseScreen( } ) val focusRequester = remember { FocusRequester() } + val listState = rememberLazyGridState(state.scrollIndex, state.scrollOffset) - val topAppBarColor = if (state.hasSelectedItems) MaterialTheme.elevation() - else MaterialTheme.colorScheme.surface - - val animatedTopAppBarColor by animateColorAsState( - targetValue = topAppBarColor, - animationSpec = tween(300, easing = LinearEasing), - label = "TopAppBar color animation" - ) + LaunchedEffect(listState) { + snapshotFlow { + listState.firstVisibleItemIndex + } + .debounce(10L) + .collectLatest { + viewModel.onEvent(BrowseEvent.OnUpdateScrollIndex(it)) + } + } + LaunchedEffect(listState) { + snapshotFlow { + listState.firstVisibleItemScrollOffset + } + .debounce(10L) + .collectLatest { + viewModel.onEvent(BrowseEvent.OnUpdateScrollOffset(it)) + } + } + LaunchedEffect(Unit) { + viewModel.onEvent(BrowseEvent.OnPermissionCheck(permissionState)) + } if (state.requestPermissionDialog) { BrowseStoragePermissionDialog(viewModel, permissionState) @@ -121,14 +131,15 @@ fun BrowseScreen( Scaffold( modifier = Modifier .fillMaxSize() - .pullRefresh(refreshState) - .nestedScroll(scrollBehavior.nestedScrollConnection), + .pullRefresh(refreshState), containerColor = MaterialTheme.colorScheme.surface, topBar = { AnimatedTopAppBar( - containerColor = animatedTopAppBarColor, + containerColor = MaterialTheme.colorScheme.surface, scrolledContainerColor = MaterialTheme.elevation(), - scrollBehavior = scrollBehavior, + + scrollBehavior = null, + isTopBarScrolled = state.scrollIndex > 0 || state.scrollOffset > 0 || state.hasSelectedItems, content1Visibility = !state.hasSelectedItems && !state.showSearch, content1NavigationIcon = {}, @@ -251,16 +262,18 @@ fun BrowseScreen( .padding(top = padding.calculateTopPadding()) ) { LazyVerticalGrid( - modifier = Modifier.fillMaxSize(), + state = listState, + modifier = Modifier + .fillMaxSize(), columns = GridCells.Adaptive(170.dp), contentPadding = PaddingValues(10.dp) ) { - items(state.selectableFiles) { selectableFile -> + items(state.selectableFiles, key = { it.first.path }) { selectableFile -> BrowseFileItem( file = selectableFile, modifier = Modifier .animateItemPlacement( - animationSpec = tween(500) + animationSpec = tween(300) ), onClick = { viewModel.onEvent(BrowseEvent.OnSelectFile(selectableFile)) diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/browse/components/BrowseAddingDialog.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/browse/components/BrowseAddingDialog.kt index 706a16a5..219dfd1f 100644 --- a/app/src/main/java/com/acclorite/books_history/presentation/screens/browse/components/BrowseAddingDialog.kt +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/browse/components/BrowseAddingDialog.kt @@ -19,14 +19,21 @@ import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import com.acclorite.books_history.R import com.acclorite.books_history.presentation.Navigator import com.acclorite.books_history.presentation.components.CustomDialogWithLazyColumn import com.acclorite.books_history.presentation.screens.browse.data.BrowseEvent import com.acclorite.books_history.presentation.screens.browse.data.BrowseViewModel +import com.acclorite.books_history.presentation.screens.library.data.LibraryEvent +import com.acclorite.books_history.presentation.screens.library.data.LibraryViewModel @Composable -fun BrowseAddingDialog(viewModel: BrowseViewModel, navigator: Navigator) { +fun BrowseAddingDialog( + libraryViewModel: LibraryViewModel = hiltViewModel(), + viewModel: BrowseViewModel, + navigator: Navigator +) { val state by viewModel.state.collectAsState() val context = LocalContext.current @@ -38,7 +45,14 @@ fun BrowseAddingDialog(viewModel: BrowseViewModel, navigator: Navigator) { isActionEnabled = !state.isBooksLoading, onDismiss = { viewModel.onEvent(BrowseEvent.OnAddingDialogDismiss) }, onAction = { - viewModel.onEvent(BrowseEvent.OnAddBooks(navigator)) + viewModel.onEvent( + BrowseEvent.OnAddBooks( + navigator, + resetScroll = { + libraryViewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(0)) + } + ) + ) Toast.makeText( context, context.getString(R.string.books_added), diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/browse/components/BrowseStoragePermissionDialog.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/browse/components/BrowseStoragePermissionDialog.kt index 81aacb11..a88a8902 100644 --- a/app/src/main/java/com/acclorite/books_history/presentation/screens/browse/components/BrowseStoragePermissionDialog.kt +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/browse/components/BrowseStoragePermissionDialog.kt @@ -8,7 +8,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import com.acclorite.books_history.R -import com.acclorite.books_history.presentation.components.CustomAlertDialog +import com.acclorite.books_history.presentation.components.CustomDialogWithContent import com.acclorite.books_history.presentation.screens.browse.data.BrowseEvent import com.acclorite.books_history.presentation.screens.browse.data.BrowseViewModel import com.google.accompanist.permissions.ExperimentalPermissionsApi @@ -19,12 +19,14 @@ import com.google.accompanist.permissions.PermissionState fun BrowseStoragePermissionDialog(viewModel: BrowseViewModel, permissionState: PermissionState) { val activity = LocalContext.current as ComponentActivity - CustomAlertDialog( + CustomDialogWithContent( title = stringResource(id = R.string.storage_permission), description = stringResource(id = R.string.storage_permission_description), actionText = stringResource(id = R.string.grant), imageVectorIcon = Icons.Default.SdStorage, onDismiss = { viewModel.onEvent(BrowseEvent.OnStoragePermissionDismiss(permissionState)) }, + isActionEnabled = true, + withDivider = false, onAction = { viewModel.onEvent( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/browse/data/BrowseEvent.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/browse/data/BrowseEvent.kt index ae148cea..492fdfb4 100644 --- a/app/src/main/java/com/acclorite/books_history/presentation/screens/browse/data/BrowseEvent.kt +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/browse/data/BrowseEvent.kt @@ -28,6 +28,10 @@ sealed class BrowseEvent() { data object OnAddingDialogRequest : BrowseEvent() data object OnAddingDialogDismiss : BrowseEvent() data object OnGetBooksFromFiles : BrowseEvent() - data class OnAddBooks(val navigator: Navigator) : BrowseEvent() + data class OnAddBooks(val navigator: Navigator, val resetScroll: () -> Unit) : BrowseEvent() data object OnLoadList : BrowseEvent() + data class OnUpdateScrollIndex(val index: Int) : BrowseEvent() + data class OnUpdateScrollOffset(val offset: Int) : BrowseEvent() + + } \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/browse/data/BrowseState.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/browse/data/BrowseState.kt index 8ba3c1ac..a4a59347 100644 --- a/app/src/main/java/com/acclorite/books_history/presentation/screens/browse/data/BrowseState.kt +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/browse/data/BrowseState.kt @@ -15,6 +15,9 @@ data class BrowseState( val selectedItemsCount: Int = 0, val hasSelectedItems: Boolean = false, + val scrollIndex: Int = 0, + val scrollOffset: Int = 0, + val showSearch: Boolean = false, val searchQuery: String = "", val hasFocused: Boolean = false, diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/browse/data/BrowseViewModel.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/browse/data/BrowseViewModel.kt index 38c3db8a..5bc95fae 100644 --- a/app/src/main/java/com/acclorite/books_history/presentation/screens/browse/data/BrowseViewModel.kt +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/browse/data/BrowseViewModel.kt @@ -16,7 +16,6 @@ import com.acclorite.books_history.presentation.Argument import com.acclorite.books_history.presentation.Screen import com.acclorite.books_history.util.Resource import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.PermissionState import com.google.accompanist.permissions.isGranted import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers @@ -52,11 +51,6 @@ class BrowseViewModel @Inject constructor( event.permissionState.launchPermissionRequest() viewModelScope.launch { - _state.update { - it.copy( - showErrorMessage = true - ) - } for (i in 0 until 100) { if (!event.permissionState.status.isGranted) { delay(100) @@ -147,7 +141,20 @@ class BrowseViewModel @Inject constructor( } is BrowseEvent.OnPermissionCheck -> { - checkPermission(event.permissionState) + val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R + val isPermissionGranted = + if (!legacyPermission) Environment.isExternalStorageManager() + else event.permissionState.status.isGranted + + if (isPermissionGranted) { + return + } + _state.update { + it.copy( + requestPermissionDialog = true, + showErrorMessage = false + ) + } } is BrowseEvent.OnSelectFile -> { @@ -299,6 +306,7 @@ class BrowseViewModel @Inject constructor( viewModelScope.launch { val booksToInsert = _state.value.selectedBooks .filterIsInstance() + .filter { it.book!!.second } .map { it.book!!.first } if (booksToInsert.isEmpty()) { @@ -308,8 +316,7 @@ class BrowseViewModel @Inject constructor( insertBooks.execute(booksToInsert) val books = fastGetBooks.execute("") - - + event.resetScroll() _state.update { it.copy( showAddingDialog = false @@ -331,22 +338,22 @@ class BrowseViewModel @Inject constructor( getFilesFromDownloads("") } } - } - } - private fun checkPermission(permissionState: PermissionState) { - val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R - val isPermissionGranted = if (!legacyPermission) Environment.isExternalStorageManager() - else permissionState.status.isGranted + is BrowseEvent.OnUpdateScrollIndex -> { + _state.update { + it.copy( + scrollIndex = event.index + ) + } + } - if (isPermissionGranted) { - return - } - _state.update { - it.copy( - requestPermissionDialog = true, - showErrorMessage = false - ) + is BrowseEvent.OnUpdateScrollOffset -> { + _state.update { + it.copy( + scrollOffset = event.offset + ) + } + } } } diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/library/LibraryScreen.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/library/LibraryScreen.kt index 69699df9..a0972972 100644 --- a/app/src/main/java/com/acclorite/books_history/presentation/screens/library/LibraryScreen.kt +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/library/LibraryScreen.kt @@ -4,8 +4,6 @@ import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.tween import androidx.compose.animation.fadeOut import androidx.compose.foundation.ExperimentalFoundationApi @@ -75,24 +73,27 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.acclorite.books_history.R import com.acclorite.books_history.domain.model.Book import com.acclorite.books_history.domain.model.Category +import com.acclorite.books_history.presentation.Argument import com.acclorite.books_history.presentation.Navigator import com.acclorite.books_history.presentation.Screen import com.acclorite.books_history.presentation.components.AnimatedTopAppBar import com.acclorite.books_history.presentation.components.IsEmpty import com.acclorite.books_history.presentation.components.MoreDropDown import com.acclorite.books_history.presentation.screens.library.components.LibraryBookItem +import com.acclorite.books_history.presentation.screens.library.components.LibraryDeleteDialog import com.acclorite.books_history.presentation.screens.library.components.LibraryMoveDialog import com.acclorite.books_history.presentation.screens.library.components.LibraryTabRow import com.acclorite.books_history.presentation.screens.library.data.LibraryEvent import com.acclorite.books_history.presentation.screens.library.data.LibraryViewModel import com.acclorite.books_history.ui.Transitions import com.acclorite.books_history.ui.elevation +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.delay import kotlinx.coroutines.launch @OptIn( ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class, - ExperimentalMaterialApi::class + ExperimentalMaterialApi::class, FlowPreview::class ) @Composable fun LibraryScreen( @@ -124,18 +125,12 @@ fun LibraryScreen( viewModel.onEvent(LibraryEvent.OnUpdateCurrentPage(pagerState.currentPage)) } - val topAppBarColor = if (state.hasSelectedItems) MaterialTheme.elevation() - else MaterialTheme.colorScheme.surface - - val animatedTopAppBarColor by animateColorAsState( - targetValue = topAppBarColor, - animationSpec = tween(300, easing = LinearEasing), - label = "TopAppBar color animation" - ) - if (state.showMoveDialog) { LibraryMoveDialog(viewModel = viewModel, pagerState = pagerState) } + if (state.showDeleteDialog) { + LibraryDeleteDialog(viewModel = viewModel) + } Scaffold( Modifier @@ -145,9 +140,11 @@ fun LibraryScreen( topBar = { Column(Modifier.fillMaxWidth()) { AnimatedTopAppBar( - containerColor = animatedTopAppBarColor, - scrolledContainerColor = null, + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.elevation(), + scrollBehavior = null, + isTopBarScrolled = state.hasSelectedItems, content1Visibility = !state.hasSelectedItems && !state.showSearch, content1NavigationIcon = {}, @@ -217,7 +214,7 @@ fun LibraryScreen( tint = MaterialTheme.colorScheme.onSurfaceVariant ) } - IconButton(onClick = { /* TODO Delete book */ }) { + IconButton(onClick = { viewModel.onEvent(LibraryEvent.OnShowHideDeleteDialog) }) { Icon( imageVector = Icons.Outlined.Delete, contentDescription = "Delete books from database", @@ -317,18 +314,28 @@ fun LibraryScreen( Box(modifier = Modifier.fillMaxSize()) { LazyVerticalGrid( columns = GridCells.Adaptive(120.dp), - Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize(), contentPadding = PaddingValues(8.dp) ) { if (!state.isLoading) { - items(categorizedBooks) { + items( + categorizedBooks, + key = { it.first.id ?: 0 } + ) { LibraryBookItem( book = it, + modifier = Modifier.animateItemPlacement( + animationSpec = tween(300) + ), onCoverImageClick = { if (state.hasSelectedItems) { viewModel.onEvent(LibraryEvent.OnSelectBook(it)) } else { - navigator.navigate(Screen.BOOKS_INFO) + navigator.navigate( + Screen.BOOK_INFO, + Argument("book", it.first) + ) } }, onLongClick = { diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/library/components/LibraryBookItem.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/library/components/LibraryBookItem.kt index 79af632f..3af2cca4 100644 --- a/app/src/main/java/com/acclorite/books_history/presentation/screens/library/components/LibraryBookItem.kt +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/library/components/LibraryBookItem.kt @@ -44,6 +44,7 @@ import com.acclorite.books_history.util.removeTrailingZero @Composable fun LibraryBookItem( book: Pair, + modifier: Modifier = Modifier, onCoverImageClick: () -> Unit, onLongClick: () -> Unit, onButtonClick: () -> Unit @@ -65,7 +66,7 @@ fun LibraryBookItem( ) Column( - Modifier + modifier .padding(3.dp) .background( animatedBackgroundColor, diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/library/components/LibraryDeleteDialog.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/library/components/LibraryDeleteDialog.kt new file mode 100644 index 00000000..9cfd1216 --- /dev/null +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/library/components/LibraryDeleteDialog.kt @@ -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() + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/library/components/LibraryMoveDialog.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/library/components/LibraryMoveDialog.kt index b815611b..4a15cc23 100644 --- a/app/src/main/java/com/acclorite/books_history/presentation/screens/library/components/LibraryMoveDialog.kt +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/library/components/LibraryMoveDialog.kt @@ -4,6 +4,8 @@ import android.widget.Toast import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.lazy.items import androidx.compose.foundation.pager.PagerState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.DriveFileMove import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -24,7 +26,7 @@ fun LibraryMoveDialog(viewModel: LibraryViewModel, pagerState: PagerState) { CustomDialogWithLazyColumn( title = stringResource(id = R.string.move_books), - imageVectorIcon = null, + imageVectorIcon = Icons.AutoMirrored.Outlined.DriveFileMove, description = stringResource( id = R.string.move_books_description, state.books.filter { it.second }.size @@ -42,10 +44,6 @@ fun LibraryMoveDialog(viewModel: LibraryViewModel, pagerState: PagerState) { }, withDivider = false, items = { -// item { -// Spacer(modifier = Modifier.height(12.dp)) -// } - items(state.categories) { val category = when (it) { Category.READING -> stringResource(id = R.string.reading_tab) diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/library/data/LibraryEvent.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/library/data/LibraryEvent.kt index 1f57de77..f3d0aa99 100644 --- a/app/src/main/java/com/acclorite/books_history/presentation/screens/library/data/LibraryEvent.kt +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/library/data/LibraryEvent.kt @@ -11,6 +11,7 @@ import com.acclorite.books_history.domain.model.Category sealed class LibraryEvent { data class OnPreloadBooks(val books: List) : LibraryEvent() data object OnRefreshList : LibraryEvent() + data object OnLoadList : LibraryEvent() data class OnScrollToPage(val index: Int, val pagerState: PagerState) : LibraryEvent() data class OnUpdateCurrentPage(val page: Int) : LibraryEvent() data object OnSearchShowHide : LibraryEvent() @@ -23,6 +24,6 @@ sealed class LibraryEvent { data object OnShowHideMoveDialog : LibraryEvent() data class OnSelectCategory(val category: Category) : LibraryEvent() data class OnMoveBooks(val pagerState: PagerState) : LibraryEvent() - - + data object OnShowHideDeleteDialog : LibraryEvent() + data object OnDeleteBooks : LibraryEvent() } \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/library/data/LibraryViewModel.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/library/data/LibraryViewModel.kt index b2416814..8e8baa76 100644 --- a/app/src/main/java/com/acclorite/books_history/presentation/screens/library/data/LibraryViewModel.kt +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/library/data/LibraryViewModel.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.acclorite.books_history.domain.model.Category +import com.acclorite.books_history.domain.use_case.DeleteBooks import com.acclorite.books_history.domain.use_case.GetBooks import com.acclorite.books_history.domain.use_case.UpdateBooks import com.acclorite.books_history.util.Resource @@ -21,7 +22,8 @@ import javax.inject.Inject @HiltViewModel class LibraryViewModel @Inject constructor( private val getBooks: GetBooks, - private val updateBooks: UpdateBooks + private val updateBooks: UpdateBooks, + private val deleteBooks: DeleteBooks ) : ViewModel() { private val _state = MutableStateFlow(LibraryState()) @@ -86,6 +88,17 @@ class LibraryViewModel @Inject constructor( } } + is LibraryEvent.OnLoadList -> { + viewModelScope.launch(Dispatchers.IO) { + _state.update { + it.copy( + isLoading = true + ) + } + getBooksFromDatabase() + } + } + is LibraryEvent.OnSearchShowHide -> { viewModelScope.launch(Dispatchers.IO) { val shouldHide = _state.value.showSearch @@ -217,6 +230,33 @@ class LibraryViewModel @Inject constructor( ) } } + + is LibraryEvent.OnShowHideDeleteDialog -> { + _state.update { + it.copy( + showDeleteDialog = !it.showDeleteDialog + ) + } + } + + is LibraryEvent.OnDeleteBooks -> { + viewModelScope.launch { + _state.update { + it.copy( + showDeleteDialog = false + ) + } + + val books = _state.value.books.filter { + it.second + }.map { it.first } + deleteBooks.execute(books) + + getBooksFromDatabase() + + onEvent(LibraryEvent.OnClearSelectedBooks) + } + } } } diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/settings/components/SettingsCategoryItem.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/settings/components/SettingsCategoryItem.kt index 83307382..55565312 100644 --- a/app/src/main/java/com/acclorite/books_history/presentation/screens/settings/components/SettingsCategoryItem.kt +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/settings/components/SettingsCategoryItem.kt @@ -35,7 +35,7 @@ fun SettingsCategoryItem( .clickable { onClick() } - .padding(vertical = verticalPadding, horizontal = 16.dp), + .padding(vertical = verticalPadding, horizontal = 18.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/settings/nested/appearance/components/AppearanceSettingsThemeSwitcher.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/settings/nested/appearance/components/AppearanceSettingsThemeSwitcher.kt index 9f225e70..e7dd133b 100644 --- a/app/src/main/java/com/acclorite/books_history/presentation/screens/settings/nested/appearance/components/AppearanceSettingsThemeSwitcher.kt +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/settings/nested/appearance/components/AppearanceSettingsThemeSwitcher.kt @@ -22,6 +22,7 @@ import com.acclorite.books_history.presentation.main.MainEvent import com.acclorite.books_history.presentation.main.MainViewModel import com.acclorite.books_history.ui.Theme import com.acclorite.books_history.ui.isDark +import com.acclorite.books_history.util.Constants @Composable fun AppearanceSettingsThemeSwitcher( @@ -30,8 +31,8 @@ fun AppearanceSettingsThemeSwitcher( val theme = mainViewModel.theme.collectAsState().value!! val darkTheme = mainViewModel.darkTheme.collectAsState().value!! - val themes = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Theme.entries - else Theme.entries.dropWhile { it == Theme.DYNAMIC } + val themes = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) Constants.THEMES + else Constants.THEMES.dropWhile { it.first == Theme.DYNAMIC } Column( Modifier @@ -56,9 +57,9 @@ fun AppearanceSettingsThemeSwitcher( AppearanceSettingsThemeSwitcherItem( theme = themeEntry, darkTheme = darkTheme.isDark(), - selected = theme == themeEntry + selected = theme == themeEntry.first ) { - mainViewModel.onEvent(MainEvent.OnChangeTheme(themeEntry.toString())) + mainViewModel.onEvent(MainEvent.OnChangeTheme(themeEntry.first.toString())) } if (index != Theme.entries.lastIndex) { diff --git a/app/src/main/java/com/acclorite/books_history/presentation/screens/settings/nested/appearance/components/AppearanceSettingsThemeSwitcherItem.kt b/app/src/main/java/com/acclorite/books_history/presentation/screens/settings/nested/appearance/components/AppearanceSettingsThemeSwitcherItem.kt index 2d462790..3d9a43cc 100644 --- a/app/src/main/java/com/acclorite/books_history/presentation/screens/settings/nested/appearance/components/AppearanceSettingsThemeSwitcherItem.kt +++ b/app/src/main/java/com/acclorite/books_history/presentation/screens/settings/nested/appearance/components/AppearanceSettingsThemeSwitcherItem.kt @@ -27,21 +27,20 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.acclorite.books_history.R import com.acclorite.books_history.ui.Theme import com.acclorite.books_history.ui.colorScheme +import com.acclorite.books_history.util.UIText @Composable fun AppearanceSettingsThemeSwitcherItem( - theme: Theme, + theme: Pair, darkTheme: Boolean, selected: Boolean, onClick: () -> Unit ) { - val colorScheme = colorScheme(theme, darkTheme) + val colorScheme = colorScheme(theme.first, darkTheme) Column( modifier = Modifier.fillMaxWidth(), @@ -133,13 +132,7 @@ fun AppearanceSettingsThemeSwitcherItem( } Spacer(modifier = Modifier.height(6.dp)) Text( - text = when (theme) { - Theme.BLUE -> stringResource(id = R.string.blue_theme) - Theme.PURPLE -> stringResource(id = R.string.purple_theme) - Theme.PINK -> stringResource(id = R.string.pink_theme) - Theme.GREEN -> stringResource(id = R.string.green_theme) - Theme.DYNAMIC -> stringResource(id = R.string.dynamic_theme) - }, + text = theme.second.asString(), color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.bodyMedium, overflow = TextOverflow.Ellipsis, diff --git a/app/src/main/java/com/acclorite/books_history/ui/Color.kt b/app/src/main/java/com/acclorite/books_history/ui/Color.kt index 6f640d04..24e7ede3 100644 --- a/app/src/main/java/com/acclorite/books_history/ui/Color.kt +++ b/app/src/main/java/com/acclorite/books_history/ui/Color.kt @@ -10,13 +10,17 @@ import com.acclorite.books_history.ui.theme.blueTheme import com.acclorite.books_history.ui.theme.greenTheme import com.acclorite.books_history.ui.theme.pinkTheme import com.acclorite.books_history.ui.theme.purpleTheme +import com.acclorite.books_history.ui.theme.redTheme +import com.acclorite.books_history.ui.theme.yellowTheme enum class Theme { DYNAMIC, BLUE, PURPLE, GREEN, - PINK + PINK, + YELLOW, + RED } /** @@ -28,6 +32,8 @@ fun String.toTheme(): Theme { "PURPLE" -> Theme.PURPLE "GREEN" -> Theme.GREEN "PINK" -> Theme.PINK + "YELLOW" -> Theme.YELLOW + "RED" -> Theme.RED else -> Theme.BLUE } } @@ -66,9 +72,19 @@ fun colorScheme(theme: Theme, darkTheme: Boolean): ColorScheme { return greenTheme(isDark = darkTheme) } - else -> { + Theme.PINK -> { /* Pink Theme */ return pinkTheme(isDark = darkTheme) } + + Theme.YELLOW -> { + /* Yellow Theme */ + return yellowTheme(isDark = darkTheme) + } + + Theme.RED -> { + /* Red Theme */ + return redTheme(isDark = darkTheme) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/ui/Elevation.kt b/app/src/main/java/com/acclorite/books_history/ui/Elevation.kt index b6d78eb6..cc0f519a 100644 --- a/app/src/main/java/com/acclorite/books_history/ui/Elevation.kt +++ b/app/src/main/java/com/acclorite/books_history/ui/Elevation.kt @@ -3,5 +3,6 @@ package com.acclorite.books_history.ui import androidx.compose.ui.unit.dp object ElevationDefaults { - val DialogElevation = 6.dp + val Dialog = 6.dp + val BottomSheet = 1.dp } \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/ui/Theme.kt b/app/src/main/java/com/acclorite/books_history/ui/Theme.kt index 60728c1f..ec47cdb2 100644 --- a/app/src/main/java/com/acclorite/books_history/ui/Theme.kt +++ b/app/src/main/java/com/acclorite/books_history/ui/Theme.kt @@ -48,7 +48,6 @@ fun BooksHistoryResurrectionTheme( if (!view.isInEditMode) { SideEffect { val window = (view.context as Activity).window - WindowCompat.setDecorFitsSystemWindows(window, false) window.statusBarColor = Color.Transparent.toArgb() window.navigationBarColor = Color.Transparent.toArgb() diff --git a/app/src/main/java/com/acclorite/books_history/ui/Transitions.kt b/app/src/main/java/com/acclorite/books_history/ui/Transitions.kt index 3680fa67..34fe0e01 100644 --- a/app/src/main/java/com/acclorite/books_history/ui/Transitions.kt +++ b/app/src/main/java/com/acclorite/books_history/ui/Transitions.kt @@ -6,16 +6,14 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally -import androidx.compose.foundation.layout.Row import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier object Transitions { val DefaultTransitionIn = fadeIn(tween(300)) val DefaultTransitionOut = fadeOut(tween(300)) - val SlidingTransitionIn = fadeIn(tween(300), initialAlpha = 0.6f) + + val SlidingTransitionIn = fadeIn(tween(300)) + slideInHorizontally(tween(300)) { it / 6 } val SlidingTransitionOut = fadeOut(tween(300)) + @@ -39,15 +37,6 @@ fun DefaultTransition( } } -@Composable -fun TopAppBarTransition(visible: Boolean, content: @Composable (() -> Unit)) { - DefaultTransition(visible = visible) { - Row(verticalAlignment = Alignment.CenterVertically) { - content() - } - } -} - diff --git a/app/src/main/java/com/acclorite/books_history/ui/theme/RedTheme.kt b/app/src/main/java/com/acclorite/books_history/ui/theme/RedTheme.kt new file mode 100644 index 00000000..43d2261f --- /dev/null +++ b/app/src/main/java/com/acclorite/books_history/ui/theme/RedTheme.kt @@ -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 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/ui/theme/YellowTheme.kt b/app/src/main/java/com/acclorite/books_history/ui/theme/YellowTheme.kt new file mode 100644 index 00000000..5d1e46ee --- /dev/null +++ b/app/src/main/java/com/acclorite/books_history/ui/theme/YellowTheme.kt @@ -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 + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/acclorite/books_history/util/Constants.kt b/app/src/main/java/com/acclorite/books_history/util/Constants.kt index 90637381..80922c76 100644 --- a/app/src/main/java/com/acclorite/books_history/util/Constants.kt +++ b/app/src/main/java/com/acclorite/books_history/util/Constants.kt @@ -5,10 +5,14 @@ import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import com.acclorite.books_history.R +import com.acclorite.books_history.domain.model.Book +import com.acclorite.books_history.domain.model.Category import com.acclorite.books_history.domain.model.FontWithName +import com.acclorite.books_history.ui.Theme object Constants { + // SavedStateHandle constants(actually just all settings params) const val LANGUAGE = "language" const val THEME = "theme" const val DARK_THEME = "dark_theme" @@ -22,13 +26,27 @@ object Constants { const val PARAGRAPH_HEIGHT = "paragraph_height" const val PARAGRAPH_INDENTATION = "paragraph_indentation" + // Supported file extensions val EXTENSIONS = listOf(".txt", ".pdf", ".epub") + // Supported languages val LANGUAGES = listOf( Pair("en", "English"), - Pair("uk", "Українська") + Pair("uk", "Українська"), ) + // Supported themes + val THEMES = listOf( + Pair(Theme.DYNAMIC, UIText.StringResource(R.string.dynamic_theme)), + Pair(Theme.BLUE, UIText.StringResource(R.string.blue_theme)), + Pair(Theme.PURPLE, UIText.StringResource(R.string.purple_theme)), + Pair(Theme.GREEN, UIText.StringResource(R.string.green_theme)), + Pair(Theme.PINK, UIText.StringResource(R.string.pink_theme)), + Pair(Theme.YELLOW, UIText.StringResource(R.string.yellow_theme)), + Pair(Theme.RED, UIText.StringResource(R.string.red_theme)), + ) + + // Fonts for Reader val FONTS = listOf( FontWithName( "Raleway", @@ -141,4 +159,18 @@ object Constants { ) ) ) + + val EMPTY_BOOK = Book( + null, + "", + UIText.StringValue(""), + null, + emptyList(), + 0f, + null, + "", + null, + Category.READING, + null + ) } \ No newline at end of file diff --git a/app/src/main/res/drawable/splash_icon.xml b/app/src/main/res/drawable/splash_icon.xml index 8400ffaa..a088bfa2 100644 --- a/app/src/main/res/drawable/splash_icon.xml +++ b/app/src/main/res/drawable/splash_icon.xml @@ -1,8 +1,8 @@ - + android:gravity="center" /> \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/app_icon.xml b/app/src/main/res/mipmap-anydpi-v26/app_icon.xml index 69b65dc0..be2d22d2 100644 --- a/app/src/main/res/mipmap-anydpi-v26/app_icon.xml +++ b/app/src/main/res/mipmap-anydpi-v26/app_icon.xml @@ -1,6 +1,6 @@ - - - + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/app_icon_round.xml b/app/src/main/res/mipmap-anydpi-v26/app_icon_round.xml index 69b65dc0..be2d22d2 100644 --- a/app/src/main/res/mipmap-anydpi-v26/app_icon_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/app_icon_round.xml @@ -1,6 +1,6 @@ - - - + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 73065e41..b14b1add 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -1,5 +1,6 @@ +