0.6.0 - BookInfo Screen

This commit is contained in:
Acclorite 2024-02-23 16:44:29 +02:00
parent 090d5fc003
commit 03b1ef48c2
74 changed files with 2410 additions and 498 deletions

View file

@ -4,7 +4,29 @@
<value>
<entry key="app">
<State>
<targetSelectedWithDropDown>
<runningDeviceTargetSelectedWithDropDown>
<Target>
<type value="RUNNING_DEVICE_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Phone_13_.avd" />
</Key>
</deviceKey>
</Target>
</runningDeviceTargetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2024-02-22T17:49:53.931710327Z" />
<multipleDevicesSelectedInDropDown value="true" />
<targetsSelectedWithDialog>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_3_XL_API_26.avd" />
</Key>
</deviceKey>
</Target>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
@ -14,27 +36,6 @@
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2024-02-15T11:58:02.950581796Z" />
<targetsSelectedWithDialog>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="C:\Users\Administrator\.android\avd\Resizable_Experimental_API_33.avd" />
</Key>
</deviceKey>
</Target>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="C:\Users\Administrator\.android\avd\Pixel_2_API_26.avd" />
</Key>
</deviceKey>
</Target>
</targetsSelectedWithDialog>
</State>
</entry>

View file

@ -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

View file

@ -1,21 +1,43 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Proguard-rules
-dontwarn org.xmlpull.v1.**
-dontwarn org.kxml2.io.**
-dontwarn android.content.res.**
-dontwarn org.slf4j.impl.StaticLoggerBinder
-dontwarn com.gemalto.jp2.JP2Decoder
-dontwarn com.google.auto.service.AutoService
-dontwarn javax.annotation.processing.AbstractProcessor
-dontwarn javax.annotation.processing.Filer
-dontwarn javax.annotation.processing.Messager
-dontwarn javax.annotation.processing.ProcessingEnvironment
-dontwarn javax.lang.model.SourceVersion
-dontwarn javax.lang.model.element.AnnotationMirror
-dontwarn javax.lang.model.element.AnnotationValue
-dontwarn javax.lang.model.element.Element
-dontwarn javax.lang.model.element.ElementKind
-dontwarn javax.lang.model.element.ElementVisitor
-dontwarn javax.lang.model.element.ExecutableElement
-dontwarn javax.lang.model.element.Modifier
-dontwarn javax.lang.model.element.Name
-dontwarn javax.lang.model.element.PackageElement
-dontwarn javax.lang.model.element.TypeElement
-dontwarn javax.lang.model.element.TypeParameterElement
-dontwarn javax.lang.model.element.VariableElement
-dontwarn javax.lang.model.type.ArrayType
-dontwarn javax.lang.model.type.DeclaredType
-dontwarn javax.lang.model.type.ExecutableType
-dontwarn javax.lang.model.type.TypeKind
-dontwarn javax.lang.model.type.TypeMirror
-dontwarn javax.lang.model.type.TypeVariable
-dontwarn javax.lang.model.type.TypeVisitor
-dontwarn javax.lang.model.util.AbstractAnnotationValueVisitor8
-dontwarn javax.lang.model.util.AbstractTypeVisitor8
-dontwarn javax.lang.model.util.ElementFilter
-dontwarn javax.lang.model.util.Elements
-dontwarn javax.lang.model.util.SimpleElementVisitor8
-dontwarn javax.lang.model.util.SimpleTypeVisitor7
-dontwarn javax.lang.model.util.SimpleTypeVisitor8
-dontwarn javax.lang.model.util.Types
-dontwarn javax.tools.Diagnostic$Kind
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keep class org.xmlpull.** { *; }
-keepclassmembers class org.xmlpull.** { *; }

View file

@ -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.
*

View file

@ -6,7 +6,6 @@
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.INTERNET" />
<application
android:name=".util.BookApplication"

View file

@ -17,12 +17,14 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import com.acclorite.books_history.domain.model.Book
import com.acclorite.books_history.presentation.NavigationHost
import com.acclorite.books_history.presentation.Screen
import com.acclorite.books_history.presentation.components.BottomNavigationBar
import com.acclorite.books_history.presentation.main.MainEvent
import com.acclorite.books_history.presentation.main.MainViewModel
import com.acclorite.books_history.presentation.screens.book_info.BookInfoScreen
import com.acclorite.books_history.presentation.screens.browse.BrowseScreen
import com.acclorite.books_history.presentation.screens.library.LibraryScreen
import com.acclorite.books_history.presentation.screens.settings.SettingsScreen
@ -41,6 +43,7 @@ class MainActivity : ComponentActivity() {
private val mainViewModel: MainViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mainViewModel.init(this)
@ -49,7 +52,9 @@ class MainActivity : ComponentActivity() {
!mainViewModel.isReady.value
}
}
enableEdgeToEdge()
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
val updating = mainViewModel.updating.collectAsState().value
@ -95,6 +100,18 @@ class MainActivity : ComponentActivity() {
}
}
// Book Info
composable(
screen = Screen.BOOK_INFO,
enterAnim = Transitions.SlidingTransitionIn,
exitAnim = Transitions.SlidingTransitionOut
) {
BookInfoScreen(
navigator = this@NavigationHost
)
}
// Settings
composable(
screen = Screen.SETTINGS,
@ -107,6 +124,7 @@ class MainActivity : ComponentActivity() {
)
}
// Nested categories
composable(
screen = Screen.GENERAL_SETTINGS,
enterAnim = Transitions.SlidingTransitionIn,

View file

@ -15,7 +15,6 @@ import javax.inject.Singleton
object AppModule {
@Provides
@Singleton
fun provideBookDao(app: Application): BookDao {

View file

@ -4,6 +4,6 @@ import androidx.datastore.preferences.core.Preferences
import kotlinx.coroutines.flow.Flow
interface DataStore {
suspend fun <T> getData (key: Preferences.Key<T>, defaultValue: T): Flow<T>
suspend fun <T> putData (key: Preferences.Key<T>, value: T)
suspend fun <T> getData(key: Preferences.Key<T>, defaultValue: T): Flow<T>
suspend fun <T> putData(key: Preferences.Key<T>, value: T)
}

View file

@ -14,18 +14,18 @@ import javax.inject.Inject
private val Context.dataStore by preferencesDataStore("data_store")
class DataStoreImpl @Inject constructor(context: Application): DataStore {
class DataStoreImpl @Inject constructor(context: Application) : DataStore {
private val dataStore = context.dataStore
override suspend fun <T> getData(key: Preferences.Key<T>, defaultValue: T): Flow<T> =
dataStore.data.catch { exception ->
if (exception is IOException){
if (exception is IOException) {
emit(emptyPreferences())
} else{
} else {
throw exception
}
}.map { preferences ->
val result = preferences[key]?: defaultValue
val result = preferences[key] ?: defaultValue
result
}

View file

@ -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
}

View file

@ -4,7 +4,6 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory
import com.acclorite.books_history.R
import com.acclorite.books_history.data.local.dto.BookEntity
import com.acclorite.books_history.data.local.room.BookDao
import com.acclorite.books_history.data.parser.EpubFileParser
import com.acclorite.books_history.data.parser.PdfFileParser
import com.acclorite.books_history.data.parser.TxtFileParser
@ -20,28 +19,14 @@ class BookMapperImpl @Inject constructor(
private val pdfFileParser: PdfFileParser,
private val epubFileParser: EpubFileParser
) : BookMapper {
override suspend fun toBookEntity(book: Book, database: BookDao): BookEntity {
override suspend fun toBookEntity(book: Book): BookEntity {
val stream = ByteArrayOutputStream()
book.coverImage?.compress(Bitmap.CompressFormat.JPEG, 50, stream)
if (book.file == null) {
val bookWithoutFile = database.findBookById(book.id!!)
book.coverImage?.compress(Bitmap.CompressFormat.JPEG, 30, stream)
return BookEntity(
id = book.id,
title = book.title,
filePath = bookWithoutFile.filePath,
progress = book.progress,
lastOpened = book.lastOpened,
image = stream.toByteArray(),
category = book.category
)
}
return BookEntity(
id = book.id,
title = book.title,
filePath = book.file.path,
filePath = book.filePath,
progress = book.progress,
lastOpened = book.lastOpened,
image = stream.toByteArray(),
@ -73,6 +58,7 @@ class BookMapperImpl @Inject constructor(
text = emptyList(),
progress = bookEntity.progress,
file = file,
filePath = bookEntity.filePath,
lastOpened = bookEntity.lastOpened,
category = bookEntity.category,
coverImage = if (bookEntity.image.isNotEmpty()) image else null
@ -86,6 +72,7 @@ class BookMapperImpl @Inject constructor(
description = null,
progress = bookEntity.progress,
file = null,
filePath = bookEntity.filePath,
lastOpened = bookEntity.lastOpened,
category = bookEntity.category,
coverImage = if (bookEntity.image.isNotEmpty()) image else null

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -66,15 +66,15 @@ class BookRepositoryImpl @Inject constructor(
}
override suspend fun insertBooks(books: List<Book>) {
database.insertBooks(books.map { bookMapper.toBookEntity(it, database) })
database.insertBooks(books.map { bookMapper.toBookEntity(it) })
}
override suspend fun updateBooks(books: List<Book>) {
database.updateBooks(books.map { bookMapper.toBookEntity(it, database) })
database.updateBooks(books.map { bookMapper.toBookEntity(it) })
}
override suspend fun deleteBooks(books: List<Book>) {
database.deleteBooks(books.map { bookMapper.toBookEntity(it, database) })
database.deleteBooks(books.map { bookMapper.toBookEntity(it) })
}
override suspend fun <T> retrieveDataFromDataStore(

View file

@ -14,6 +14,7 @@ data class Book(
val text: List<StringWithId> = emptyList(),
val progress: Float,
val file: File?,
val filePath: String,
val lastOpened: Long?,
val category: Category,
val coverImage: Bitmap?

View file

@ -0,0 +1,12 @@
package com.acclorite.books_history.domain.use_case
import com.acclorite.books_history.domain.model.Book
import com.acclorite.books_history.domain.repository.BookRepository
import javax.inject.Inject
class DeleteBooks @Inject constructor(private val repository: BookRepository) {
suspend fun execute(books: List<Book>) {
repository.deleteBooks(books)
}
}

View file

@ -7,7 +7,7 @@ import javax.inject.Inject
class GetDatastore @Inject constructor(private val repository: BookRepository) {
suspend fun <T> execute (key: Preferences.Key<T>, defaultValue: T): Flow<T> {
suspend fun <T> execute(key: Preferences.Key<T>, defaultValue: T): Flow<T> {
return repository.retrieveDataFromDataStore(key, defaultValue)
}
}

View file

@ -6,7 +6,7 @@ import javax.inject.Inject
class SetDatastore @Inject constructor(private val repository: BookRepository) {
suspend fun <T> execute (key: Preferences.Key<T>, value: T) {
suspend fun <T> execute(key: Preferences.Key<T>, value: T) {
repository.putDataToDataStore(key, value)
}
}

View file

@ -42,7 +42,7 @@ enum class Screen {
HISTORY,
BROWSE,
BOOKS_INFO,
BOOK_INFO,
READER,
SETTINGS,
@ -134,10 +134,6 @@ class Navigator @AssistedInject constructor(
return currentScreen
}
fun init(startScreen: Screen) {
savedStateHandle[CURRENT_SCREEN] = startScreen
}
/**
* Animated Screen. Using in [NavigationHost]. Be sure to not use the same [screen] parameter twice, it'll override the highest one in your code.
*
@ -187,10 +183,11 @@ fun NavigationHost(
) {
val activity = LocalContext.current as ComponentActivity
activity.apply {
val navigator by viewModels<Navigator>(
val navigator by activity.viewModels<Navigator>(
extrasProducer = {
defaultViewModelCreationExtras.withCreationCallback<Navigator.Factory> { factory ->
activity
.defaultViewModelCreationExtras
.withCreationCallback<Navigator.Factory> { factory ->
factory.create(startScreen)
}
}
@ -211,5 +208,4 @@ fun NavigationHost(
)
content(navigator)
}
}

View file

@ -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
)
)
}

View file

@ -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),

View file

@ -1,7 +1,6 @@
package com.acclorite.books_history.presentation.components
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@ -18,13 +17,12 @@ import com.acclorite.books_history.domain.model.NavigationBarItem
* Bottom Navigation Bar Item, uses default [NavigationBarItem].
*/
@Composable
fun BottomNavigationBarItem(
modifier: Modifier,
fun RowScope.BottomNavigationBarItem(
modifier: Modifier = Modifier,
item: NavigationBarItem,
isSelected: Boolean,
onClick: () -> Unit
) {
Row(modifier.fillMaxHeight()) {
NavigationBarItem(
label = {
Text(
@ -46,6 +44,7 @@ fun BottomNavigationBarItem(
modifier = Modifier.size(24.dp)
)
},
modifier = modifier,
colors = NavigationBarItemDefaults.colors(
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
@ -53,5 +52,4 @@ fun BottomNavigationBarItem(
disabledTextColor = MaterialTheme.colorScheme.onSurface
)
)
}
}

View file

@ -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
)
}
)
}

View file

@ -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) {

View file

@ -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) {

View file

@ -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)
)
}

View file

@ -0,0 +1,282 @@
package com.acclorite.books_history.presentation.screens.book_info
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.viewModels
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.acclorite.books_history.R
import com.acclorite.books_history.domain.model.Book
import com.acclorite.books_history.presentation.Argument
import com.acclorite.books_history.presentation.Navigator
import com.acclorite.books_history.presentation.Screen
import com.acclorite.books_history.presentation.screens.book_info.components.BookInfoBackground
import com.acclorite.books_history.presentation.screens.book_info.components.BookInfoChangeCoverBottomSheet
import com.acclorite.books_history.presentation.screens.book_info.components.BookInfoDeleteDialog
import com.acclorite.books_history.presentation.screens.book_info.components.BookInfoDescriptionSection
import com.acclorite.books_history.presentation.screens.book_info.components.BookInfoDetailsBottomSheet
import com.acclorite.books_history.presentation.screens.book_info.components.BookInfoInfoSection
import com.acclorite.books_history.presentation.screens.book_info.components.BookInfoMoreDropDown
import com.acclorite.books_history.presentation.screens.book_info.components.BookInfoMoveDialog
import com.acclorite.books_history.presentation.screens.book_info.components.BookInfoStatisticSection
import com.acclorite.books_history.presentation.screens.book_info.data.BookInfoEvent
import com.acclorite.books_history.presentation.screens.book_info.data.BookInfoViewModel
import com.acclorite.books_history.presentation.screens.library.data.LibraryEvent
import com.acclorite.books_history.presentation.screens.library.data.LibraryViewModel
import com.acclorite.books_history.ui.DefaultTransition
import com.acclorite.books_history.ui.Transitions
import com.acclorite.books_history.ui.elevation
import com.acclorite.books_history.util.Constants
import dagger.hilt.android.lifecycle.withCreationCallback
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BookInfoScreen(
libraryViewModel: LibraryViewModel = hiltViewModel(),
navigator: Navigator
) {
val context = LocalContext.current
val viewModel by (context as ComponentActivity).viewModels<BookInfoViewModel>(
extrasProducer = {
val book = navigator.retrieveArgument("book") as? Book
if (book == null) {
navigator.navigateBack()
}
context.defaultViewModelCreationExtras
.withCreationCallback<BookInfoViewModel.Factory> { factory ->
factory.create(book ?: Constants.EMPTY_BOOK)
}
}
)
val state by viewModel.state.collectAsState()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val scrollState = rememberScrollState()
LaunchedEffect(Unit) {
viewModel.init(
navigator,
failure = {
Toast.makeText(
context,
context.getString(
R.string.file_not_found,
it.takeLast(40)
),
Toast.LENGTH_LONG
).show()
}
)
}
if (state.showChangeCoverBottomSheet) {
BookInfoChangeCoverBottomSheet(libraryViewModel = libraryViewModel, viewModel = viewModel)
}
if (state.showDetailsBottomSheet) {
BookInfoDetailsBottomSheet(viewModel = viewModel)
}
if (state.showDeleteDialog) {
BookInfoDeleteDialog(
libraryViewModel = libraryViewModel,
viewModel = viewModel,
navigator = navigator
)
}
if (state.showMoveDialog) {
BookInfoMoveDialog(
libraryViewModel = libraryViewModel,
viewModel = viewModel,
navigator = navigator
)
}
Scaffold(
Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection)
.windowInsetsPadding(WindowInsets.navigationBars),
containerColor = MaterialTheme.colorScheme.surface,
topBar = {
TopAppBar(
scrollBehavior = scrollBehavior,
navigationIcon = {
IconButton(onClick = {
navigator.navigateBack()
}) {
Icon(
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
contentDescription = "Go back",
tint = MaterialTheme.colorScheme.onSurface
)
}
},
title = {},
actions = {
Box {
DefaultTransition(
visible = !state.editTitle
) {
BookInfoMoreDropDown(viewModel = viewModel)
}
androidx.compose.animation.AnimatedVisibility(
visible = state.editTitle,
enter = Transitions.DefaultTransitionIn,
exit = fadeOut(tween(200))
) {
IconButton(
enabled = state.titleValue.isNotBlank() &&
state.titleValue.trim() != state.book.title.trim(),
onClick = {
viewModel.onEvent(BookInfoEvent.OnUpdateTitle(
refreshList = {
libraryViewModel.onEvent(
LibraryEvent.OnLoadList
)
}
))
Toast.makeText(
context,
context.getString(R.string.title_changed),
Toast.LENGTH_LONG
).show()
}
) {
Icon(
imageVector = Icons.Default.Done,
contentDescription = "Apply changes",
tint =
if (state.titleValue.isNotBlank()
&& state.titleValue != state.book.title
)
MaterialTheme.colorScheme.onSurface
else MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent,
scrolledContainerColor = MaterialTheme.elevation()
)
)
}
) { paddingValues ->
if (state.book.coverImage != null) {
BookInfoBackground(image = state.book.coverImage!!.asImageBitmap())
}
Box(
Modifier
.fillMaxSize()
.padding(top = paddingValues.calculateTopPadding())
) {
Column(
Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(horizontal = 24.dp)
) {
Spacer(modifier = Modifier.height(12.dp))
// Info
BookInfoInfoSection(viewModel = viewModel, book = state.book)
Spacer(modifier = Modifier.height(24.dp))
// Statistic
BookInfoStatisticSection(book = state.book)
Spacer(modifier = Modifier.height(24.dp))
// Description
BookInfoDescriptionSection(book = state.book)
}
if (state.book.file != null) {
FloatingActionButton(
onClick = {
navigator.navigate(
Screen.READER, Argument(
"book", state.book
)
)
},
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp),
shape = MaterialTheme.shapes.large,
elevation = FloatingActionButtonDefaults.elevation(),
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
content = {
Row(verticalAlignment = Alignment.CenterVertically) {
Spacer(modifier = Modifier.width(13.dp))
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = "Continue reading",
Modifier.size(24.dp)
)
AnimatedVisibility(
visible = scrollState.value < 5
) {
Text(
text = stringResource(
if (state.book.progress == 0f) R.string.start
else R.string.continue_read
),
style = MaterialTheme.typography.labelLarge,
maxLines = 1,
modifier = Modifier.padding(start = 8.dp)
)
}
Spacer(modifier = Modifier.width(16.dp))
}
}
)
}
}
}
}

View file

@ -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
)
)
)
}

View file

@ -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))
}
}

View file

@ -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,
)
}
}
}

View file

@ -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()
}
)
}

View file

@ -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))
}

View file

@ -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))
}
}

View file

@ -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,
)
}
}

View file

@ -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
)
}
}
}
}

View file

@ -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)
}
}
}
}

View file

@ -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))
}
}
}
)
}

View file

@ -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
)
}

View file

@ -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()
}

View file

@ -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,
)

View file

@ -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
}
}

View file

@ -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))

View file

@ -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),

View file

@ -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) {

View file

@ -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()
}

View file

@ -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,

View file

@ -16,7 +16,6 @@ import com.acclorite.books_history.presentation.Argument
import com.acclorite.books_history.presentation.Screen
import com.acclorite.books_history.util.Resource
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionState
import com.google.accompanist.permissions.isGranted
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
@ -52,11 +51,6 @@ class BrowseViewModel @Inject constructor(
event.permissionState.launchPermissionRequest()
viewModelScope.launch {
_state.update {
it.copy(
showErrorMessage = true
)
}
for (i in 0 until 100) {
if (!event.permissionState.status.isGranted) {
delay(100)
@ -147,7 +141,20 @@ class BrowseViewModel @Inject constructor(
}
is BrowseEvent.OnPermissionCheck -> {
checkPermission(event.permissionState)
val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
val isPermissionGranted =
if (!legacyPermission) Environment.isExternalStorageManager()
else event.permissionState.status.isGranted
if (isPermissionGranted) {
return
}
_state.update {
it.copy(
requestPermissionDialog = true,
showErrorMessage = false
)
}
}
is BrowseEvent.OnSelectFile -> {
@ -299,6 +306,7 @@ class BrowseViewModel @Inject constructor(
viewModelScope.launch {
val booksToInsert = _state.value.selectedBooks
.filterIsInstance<NullableBook.NotNull>()
.filter { it.book!!.second }
.map { it.book!!.first }
if (booksToInsert.isEmpty()) {
@ -308,8 +316,7 @@ class BrowseViewModel @Inject constructor(
insertBooks.execute(booksToInsert)
val books = fastGetBooks.execute("")
event.resetScroll()
_state.update {
it.copy(
showAddingDialog = false
@ -331,25 +338,25 @@ class BrowseViewModel @Inject constructor(
getFilesFromDownloads("")
}
}
}
}
private fun checkPermission(permissionState: PermissionState) {
val legacyPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.R
val isPermissionGranted = if (!legacyPermission) Environment.isExternalStorageManager()
else permissionState.status.isGranted
if (isPermissionGranted) {
return
}
is BrowseEvent.OnUpdateScrollIndex -> {
_state.update {
it.copy(
requestPermissionDialog = true,
showErrorMessage = false
scrollIndex = event.index
)
}
}
is BrowseEvent.OnUpdateScrollOffset -> {
_state.update {
it.copy(
scrollOffset = event.offset
)
}
}
}
}
private suspend fun getFilesFromDownloads(query: String = _state.value.searchQuery) {
getFilesFromDownloads.execute(query).collect { result ->
when (result) {

View file

@ -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 = {

View file

@ -44,6 +44,7 @@ import com.acclorite.books_history.util.removeTrailingZero
@Composable
fun LibraryBookItem(
book: Pair<Book, Boolean>,
modifier: Modifier = Modifier,
onCoverImageClick: () -> Unit,
onLongClick: () -> Unit,
onButtonClick: () -> Unit
@ -65,7 +66,7 @@ fun LibraryBookItem(
)
Column(
Modifier
modifier
.padding(3.dp)
.background(
animatedBackgroundColor,

View file

@ -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()
}
)
}

View file

@ -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)

View file

@ -11,6 +11,7 @@ import com.acclorite.books_history.domain.model.Category
sealed class LibraryEvent {
data class OnPreloadBooks(val books: List<Book>) : LibraryEvent()
data object OnRefreshList : LibraryEvent()
data object OnLoadList : LibraryEvent()
data class OnScrollToPage(val index: Int, val pagerState: PagerState) : LibraryEvent()
data class OnUpdateCurrentPage(val page: Int) : LibraryEvent()
data object OnSearchShowHide : LibraryEvent()
@ -23,6 +24,6 @@ sealed class LibraryEvent {
data object OnShowHideMoveDialog : LibraryEvent()
data class OnSelectCategory(val category: Category) : LibraryEvent()
data class OnMoveBooks(val pagerState: PagerState) : LibraryEvent()
data object OnShowHideDeleteDialog : LibraryEvent()
data object OnDeleteBooks : LibraryEvent()
}

View file

@ -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)
}
}
}
}

View file

@ -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
) {

View file

@ -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) {

View file

@ -27,21 +27,20 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.acclorite.books_history.R
import com.acclorite.books_history.ui.Theme
import com.acclorite.books_history.ui.colorScheme
import com.acclorite.books_history.util.UIText
@Composable
fun AppearanceSettingsThemeSwitcherItem(
theme: Theme,
theme: Pair<Theme, UIText>,
darkTheme: Boolean,
selected: Boolean,
onClick: () -> Unit
) {
val colorScheme = colorScheme(theme, darkTheme)
val colorScheme = colorScheme(theme.first, darkTheme)
Column(
modifier = Modifier.fillMaxWidth(),
@ -133,13 +132,7 @@ fun AppearanceSettingsThemeSwitcherItem(
}
Spacer(modifier = Modifier.height(6.dp))
Text(
text = when (theme) {
Theme.BLUE -> stringResource(id = R.string.blue_theme)
Theme.PURPLE -> stringResource(id = R.string.purple_theme)
Theme.PINK -> stringResource(id = R.string.pink_theme)
Theme.GREEN -> stringResource(id = R.string.green_theme)
Theme.DYNAMIC -> stringResource(id = R.string.dynamic_theme)
},
text = theme.second.asString(),
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyMedium,
overflow = TextOverflow.Ellipsis,

View file

@ -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)
}
}
}

View file

@ -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
}

View file

@ -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()

View file

@ -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()
}
}
}

View file

@ -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
)
}
}

View file

@ -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
)
}
}

View file

@ -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
)
}

View file

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/app_icon_colored"
<item
android:drawable="@drawable/app_icon_colored"
android:height="75.dp"
android:width="75.dp"
android:gravity="center"
/>
android:gravity="center" />
</layer-list>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/app_icon_background"/>
<foreground android:drawable="@mipmap/app_icon_foreground"/>
<monochrome android:drawable="@mipmap/app_icon_monochrome"/>
<background android:drawable="@color/app_icon_background" />
<foreground android:drawable="@mipmap/app_icon_foreground" />
<monochrome android:drawable="@mipmap/app_icon_monochrome" />
</adaptive-icon>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/app_icon_background"/>
<foreground android:drawable="@mipmap/app_icon_foreground"/>
<monochrome android:drawable="@mipmap/app_icon_monochrome"/>
<background android:drawable="@color/app_icon_background" />
<foreground android:drawable="@mipmap/app_icon_foreground" />
<monochrome android:drawable="@mipmap/app_icon_monochrome" />
</adaptive-icon>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.Start.Splash" parent="Theme.SplashScreen">
<item name="android:forceDarkAllowed" tools:ignore="NewApi">false</item>
<item name="postSplashScreenTheme">@style/Theme.BooksHistoryResurrection</item>

View file

@ -12,8 +12,11 @@
<!-- Dialog Labels -->
<string name="storage_permission">Надати Дозвіл?</string>
<string name="add_books">Додати ці книги?</string>
<string name="add_books">Додати книги?</string>
<string name="move_books">Перемістити книги?</string>
<string name="delete_books">Видалити книги?</string>
<string name="delete_book">Видалити книгу?</string>
<string name="move_book">Перемістити книгу?</string>
<!-- Dialog Descriptions -->
<string name="storage_permission_description">
@ -28,11 +31,26 @@
Виберіть категорію, в яку ви хочете перемістити всі обрані книги(%1$s).
Всі обрані книги будуть переміщені в обрану категорію.
</string>
<string name="delete_books_description">
Це видалить всі обрані книги(%1$s) з бази даних.
Це не видалить обрані книги з вашого пристрою.
</string>
<string name="delete_book_description">
Це видалить цю книгу з бази даних.
Це не видалить цю книгу з вашого пристрою.
</string>
<string name="move_book_description">
Виберіть категорію, в яку ви хочете перемістити цю книгу.
Ця книга буде переміщена в обрану категорію.
</string>
<!-- Errors -->
<string name="error_permission">
Нам потрібен дозвіл на сховище, щоб просканувати вашу теку Завантажень та отримати книги
</string>
<string name="error_no_description">
Немає опису.
</string>
<!-- Is Empty messages -->
<string name="browse_empty">
@ -44,7 +62,8 @@
<!-- Query strings -->
<string name="selected_items_count_query">%1$s обрано</string>
<string name="search_query">Знайти %1$s...</string>
<string name="search_query">Знайти %1$s…</string>
<string name="you_read_query">Ви прочитали %1$s цієї книги.</string>
<!-- Action Texts -->
<string name="cancel">Відмінити</string>
@ -54,6 +73,10 @@
<string name="add">Додати</string>
<string name="add_book">Додати книгу</string>
<string name="move">Перемістити</string>
<string name="delete">Видалити</string>
<string name="start">Почати</string>
<string name="continue_read">Продовжити</string>
<string name="never">Ніколи</string>
<!-- Items -->
<string name="files">файли</string>
@ -61,6 +84,12 @@
<!-- Book -->
<string name="unknown_author">Невідомий</string>
<string name="read_more">Вам потрібно читати більше!</string>
<string name="read_keep">Так тримати!</string>
<string name="change_cover">Змінити обкладинку</string>
<string name="change_cover_desc">Виберіть нову обкладинку з ваших фото</string>
<string name="delete_cover">Видалити обкладинку</string>
<string name="delete_cover_desc">Видаліть нинішню обкладинку книги</string>
<!-- Tabs -->
<string name="reading_tab">Читаю</string>
@ -84,16 +113,38 @@
<string name="dark_theme_on">Ввімкнено</string>
<string name="dark_theme_follow_system">Тема системи</string>
<!-- Theme properties -->
<!-- Themes -->
<string name="dynamic_theme">Хаос</string>
<string name="blue_theme">Зоряний шлях</string>
<string name="blue_theme">Нептун</string>
<string name="purple_theme">Супернова</string>
<string name="green_theme">Земля</string>
<string name="pink_theme">Пульсар</string>
<string name="yellow_theme">Венера</string>
<string name="red_theme">Марс</string>
<!-- Toast messages -->
<string name="press_again_toast">Натисніть ще раз, щоб вийти</string>
<string name="books_added">Всі книги були успішно додані.</string>
<string name="books_moved">Всі книги були успішно переміщені.</string>
<string name="books_deleted">Всі книги були успішно видалені.</string>
<string name="cover_image_changed">Обкладинка була успішно змінена.</string>
<string name="cover_image_deleted">Обкладинка була успішно видалена.</string>
<string name="title_changed">Назва була успішно змінена.</string>
<string name="file_not_found">Файл \"%1$s\" не знайдено.</string>
<string name="book_deleted">Ця книга була успішно видалена.</string>
<string name="book_moved">Ця книга була успішно переміщена.</string>
<string name="copied">Скопійовано</string>
<!-- DropDown properties -->
<string name="move_this_book">Перемістити</string>
<string name="delete_this_book">Видалити</string>
<string name="details_book">Подробиці</string>
<!-- BookInfo details -->
<string name="file_name">Ім\'я файлу</string>
<string name="file_path">Шлях до файлу</string>
<string name="file_last_opened">Останнє відкриття файлу</string>
<string name="file_size">Розмір файлу</string>
<string name="unknown">Невідомо</string>
</resources>

View file

@ -1,6 +1,6 @@
<resources>
<!-- Basic Strings -->
<string name="app_version" translatable="false">0.5.0</string>
<string name="app_version" translatable="false">0.6.0</string>
<string name="app_name">Book\'s History</string>
<!-- Screens -->
@ -13,8 +13,11 @@
<!-- Dialog Labels -->
<string name="storage_permission">Grant Permission?</string>
<string name="add_books">Add these books?</string>
<string name="add_books">Add books?</string>
<string name="move_books">Move books?</string>
<string name="delete_books">Delete books?</string>
<string name="delete_book">Delete book?</string>
<string name="move_book">Move book?</string>
<!-- Dialog Descriptions -->
<string name="storage_permission_description">
@ -29,11 +32,26 @@
Choose the category to which you want to move all the selected books(%1$s).
All selected books will be moved to the selected category.
</string>
<string name="delete_books_description">
It will delete all selected books(%1$s) from the database.
It will not delete selected books from your device.
</string>
<string name="delete_book_description">
It will delete this book from the database.
It will not delete this book from your device.
</string>
<string name="move_book_description">
Choose the category to which you want to move this book.
This book will be moved to the selected category.
</string>
<!-- Errors -->
<string name="error_permission">
We need storage permission to scan your Downloads directory and get books
</string>
<string name="error_no_description">
No description.
</string>
<!-- Is Empty messages -->
<string name="browse_empty">
@ -45,7 +63,8 @@
<!-- Query strings -->
<string name="selected_items_count_query">%1$s selected</string>
<string name="search_query">Search %1$s...</string>
<string name="search_query">Search %1$s…</string>
<string name="you_read_query">You read %1$s of this book.</string>
<!-- Action Texts -->
<string name="cancel">Cancel</string>
@ -55,6 +74,10 @@
<string name="add">Add</string>
<string name="add_book">Add a book</string>
<string name="move">Move</string>
<string name="delete">Delete</string>
<string name="start">Start</string>
<string name="continue_read">Continue</string>
<string name="never">Never</string>
<!-- Items -->
<string name="files">files</string>
@ -62,6 +85,12 @@
<!-- Book -->
<string name="unknown_author">Unknown</string>
<string name="read_more">You need to read more!</string>
<string name="read_keep">Keep it up!</string>
<string name="change_cover">Change cover image</string>
<string name="change_cover_desc">Choose new cover from your photos</string>
<string name="delete_cover">Delete cover image</string>
<string name="delete_cover_desc">Delete current cover image of the book</string>
<!-- Tabs -->
<string name="reading_tab">Reading</string>
@ -85,16 +114,38 @@
<string name="dark_theme_on">Enabled</string>
<string name="dark_theme_follow_system">Follow system</string>
<!-- Theme properties -->
<!-- Themes -->
<string name="dynamic_theme">Chaos</string>
<string name="blue_theme">Milky Way</string>
<string name="blue_theme">Neptune</string>
<string name="purple_theme">Supernova</string>
<string name="green_theme">Earth</string>
<string name="pink_theme">Pulsar</string>
<string name="yellow_theme">Venus</string>
<string name="red_theme">Mars</string>
<!-- Toast messages -->
<string name="press_again_toast">Press back again to exit</string>
<string name="books_added">All books were successfully added.</string>
<string name="books_moved">All books were successfully moved.</string>
<string name="books_deleted">All books were successfully deleted.</string>
<string name="cover_image_changed">Cover Image was successfully changed.</string>
<string name="cover_image_deleted">Cover Image was successfully deleted.</string>
<string name="title_changed">Title was successfully changed.</string>
<string name="file_not_found">File \"%1$s\" was not found.</string>
<string name="book_deleted">This book was successfully deleted.</string>
<string name="book_moved">This book was successfully moved.</string>
<string name="copied">Copied</string>
<!-- DropDown properties -->
<string name="move_this_book">Move</string>
<string name="delete_this_book">Delete</string>
<string name="details_book">Details</string>
<!-- BookInfo details -->
<string name="file_name">File name</string>
<string name="file_path">File path</string>
<string name="file_last_opened">File last opened</string>
<string name="file_size">File size</string>
<string name="unknown">Unknown</string>
</resources>

View file

@ -3,6 +3,9 @@
<style name="Theme.BooksHistoryResurrection" parent="android:Theme.Material.Light.NoActionBar">
<item name="android:forceDarkAllowed" tools:ignore="NewApi">false</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
</style>
<style name="Theme.Start.Splash" parent="Theme.SplashScreen">

View file

@ -1,9 +1,8 @@
package com.acclorite.books_history
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*

View file

@ -1,45 +0,0 @@
package com.acclorite.books_history.data.repository
import androidx.datastore.preferences.core.Preferences
import com.acclorite.books_history.domain.model.Book
import com.acclorite.books_history.domain.repository.BookRepository
import com.acclorite.books_history.util.Resource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class FakeBookRepository: BookRepository {
private val booksList = mutableListOf<Book>()
override suspend fun getBooks(query: String): Flow<Resource<List<Book>>> {
return flow { emit(Resource.Success(booksList.filter { it.title.lowercase().contains(query) })) }
}
override suspend fun insertBooks(books: List<Book>) {
booksList.addAll(books)
}
override suspend fun updateBooks(books: List<Book>) {
booksList.forEachIndexed { index, book ->
books.forEach {
if(it.id == book.id)
booksList[index] = it
}
}
}
override suspend fun deleteBooks(books: List<Book>) {
booksList.removeAll(books)
}
override suspend fun <T> retrieveDataFromDataStore(
key: Preferences.Key<T>,
defaultValue: T
): Flow<T> {
return flow { emit(defaultValue) }
}
override suspend fun <T> putDataToDataStore(key: Preferences.Key<T>, value: T) {
TODO("Not yet implemented")
}
}

View file

@ -1,50 +0,0 @@
package com.acclorite.books_history.domain.use_case
import com.acclorite.books_history.data.repository.FakeBookRepository
import com.acclorite.books_history.domain.model.Book
import com.acclorite.books_history.domain.model.Category
import com.google.common.truth.Truth.*
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
class InsertBooksTest {
private lateinit var insertBooks: InsertBooks
private lateinit var fakeRepository: FakeBookRepository
@Before
fun setUp() {
fakeRepository = FakeBookRepository()
insertBooks = InsertBooks(fakeRepository)
}
@Test
fun `Insert all books, inserted book`() = runBlocking {
val booksToInsert = mutableListOf<Book>()
('a'..'z').forEachIndexed { index, c ->
booksToInsert.add(
Book(
index,
c.toString(),
c.toString(),
c.toString(),
emptyList(),
(index / 100f),
null,
null,
Category.READING,
coverImage = null
)
)
}
booksToInsert.shuffle()
val oldRepoSize = fakeRepository.getBooks("").first().data?.size ?: return@runBlocking
insertBooks.execute(booksToInsert)
val repoSize = fakeRepository.getBooks("").first().data?.size ?: return@runBlocking
assertThat(oldRepoSize).isLessThan(repoSize)
}
}